Resolve excessive profile reloads (#883)

Optimize ProfileManager in several ways:

- Refine control over objectWillChange
- Observe search separately
- Store subscriptions separately (local, remote, search)
- Fix multiple local updates on save/remove/foreground (updating
allProfiles manually)
- Update the library with more optimized NE reloads
- Cancel pending remote import before a new one
- Yield 100ms between imports
- Reorganize code

Extras:

- Only use background context in provider repositories
- Externalize tunnel receipt URL, do not hardcode BundleConfiguration
- Improve some logging

Self-reminder: NEVER use a Core Data background context to observe
changes in CloudKit containers. They just won't be notified (e.g. in
NSFetchedResultsController).

Fixes #857
This commit is contained in:
Davide 2024-11-17 11:34:43 +01:00 committed by GitHub
parent 9e5beff23a
commit d3e344670b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 190 additions and 169 deletions

View File

@ -41,7 +41,7 @@
"kind" : "remoteSourceControl",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
"state" : {
"revision" : "d74cd0f02ba844beff2be55bf5f93796a3c43a6d"
"revision" : "39cd828d3ee7cb502c4c0e36e3dc42e45bfae10b"
}
},
{

View File

@ -44,7 +44,7 @@ let package = Package(
],
dependencies: [
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.11.0"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "d74cd0f02ba844beff2be55bf5f93796a3c43a6d"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "39cd828d3ee7cb502c4c0e36e3dc42e45bfae10b"),
// .package(path: "../../../passepartoutkit-source"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", from: "0.9.1"),
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"),

View File

@ -31,28 +31,22 @@ import Foundation
import PassepartoutKit
extension AppData {
public static func cdProviderRepositoryV3(
context: NSManagedObjectContext,
backgroundContext: NSManagedObjectContext
) -> ProviderRepository {
CDProviderRepositoryV3(context: context, backgroundContext: backgroundContext)
public static func cdProviderRepositoryV3(context: NSManagedObjectContext) -> ProviderRepository {
CDProviderRepositoryV3(context: context)
}
}
actor CDProviderRepositoryV3: NSObject, ProviderRepository {
private nonisolated let context: NSManagedObjectContext
private nonisolated let backgroundContext: NSManagedObjectContext
private nonisolated let providersSubject: CurrentValueSubject<[ProviderMetadata], Never>
private nonisolated let lastUpdateSubject: CurrentValueSubject<[ProviderID: Date], Never>
private nonisolated let providersController: NSFetchedResultsController<CDProviderV3>
init(context: NSManagedObjectContext, backgroundContext: NSManagedObjectContext) {
init(context: NSManagedObjectContext) {
self.context = context
self.backgroundContext = backgroundContext
providersSubject = CurrentValueSubject([])
lastUpdateSubject = CurrentValueSubject([:])
@ -90,7 +84,7 @@ actor CDProviderRepositoryV3: NSObject, ProviderRepository {
}
func store(_ index: [ProviderMetadata]) async throws {
try await backgroundContext.perform { [weak self] in
try await context.perform { [weak self] in
guard let self else {
return
}
@ -101,25 +95,25 @@ actor CDProviderRepositoryV3: NSObject, ProviderRepository {
let lastUpdatesByProvider = results.reduce(into: [:]) {
$0[$1.providerId] = $1.lastUpdate
}
results.forEach(backgroundContext.delete)
results.forEach(context.delete)
// replace but retain last update
let mapper = CoreDataMapper(context: backgroundContext)
let mapper = CoreDataMapper(context: context)
index.forEach {
let lastUpdate = lastUpdatesByProvider[$0.id.rawValue]
mapper.cdProvider(from: $0, lastUpdate: lastUpdate)
}
try backgroundContext.save()
try context.save()
} catch {
backgroundContext.rollback()
context.rollback()
throw error
}
}
}
func store(_ infrastructure: VPNInfrastructure, for providerId: ProviderID) async throws {
try await backgroundContext.perform { [weak self] in
try await context.perform { [weak self] in
guard let self else {
return
}
@ -138,15 +132,15 @@ actor CDProviderRepositoryV3: NSObject, ProviderRepository {
let serverRequest = CDVPNServerV3.fetchRequest()
serverRequest.predicate = predicate
let servers = try serverRequest.execute()
servers.forEach(backgroundContext.delete)
servers.forEach(context.delete)
let presetRequest = CDVPNPresetV3.fetchRequest()
presetRequest.predicate = predicate
let presets = try presetRequest.execute()
presets.forEach(backgroundContext.delete)
presets.forEach(context.delete)
// create new entities
let mapper = CoreDataMapper(context: backgroundContext)
let mapper = CoreDataMapper(context: context)
try infrastructure.servers.forEach {
try mapper.cdServer(from: $0)
}
@ -154,9 +148,9 @@ actor CDProviderRepositoryV3: NSObject, ProviderRepository {
try mapper.cdPreset(from: $0)
}
try backgroundContext.save()
try context.save()
} catch {
backgroundContext.rollback()
context.rollback()
throw error
}
}

View File

@ -57,9 +57,6 @@ public final class ProfileManager: ObservableObject {
// MARK: State
@Published
private var profiles: [Profile]
private var allProfiles: [Profile.ID: Profile] {
didSet {
reloadFilteredProfiles(with: searchSubject.value)
@ -68,14 +65,11 @@ public final class ProfileManager: ObservableObject {
private var allRemoteProfiles: [Profile.ID: Profile]
private var filteredProfiles: [Profile]
@Published
public private(set) var isRemoteImportingEnabled: Bool
public var isReady: Bool {
waitingObservers.isEmpty
}
@Published
private var waitingObservers: Set<Observer>
// MARK: Publishers
@ -84,9 +78,13 @@ public final class ProfileManager: ObservableObject {
private let searchSubject: CurrentValueSubject<String, Never>
private var subscriptions: Set<AnyCancellable>
private var localSubscription: AnyCancellable?
private var remoteSubscriptions: Set<AnyCancellable>
private var remoteSubscription: AnyCancellable?
private var searchSubscription: AnyCancellable?
private var remoteImportTask: Task<Void, Never>?
// for testing/previews
public init(profiles: [Profile]) {
@ -98,18 +96,18 @@ public final class ProfileManager: ObservableObject {
mirrorsRemoteRepository = false
processor = nil
self.profiles = []
allProfiles = profiles.reduce(into: [:]) {
$0[$1.id] = $1
}
allRemoteProfiles = [:]
filteredProfiles = []
isRemoteImportingEnabled = false
waitingObservers = []
didChange = PassthroughSubject()
searchSubject = CurrentValueSubject("")
subscriptions = []
remoteSubscriptions = []
observeSearch()
}
public init(
@ -126,9 +124,9 @@ public final class ProfileManager: ObservableObject {
self.mirrorsRemoteRepository = mirrorsRemoteRepository
self.processor = processor
profiles = []
allProfiles = [:]
allRemoteProfiles = [:]
filteredProfiles = []
isRemoteImportingEnabled = false
if remoteRepositoryBlock != nil {
waitingObservers = [.local, .remote]
@ -138,16 +136,20 @@ public final class ProfileManager: ObservableObject {
didChange = PassthroughSubject()
searchSubject = CurrentValueSubject("")
subscriptions = []
remoteSubscriptions = []
observeSearch()
}
}
// MARK: - CRUD
// MARK: - View
extension ProfileManager {
public var isReady: Bool {
waitingObservers.isEmpty
}
public var hasProfiles: Bool {
!profiles.isEmpty
!filteredProfiles.isEmpty
}
public var isSearching: Bool {
@ -155,21 +157,25 @@ extension ProfileManager {
}
public var headers: [ProfileHeader] {
profiles.map {
filteredProfiles.map {
$0.header()
}
}
public func profile(withId profileId: Profile.ID) -> Profile? {
filteredProfiles.first {
$0.id == profileId
}
}
public func search(byName name: String) {
searchSubject.send(name)
}
public func profile(withId profileId: Profile.ID) -> Profile? {
profiles.first {
$0.id == profileId
}
}
// MARK: - CRUD
extension ProfileManager {
public func save(_ originalProfile: Profile, force: Bool = false, remotelyShared: Bool? = nil) async throws {
let profile: Profile
if force {
@ -194,7 +200,6 @@ extension ProfileManager {
try await backupRepository.saveProfile(profile)
}
}
allProfiles[profile.id] = profile
didChange.send(.save(profile))
} else {
pp_log(.App.profiles, .notice, "\tProfile \(profile.id) not modified, not saving")
@ -203,8 +208,8 @@ extension ProfileManager {
pp_log(.App.profiles, .fault, "\tUnable to save profile \(profile.id): \(error)")
throw error
}
do {
if let remotelyShared, let remoteRepository {
do {
if remotelyShared {
pp_log(.App.profiles, .notice, "\tEnable remote sharing of profile \(profile.id)...")
try await remoteRepository.saveProfile(profile)
@ -212,11 +217,11 @@ extension ProfileManager {
pp_log(.App.profiles, .notice, "\tDisable remote sharing of profile \(profile.id)...")
try await remoteRepository.removeProfiles(withIds: [profile.id])
}
}
} catch {
pp_log(.App.profiles, .fault, "\tUnable to save/remove remote profile \(profile.id): \(error)")
throw error
}
}
pp_log(.App.profiles, .notice, "Finished saving profile \(profile.id)")
}
@ -227,21 +232,8 @@ extension ProfileManager {
public func remove(withIds profileIds: [Profile.ID]) async {
pp_log(.App.profiles, .notice, "Remove profiles \(profileIds)...")
do {
// remove local profiles
var newAllProfiles = allProfiles
try await repository.removeProfiles(withIds: profileIds)
profileIds.forEach {
newAllProfiles.removeValue(forKey: $0)
}
// remove remote counterpart too
try? await remoteRepository?.removeProfiles(withIds: profileIds)
profileIds.forEach {
allRemoteProfiles.removeValue(forKey: $0)
}
// publish update
allProfiles = newAllProfiles
didChange.send(.remove(profileIds))
} catch {
pp_log(.App.profiles, .fault, "Unable to remove profiles \(profileIds): \(error)")
@ -299,7 +291,7 @@ extension ProfileManager {
private extension ProfileManager {
func firstUniqueName(from name: String) -> String {
let allNames = profiles.map(\.name)
let allNames = Set(allProfiles.values.map(\.name))
var newName = name
var index = 1
while true {
@ -315,27 +307,18 @@ private extension ProfileManager {
// MARK: - Observation
extension ProfileManager {
public func observeLocal(searchDebounce: Int = 200) async throws {
subscriptions.removeAll()
public func observeLocal() async throws {
localSubscription = nil
let initialProfiles = try await repository.fetchProfiles()
reloadLocalProfiles(initialProfiles)
repository
localSubscription = repository
.profilesPublisher
.dropFirst()
.receive(on: DispatchQueue.main)
.sink { [weak self] in
self?.reloadLocalProfiles($0)
}
.store(in: &subscriptions)
searchSubject
.debounce(for: .milliseconds(searchDebounce), scheduler: DispatchQueue.main)
.sink { [weak self] in
self?.performSearch($0)
}
.store(in: &subscriptions)
}
public func observeRemote(_ isRemoteImportingEnabled: Bool) async throws {
@ -348,21 +331,30 @@ extension ProfileManager {
}
self.isRemoteImportingEnabled = isRemoteImportingEnabled
remoteSubscriptions.removeAll()
remoteSubscription = nil
let newRepository = remoteRepositoryBlock(isRemoteImportingEnabled)
let initialProfiles = try await newRepository.fetchProfiles()
reloadRemoteProfiles(initialProfiles)
remoteRepository = newRepository
remoteRepository?
remoteSubscription = remoteRepository?
.profilesPublisher
.dropFirst()
.receive(on: DispatchQueue.main)
.sink { [weak self] in
self?.reloadRemoteProfiles($0)
}
.store(in: &remoteSubscriptions)
}
}
private extension ProfileManager {
func observeSearch(debounce: Int = 200) {
searchSubscription = searchSubject
.debounce(for: .milliseconds(debounce), scheduler: DispatchQueue.main)
.sink { [weak self] in
self?.reloadFilteredProfiles(with: $0)
}
}
}
@ -373,11 +365,31 @@ private extension ProfileManager {
$0[$1.id] = $1
}
if waitingObservers.contains(.local) {
waitingObservers.remove(.local)
waitingObservers.remove(.local) // @Published
}
deleteExcludedProfiles()
objectWillChange.send()
}
func reloadRemoteProfiles(_ result: [Profile]) {
pp_log(.App.profiles, .info, "Reload remote profiles: \(result.map(\.id))")
allRemoteProfiles = result.reduce(into: [:]) {
$0[$1.id] = $1
}
if waitingObservers.contains(.remote) {
waitingObservers.remove(.remote) // @Published
}
importRemoteProfiles(result)
objectWillChange.send()
}
// should not be imported at all, but you never know
if let processor {
func deleteExcludedProfiles() {
guard let processor else {
return
}
let idsToRemove: [Profile.ID] = allProfiles
.filter {
!processor.isIncluded($0.value)
@ -391,49 +403,49 @@ private extension ProfileManager {
}
}
}
}
func reloadRemoteProfiles(_ result: [Profile]) {
pp_log(.App.profiles, .info, "Reload remote profiles: \(result.map(\.id))")
allRemoteProfiles = result.reduce(into: [:]) {
$0[$1.id] = $1
}
if waitingObservers.contains(.remote) {
waitingObservers.remove(.remote)
}
Task.detached { [weak self] in
guard let self else {
func importRemoteProfiles(_ profiles: [Profile]) {
guard !profiles.isEmpty else {
return
}
pp_log(.App.profiles, .info, "Start importing remote profiles...")
assert(result.count == Set(result.map(\.id)).count, "Remote repository must not have duplicates")
pp_log(.App.profiles, .info, "Start importing remote profiles: \(profiles.map(\.id)))")
assert(profiles.count == Set(profiles.map(\.id)).count, "Remote repository must not have duplicates")
pp_log(.App.profiles, .debug, "Local attributes:")
let localAttributes: [Profile.ID: ProfileAttributes] = await allProfiles.values.reduce(into: [:]) {
let localAttributes: [Profile.ID: ProfileAttributes] = allProfiles.values.reduce(into: [:]) {
$0[$1.id] = $1.attributes
pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)")
}
pp_log(.App.profiles, .debug, "Remote attributes:")
let remoteAttributes: [Profile.ID: ProfileAttributes] = result.reduce(into: [:]) {
let remoteAttributes: [Profile.ID: ProfileAttributes] = profiles.reduce(into: [:]) {
$0[$1.id] = $1.attributes
pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)")
}
let profilesToImport = result
let remotelyDeletedIds = await Set(allProfiles.keys).subtracting(Set(allRemoteProfiles.keys))
let remotelyDeletedIds = Set(allProfiles.keys).subtracting(Set(allRemoteProfiles.keys))
let mirrorsRemoteRepository = mirrorsRemoteRepository
let previousTask = remoteImportTask
remoteImportTask = Task.detached { [weak self] in
guard let self else {
return
}
if let previousTask {
pp_log(.App.profiles, .info, "Cancel ongoing remote import...")
previousTask.cancel()
await previousTask.value
}
var idsToRemove: [Profile.ID] = []
if !remotelyDeletedIds.isEmpty {
pp_log(.App.profiles, .info, "Will \(mirrorsRemoteRepository ? "delete" : "retain") local profiles not present in remote repository: \(remotelyDeletedIds)")
if mirrorsRemoteRepository {
idsToRemove.append(contentsOf: remotelyDeletedIds)
}
}
for remoteProfile in profilesToImport {
for remoteProfile in profiles {
do {
guard processor?.isIncluded(remoteProfile) ?? true else {
pp_log(.App.profiles, .info, "Will delete non-included remote profile \(remoteProfile.id)")
@ -452,19 +464,26 @@ private extension ProfileManager {
} catch {
pp_log(.App.profiles, .error, "Unable to import remote profile: \(error)")
}
}
pp_log(.App.profiles, .notice, "Finished importing remote profiles, delete stale profiles: \(idsToRemove)")
try? await repository.removeProfiles(withIds: idsToRemove)
guard !Task.isCancelled else {
pp_log(.App.profiles, .info, "Cancelled import of remote profiles: \(profiles.map(\.id))")
return
}
}
func performSearch(_ search: String) {
pp_log(.App.profiles, .notice, "Filter profiles with '\(search)'")
reloadFilteredProfiles(with: search)
pp_log(.App.profiles, .notice, "Finished importing remote profiles, delete stale profiles: \(idsToRemove)")
do {
try await repository.removeProfiles(withIds: idsToRemove)
} catch {
pp_log(.App.profiles, .error, "Unable to delete stale profiles: \(error)")
}
// yield a little bit
try? await Task.sleep(for: .milliseconds(100))
}
}
func reloadFilteredProfiles(with search: String) {
profiles = allProfiles
filteredProfiles = allProfiles
.values
.filter {
if !search.isEmpty {
@ -475,5 +494,9 @@ private extension ProfileManager {
.sorted {
$0.name.lowercased() < $1.name.lowercased()
}
pp_log(.App.profiles, .notice, "Filter profiles with '\(search)' (\(filteredProfiles.count) results)")
objectWillChange.send()
}
}

View File

@ -261,7 +261,6 @@ extension IAPManager {
}
}
.store(in: &subscriptions)
} catch {
pp_log(.App.iap, .error, "Unable to fetch in-app products: \(error)")
}

View File

@ -50,6 +50,8 @@ public actor CoreDataRepository<CD, T>: NSObject,
private let observingResults: Bool
private let beforeFetch: ((NSFetchRequest<CD>) -> Void)?
private nonisolated let fromMapper: (CD) throws -> T?
private nonisolated let toMapper: (T, NSManagedObjectContext) throws -> CD
@ -58,8 +60,7 @@ public actor CoreDataRepository<CD, T>: NSObject,
private nonisolated let entitiesSubject: CurrentValueSubject<EntitiesResult<T>, Never>
// cannot easily use CD as generic
private var resultsController: NSFetchedResultsController<CD>
private var resultsController: NSFetchedResultsController<CD>?
public init(
context: NSManagedObjectContext,
@ -76,19 +77,11 @@ public actor CoreDataRepository<CD, T>: NSObject,
self.entityName = entityName
self.context = context
self.observingResults = observingResults
self.beforeFetch = beforeFetch
self.fromMapper = fromMapper
self.toMapper = toMapper
self.onResultError = onResultError
entitiesSubject = CurrentValueSubject(EntitiesResult())
let request = NSFetchRequest<CD>(entityName: entityName)
beforeFetch?(request)
resultsController = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil
)
}
public nonisolated var entitiesPublisher: AnyPublisher<EntitiesResult<T>, Never> {
@ -183,17 +176,24 @@ private extension CoreDataRepository {
@discardableResult
func filter(byPredicate predicate: NSPredicate?) async throws -> [T] {
let request = resultsController.fetchRequest
let request = newFetchRequest()
request.predicate = predicate
resultsController = NSFetchedResultsController(
beforeFetch?(request)
let newController = try await context.perform {
let newController = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: context,
managedObjectContext: self.context,
sectionNameKeyPath: nil,
cacheName: nil
)
resultsController.delegate = self
try resultsController.performFetch()
return await sendResults(from: resultsController)
newController.delegate = self
try newController.performFetch()
return newController
}
resultsController = newController
return await sendResults(from: newController)
}
@discardableResult

View File

@ -43,6 +43,8 @@ public final class AppContext: ObservableObject {
public let tunnel: ExtendedTunnel
private let tunnelReceiptURL: URL
private var launchTask: Task<Void, Error>?
private var pendingTask: Task<Void, Never>?
@ -55,7 +57,8 @@ public final class AppContext: ObservableObject {
profileManager: ProfileManager,
providerManager: ProviderManager,
registry: Registry,
tunnel: ExtendedTunnel
tunnel: ExtendedTunnel,
tunnelReceiptURL: URL
) {
self.iapManager = iapManager
self.migrationManager = migrationManager
@ -63,6 +66,7 @@ public final class AppContext: ObservableObject {
self.providerManager = providerManager
self.registry = registry
self.tunnel = tunnel
self.tunnelReceiptURL = tunnelReceiptURL
subscriptions = []
}
}
@ -84,12 +88,14 @@ private extension AppContext {
func onLaunch() async throws {
pp_log(.app, .notice, "Application did launch")
pp_log(.App.profiles, .info, "Read and observe local profiles...")
pp_log(.App.profiles, .info, "\tRead and observe local profiles...")
try await profileManager.observeLocal()
pp_log(.App.profiles, .info, "\tObserve in-app events...")
iapManager.observeObjects()
await iapManager.reloadReceipt()
pp_log(.App.profiles, .info, "\tObserve eligible features...")
iapManager
.$eligibleFeatures
.removeDuplicates()
@ -100,6 +106,7 @@ private extension AppContext {
}
.store(in: &subscriptions)
pp_log(.App.profiles, .info, "\tObserve changes in ProfileManager...")
profileManager
.didChange
.sink { [weak self] event in
@ -117,21 +124,20 @@ private extension AppContext {
// copy release receipt to tunnel for TestFlight eligibility (once is enough, it won't change)
if let appReceiptURL = Bundle.main.appStoreProductionReceiptURL {
let tunnelReceiptURL = BundleConfiguration.urlForBetaReceipt
do {
pp_log(.App.iap, .info, "Copy release receipt to tunnel...")
pp_log(.App.iap, .info, "\tCopy release receipt to tunnel...")
try? FileManager.default.removeItem(at: tunnelReceiptURL)
try FileManager.default.copyItem(at: appReceiptURL, to: tunnelReceiptURL)
} catch {
pp_log(.App.iap, .error, "Unable to copy release receipt to tunnel: \(error)")
pp_log(.App.iap, .error, "\tUnable to copy release receipt to tunnel: \(error)")
}
}
do {
pp_log(.app, .notice, "Fetch providers index...")
pp_log(.app, .info, "\tFetch providers index...")
try await providerManager.fetchIndex(from: API.shared)
} catch {
pp_log(.app, .error, "Unable to fetch providers index: \(error)")
pp_log(.app, .error, "\tUnable to fetch providers index: \(error)")
}
}
@ -144,10 +150,10 @@ private extension AppContext {
pp_log(.app, .notice, "Application did enter foreground")
pendingTask = Task {
do {
pp_log(.App.profiles, .info, "Refresh local profiles observers...")
pp_log(.App.profiles, .info, "\tRefresh local profiles observers...")
try await profileManager.observeLocal()
} catch {
pp_log(.App.profiles, .error, "Unable to re-observe local profiles: \(error)")
pp_log(.App.profiles, .error, "\tUnable to re-observe local profiles: \(error)")
}
await iapManager.reloadReceipt()
@ -165,10 +171,10 @@ private extension AppContext {
// toggle sync based on .sharing eligibility
let isEligibleForSharing = features.contains(.sharing)
do {
pp_log(.App.profiles, .info, "Refresh remote profiles observers (eligible=\(isEligibleForSharing), CloudKit=\(isCloudKitEnabled))...")
pp_log(.App.profiles, .info, "\tRefresh remote profiles observers (eligible=\(isEligibleForSharing), CloudKit=\(isCloudKitEnabled))...")
try await profileManager.observeRemote(isEligibleForSharing && isCloudKitEnabled)
} catch {
pp_log(.App.profiles, .error, "Unable to re-observe remote profiles: \(error)")
pp_log(.App.profiles, .error, "\tUnable to re-observe remote profiles: \(error)")
}
}
await pendingTask?.value
@ -180,29 +186,29 @@ private extension AppContext {
pp_log(.app, .notice, "Application did save profile (\(profile.id))")
guard profile.id == tunnel.currentProfile?.id else {
pp_log(.app, .debug, "Profile \(profile.id) is not current, do nothing")
pp_log(.app, .debug, "\tProfile \(profile.id) is not current, do nothing")
return
}
guard [.active, .activating].contains(tunnel.status) else {
pp_log(.app, .debug, "Connection is not active (\(tunnel.status)), do nothing")
pp_log(.app, .debug, "\tConnection is not active (\(tunnel.status)), do nothing")
return
}
pendingTask = Task {
do {
if profile.isInteractive {
pp_log(.app, .info, "Profile \(profile.id) is interactive, disconnect")
pp_log(.app, .info, "\tProfile \(profile.id) is interactive, disconnect")
try await tunnel.disconnect()
return
}
do {
pp_log(.app, .info, "Reconnect profile \(profile.id)")
pp_log(.app, .info, "\tReconnect profile \(profile.id)")
try await tunnel.connect(with: profile)
} catch {
pp_log(.app, .error, "Unable to reconnect profile \(profile.id), disconnect: \(error)")
pp_log(.app, .error, "\tUnable to reconnect profile \(profile.id), disconnect: \(error)")
try await tunnel.disconnect()
}
} catch {
pp_log(.app, .error, "Unable to reinstate connection on save profile \(profile.id): \(error)")
pp_log(.app, .error, "\tUnable to reinstate connection on save profile \(profile.id): \(error)")
}
}
await pendingTask?.value

View File

@ -83,7 +83,8 @@ extension AppContext {
profileManager: profileManager,
providerManager: providerManager,
registry: registry,
tunnel: tunnel
tunnel: tunnel,
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
)
}
}

View File

@ -87,10 +87,7 @@ extension AppContext {
cloudKitIdentifier: nil,
author: nil
)
let repository = AppData.cdProviderRepositoryV3(
context: store.context,
backgroundContext: store.backgroundContext
)
let repository = AppData.cdProviderRepositoryV3(context: store.backgroundContext)
return ProviderManager(repository: repository)
}()
@ -119,7 +116,8 @@ extension AppContext {
profileManager: profileManager,
providerManager: providerManager,
registry: .shared,
tunnel: tunnel
tunnel: tunnel,
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
)
}()
}