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:
parent
9e5beff23a
commit
d3e344670b
|
@ -41,7 +41,7 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
|
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "d74cd0f02ba844beff2be55bf5f93796a3c43a6d"
|
"revision" : "39cd828d3ee7cb502c4c0e36e3dc42e45bfae10b"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -44,7 +44,7 @@ let package = Package(
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.11.0"),
|
// .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(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", from: "0.9.1"),
|
||||||
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"),
|
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"),
|
||||||
|
|
|
@ -31,28 +31,22 @@ import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
||||||
extension AppData {
|
extension AppData {
|
||||||
public static func cdProviderRepositoryV3(
|
public static func cdProviderRepositoryV3(context: NSManagedObjectContext) -> ProviderRepository {
|
||||||
context: NSManagedObjectContext,
|
CDProviderRepositoryV3(context: context)
|
||||||
backgroundContext: NSManagedObjectContext
|
|
||||||
) -> ProviderRepository {
|
|
||||||
CDProviderRepositoryV3(context: context, backgroundContext: backgroundContext)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actor CDProviderRepositoryV3: NSObject, ProviderRepository {
|
actor CDProviderRepositoryV3: NSObject, ProviderRepository {
|
||||||
private nonisolated let context: NSManagedObjectContext
|
private nonisolated let context: NSManagedObjectContext
|
||||||
|
|
||||||
private nonisolated let backgroundContext: NSManagedObjectContext
|
|
||||||
|
|
||||||
private nonisolated let providersSubject: CurrentValueSubject<[ProviderMetadata], Never>
|
private nonisolated let providersSubject: CurrentValueSubject<[ProviderMetadata], Never>
|
||||||
|
|
||||||
private nonisolated let lastUpdateSubject: CurrentValueSubject<[ProviderID: Date], Never>
|
private nonisolated let lastUpdateSubject: CurrentValueSubject<[ProviderID: Date], Never>
|
||||||
|
|
||||||
private nonisolated let providersController: NSFetchedResultsController<CDProviderV3>
|
private nonisolated let providersController: NSFetchedResultsController<CDProviderV3>
|
||||||
|
|
||||||
init(context: NSManagedObjectContext, backgroundContext: NSManagedObjectContext) {
|
init(context: NSManagedObjectContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.backgroundContext = backgroundContext
|
|
||||||
providersSubject = CurrentValueSubject([])
|
providersSubject = CurrentValueSubject([])
|
||||||
lastUpdateSubject = CurrentValueSubject([:])
|
lastUpdateSubject = CurrentValueSubject([:])
|
||||||
|
|
||||||
|
@ -90,7 +84,7 @@ actor CDProviderRepositoryV3: NSObject, ProviderRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
func store(_ index: [ProviderMetadata]) async throws {
|
func store(_ index: [ProviderMetadata]) async throws {
|
||||||
try await backgroundContext.perform { [weak self] in
|
try await context.perform { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -101,25 +95,25 @@ actor CDProviderRepositoryV3: NSObject, ProviderRepository {
|
||||||
let lastUpdatesByProvider = results.reduce(into: [:]) {
|
let lastUpdatesByProvider = results.reduce(into: [:]) {
|
||||||
$0[$1.providerId] = $1.lastUpdate
|
$0[$1.providerId] = $1.lastUpdate
|
||||||
}
|
}
|
||||||
results.forEach(backgroundContext.delete)
|
results.forEach(context.delete)
|
||||||
|
|
||||||
// replace but retain last update
|
// replace but retain last update
|
||||||
let mapper = CoreDataMapper(context: backgroundContext)
|
let mapper = CoreDataMapper(context: context)
|
||||||
index.forEach {
|
index.forEach {
|
||||||
let lastUpdate = lastUpdatesByProvider[$0.id.rawValue]
|
let lastUpdate = lastUpdatesByProvider[$0.id.rawValue]
|
||||||
mapper.cdProvider(from: $0, lastUpdate: lastUpdate)
|
mapper.cdProvider(from: $0, lastUpdate: lastUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
try backgroundContext.save()
|
try context.save()
|
||||||
} catch {
|
} catch {
|
||||||
backgroundContext.rollback()
|
context.rollback()
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func store(_ infrastructure: VPNInfrastructure, for providerId: ProviderID) async throws {
|
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 {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -138,15 +132,15 @@ actor CDProviderRepositoryV3: NSObject, ProviderRepository {
|
||||||
let serverRequest = CDVPNServerV3.fetchRequest()
|
let serverRequest = CDVPNServerV3.fetchRequest()
|
||||||
serverRequest.predicate = predicate
|
serverRequest.predicate = predicate
|
||||||
let servers = try serverRequest.execute()
|
let servers = try serverRequest.execute()
|
||||||
servers.forEach(backgroundContext.delete)
|
servers.forEach(context.delete)
|
||||||
|
|
||||||
let presetRequest = CDVPNPresetV3.fetchRequest()
|
let presetRequest = CDVPNPresetV3.fetchRequest()
|
||||||
presetRequest.predicate = predicate
|
presetRequest.predicate = predicate
|
||||||
let presets = try presetRequest.execute()
|
let presets = try presetRequest.execute()
|
||||||
presets.forEach(backgroundContext.delete)
|
presets.forEach(context.delete)
|
||||||
|
|
||||||
// create new entities
|
// create new entities
|
||||||
let mapper = CoreDataMapper(context: backgroundContext)
|
let mapper = CoreDataMapper(context: context)
|
||||||
try infrastructure.servers.forEach {
|
try infrastructure.servers.forEach {
|
||||||
try mapper.cdServer(from: $0)
|
try mapper.cdServer(from: $0)
|
||||||
}
|
}
|
||||||
|
@ -154,9 +148,9 @@ actor CDProviderRepositoryV3: NSObject, ProviderRepository {
|
||||||
try mapper.cdPreset(from: $0)
|
try mapper.cdPreset(from: $0)
|
||||||
}
|
}
|
||||||
|
|
||||||
try backgroundContext.save()
|
try context.save()
|
||||||
} catch {
|
} catch {
|
||||||
backgroundContext.rollback()
|
context.rollback()
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,9 +57,6 @@ public final class ProfileManager: ObservableObject {
|
||||||
|
|
||||||
// MARK: State
|
// MARK: State
|
||||||
|
|
||||||
@Published
|
|
||||||
private var profiles: [Profile]
|
|
||||||
|
|
||||||
private var allProfiles: [Profile.ID: Profile] {
|
private var allProfiles: [Profile.ID: Profile] {
|
||||||
didSet {
|
didSet {
|
||||||
reloadFilteredProfiles(with: searchSubject.value)
|
reloadFilteredProfiles(with: searchSubject.value)
|
||||||
|
@ -68,14 +65,11 @@ public final class ProfileManager: ObservableObject {
|
||||||
|
|
||||||
private var allRemoteProfiles: [Profile.ID: Profile]
|
private var allRemoteProfiles: [Profile.ID: Profile]
|
||||||
|
|
||||||
|
private var filteredProfiles: [Profile]
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
public private(set) var isRemoteImportingEnabled: Bool
|
public private(set) var isRemoteImportingEnabled: Bool
|
||||||
|
|
||||||
public var isReady: Bool {
|
|
||||||
waitingObservers.isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
@Published
|
|
||||||
private var waitingObservers: Set<Observer>
|
private var waitingObservers: Set<Observer>
|
||||||
|
|
||||||
// MARK: Publishers
|
// MARK: Publishers
|
||||||
|
@ -84,9 +78,13 @@ public final class ProfileManager: ObservableObject {
|
||||||
|
|
||||||
private let searchSubject: CurrentValueSubject<String, Never>
|
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
|
// for testing/previews
|
||||||
public init(profiles: [Profile]) {
|
public init(profiles: [Profile]) {
|
||||||
|
@ -98,18 +96,18 @@ public final class ProfileManager: ObservableObject {
|
||||||
mirrorsRemoteRepository = false
|
mirrorsRemoteRepository = false
|
||||||
processor = nil
|
processor = nil
|
||||||
|
|
||||||
self.profiles = []
|
|
||||||
allProfiles = profiles.reduce(into: [:]) {
|
allProfiles = profiles.reduce(into: [:]) {
|
||||||
$0[$1.id] = $1
|
$0[$1.id] = $1
|
||||||
}
|
}
|
||||||
allRemoteProfiles = [:]
|
allRemoteProfiles = [:]
|
||||||
|
filteredProfiles = []
|
||||||
isRemoteImportingEnabled = false
|
isRemoteImportingEnabled = false
|
||||||
waitingObservers = []
|
waitingObservers = []
|
||||||
|
|
||||||
didChange = PassthroughSubject()
|
didChange = PassthroughSubject()
|
||||||
searchSubject = CurrentValueSubject("")
|
searchSubject = CurrentValueSubject("")
|
||||||
subscriptions = []
|
|
||||||
remoteSubscriptions = []
|
observeSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
@ -126,9 +124,9 @@ public final class ProfileManager: ObservableObject {
|
||||||
self.mirrorsRemoteRepository = mirrorsRemoteRepository
|
self.mirrorsRemoteRepository = mirrorsRemoteRepository
|
||||||
self.processor = processor
|
self.processor = processor
|
||||||
|
|
||||||
profiles = []
|
|
||||||
allProfiles = [:]
|
allProfiles = [:]
|
||||||
allRemoteProfiles = [:]
|
allRemoteProfiles = [:]
|
||||||
|
filteredProfiles = []
|
||||||
isRemoteImportingEnabled = false
|
isRemoteImportingEnabled = false
|
||||||
if remoteRepositoryBlock != nil {
|
if remoteRepositoryBlock != nil {
|
||||||
waitingObservers = [.local, .remote]
|
waitingObservers = [.local, .remote]
|
||||||
|
@ -138,16 +136,20 @@ public final class ProfileManager: ObservableObject {
|
||||||
|
|
||||||
didChange = PassthroughSubject()
|
didChange = PassthroughSubject()
|
||||||
searchSubject = CurrentValueSubject("")
|
searchSubject = CurrentValueSubject("")
|
||||||
subscriptions = []
|
|
||||||
remoteSubscriptions = []
|
observeSearch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - CRUD
|
// MARK: - View
|
||||||
|
|
||||||
extension ProfileManager {
|
extension ProfileManager {
|
||||||
|
public var isReady: Bool {
|
||||||
|
waitingObservers.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
public var hasProfiles: Bool {
|
public var hasProfiles: Bool {
|
||||||
!profiles.isEmpty
|
!filteredProfiles.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
public var isSearching: Bool {
|
public var isSearching: Bool {
|
||||||
|
@ -155,21 +157,25 @@ extension ProfileManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public var headers: [ProfileHeader] {
|
public var headers: [ProfileHeader] {
|
||||||
profiles.map {
|
filteredProfiles.map {
|
||||||
$0.header()
|
$0.header()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func profile(withId profileId: Profile.ID) -> Profile? {
|
||||||
|
filteredProfiles.first {
|
||||||
|
$0.id == profileId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func search(byName name: String) {
|
public func search(byName name: String) {
|
||||||
searchSubject.send(name)
|
searchSubject.send(name)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func profile(withId profileId: Profile.ID) -> Profile? {
|
// MARK: - CRUD
|
||||||
profiles.first {
|
|
||||||
$0.id == profileId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
extension ProfileManager {
|
||||||
public func save(_ originalProfile: Profile, force: Bool = false, remotelyShared: Bool? = nil) async throws {
|
public func save(_ originalProfile: Profile, force: Bool = false, remotelyShared: Bool? = nil) async throws {
|
||||||
let profile: Profile
|
let profile: Profile
|
||||||
if force {
|
if force {
|
||||||
|
@ -194,7 +200,6 @@ extension ProfileManager {
|
||||||
try await backupRepository.saveProfile(profile)
|
try await backupRepository.saveProfile(profile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allProfiles[profile.id] = profile
|
|
||||||
didChange.send(.save(profile))
|
didChange.send(.save(profile))
|
||||||
} else {
|
} else {
|
||||||
pp_log(.App.profiles, .notice, "\tProfile \(profile.id) not modified, not saving")
|
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)")
|
pp_log(.App.profiles, .fault, "\tUnable to save profile \(profile.id): \(error)")
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
do {
|
|
||||||
if let remotelyShared, let remoteRepository {
|
if let remotelyShared, let remoteRepository {
|
||||||
|
do {
|
||||||
if remotelyShared {
|
if remotelyShared {
|
||||||
pp_log(.App.profiles, .notice, "\tEnable remote sharing of profile \(profile.id)...")
|
pp_log(.App.profiles, .notice, "\tEnable remote sharing of profile \(profile.id)...")
|
||||||
try await remoteRepository.saveProfile(profile)
|
try await remoteRepository.saveProfile(profile)
|
||||||
|
@ -212,11 +217,11 @@ extension ProfileManager {
|
||||||
pp_log(.App.profiles, .notice, "\tDisable remote sharing of profile \(profile.id)...")
|
pp_log(.App.profiles, .notice, "\tDisable remote sharing of profile \(profile.id)...")
|
||||||
try await remoteRepository.removeProfiles(withIds: [profile.id])
|
try await remoteRepository.removeProfiles(withIds: [profile.id])
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
pp_log(.App.profiles, .fault, "\tUnable to save/remove remote profile \(profile.id): \(error)")
|
pp_log(.App.profiles, .fault, "\tUnable to save/remove remote profile \(profile.id): \(error)")
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
}
|
||||||
pp_log(.App.profiles, .notice, "Finished saving profile \(profile.id)")
|
pp_log(.App.profiles, .notice, "Finished saving profile \(profile.id)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,21 +232,8 @@ extension ProfileManager {
|
||||||
public func remove(withIds profileIds: [Profile.ID]) async {
|
public func remove(withIds profileIds: [Profile.ID]) async {
|
||||||
pp_log(.App.profiles, .notice, "Remove profiles \(profileIds)...")
|
pp_log(.App.profiles, .notice, "Remove profiles \(profileIds)...")
|
||||||
do {
|
do {
|
||||||
// remove local profiles
|
|
||||||
var newAllProfiles = allProfiles
|
|
||||||
try await repository.removeProfiles(withIds: profileIds)
|
try await repository.removeProfiles(withIds: profileIds)
|
||||||
profileIds.forEach {
|
|
||||||
newAllProfiles.removeValue(forKey: $0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove remote counterpart too
|
|
||||||
try? await remoteRepository?.removeProfiles(withIds: profileIds)
|
try? await remoteRepository?.removeProfiles(withIds: profileIds)
|
||||||
profileIds.forEach {
|
|
||||||
allRemoteProfiles.removeValue(forKey: $0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// publish update
|
|
||||||
allProfiles = newAllProfiles
|
|
||||||
didChange.send(.remove(profileIds))
|
didChange.send(.remove(profileIds))
|
||||||
} catch {
|
} catch {
|
||||||
pp_log(.App.profiles, .fault, "Unable to remove profiles \(profileIds): \(error)")
|
pp_log(.App.profiles, .fault, "Unable to remove profiles \(profileIds): \(error)")
|
||||||
|
@ -299,7 +291,7 @@ extension ProfileManager {
|
||||||
|
|
||||||
private extension ProfileManager {
|
private extension ProfileManager {
|
||||||
func firstUniqueName(from name: String) -> String {
|
func firstUniqueName(from name: String) -> String {
|
||||||
let allNames = profiles.map(\.name)
|
let allNames = Set(allProfiles.values.map(\.name))
|
||||||
var newName = name
|
var newName = name
|
||||||
var index = 1
|
var index = 1
|
||||||
while true {
|
while true {
|
||||||
|
@ -315,27 +307,18 @@ private extension ProfileManager {
|
||||||
// MARK: - Observation
|
// MARK: - Observation
|
||||||
|
|
||||||
extension ProfileManager {
|
extension ProfileManager {
|
||||||
public func observeLocal(searchDebounce: Int = 200) async throws {
|
public func observeLocal() async throws {
|
||||||
subscriptions.removeAll()
|
localSubscription = nil
|
||||||
|
|
||||||
let initialProfiles = try await repository.fetchProfiles()
|
let initialProfiles = try await repository.fetchProfiles()
|
||||||
reloadLocalProfiles(initialProfiles)
|
reloadLocalProfiles(initialProfiles)
|
||||||
|
|
||||||
repository
|
localSubscription = repository
|
||||||
.profilesPublisher
|
.profilesPublisher
|
||||||
.dropFirst()
|
.dropFirst()
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
self?.reloadLocalProfiles($0)
|
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 {
|
public func observeRemote(_ isRemoteImportingEnabled: Bool) async throws {
|
||||||
|
@ -348,21 +331,30 @@ extension ProfileManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.isRemoteImportingEnabled = isRemoteImportingEnabled
|
self.isRemoteImportingEnabled = isRemoteImportingEnabled
|
||||||
remoteSubscriptions.removeAll()
|
|
||||||
|
|
||||||
|
remoteSubscription = nil
|
||||||
let newRepository = remoteRepositoryBlock(isRemoteImportingEnabled)
|
let newRepository = remoteRepositoryBlock(isRemoteImportingEnabled)
|
||||||
let initialProfiles = try await newRepository.fetchProfiles()
|
let initialProfiles = try await newRepository.fetchProfiles()
|
||||||
reloadRemoteProfiles(initialProfiles)
|
reloadRemoteProfiles(initialProfiles)
|
||||||
remoteRepository = newRepository
|
remoteRepository = newRepository
|
||||||
|
|
||||||
remoteRepository?
|
remoteSubscription = remoteRepository?
|
||||||
.profilesPublisher
|
.profilesPublisher
|
||||||
.dropFirst()
|
.dropFirst()
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
self?.reloadRemoteProfiles($0)
|
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
|
$0[$1.id] = $1
|
||||||
}
|
}
|
||||||
if waitingObservers.contains(.local) {
|
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
|
// 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
|
let idsToRemove: [Profile.ID] = allProfiles
|
||||||
.filter {
|
.filter {
|
||||||
!processor.isIncluded($0.value)
|
!processor.isIncluded($0.value)
|
||||||
|
@ -391,49 +403,49 @@ private extension ProfileManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func reloadRemoteProfiles(_ result: [Profile]) {
|
func importRemoteProfiles(_ profiles: [Profile]) {
|
||||||
pp_log(.App.profiles, .info, "Reload remote profiles: \(result.map(\.id))")
|
guard !profiles.isEmpty else {
|
||||||
allRemoteProfiles = result.reduce(into: [:]) {
|
|
||||||
$0[$1.id] = $1
|
|
||||||
}
|
|
||||||
if waitingObservers.contains(.remote) {
|
|
||||||
waitingObservers.remove(.remote)
|
|
||||||
}
|
|
||||||
|
|
||||||
Task.detached { [weak self] in
|
|
||||||
guard let self else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pp_log(.App.profiles, .info, "Start importing remote profiles...")
|
pp_log(.App.profiles, .info, "Start importing remote profiles: \(profiles.map(\.id)))")
|
||||||
assert(result.count == Set(result.map(\.id)).count, "Remote repository must not have duplicates")
|
assert(profiles.count == Set(profiles.map(\.id)).count, "Remote repository must not have duplicates")
|
||||||
|
|
||||||
pp_log(.App.profiles, .debug, "Local attributes:")
|
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
|
$0[$1.id] = $1.attributes
|
||||||
pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)")
|
pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)")
|
||||||
}
|
}
|
||||||
pp_log(.App.profiles, .debug, "Remote 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
|
$0[$1.id] = $1.attributes
|
||||||
pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)")
|
pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)")
|
||||||
}
|
}
|
||||||
|
|
||||||
let profilesToImport = result
|
let remotelyDeletedIds = Set(allProfiles.keys).subtracting(Set(allRemoteProfiles.keys))
|
||||||
let remotelyDeletedIds = await Set(allProfiles.keys).subtracting(Set(allRemoteProfiles.keys))
|
|
||||||
let mirrorsRemoteRepository = mirrorsRemoteRepository
|
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] = []
|
var idsToRemove: [Profile.ID] = []
|
||||||
if !remotelyDeletedIds.isEmpty {
|
if !remotelyDeletedIds.isEmpty {
|
||||||
pp_log(.App.profiles, .info, "Will \(mirrorsRemoteRepository ? "delete" : "retain") local profiles not present in remote repository: \(remotelyDeletedIds)")
|
pp_log(.App.profiles, .info, "Will \(mirrorsRemoteRepository ? "delete" : "retain") local profiles not present in remote repository: \(remotelyDeletedIds)")
|
||||||
|
|
||||||
if mirrorsRemoteRepository {
|
if mirrorsRemoteRepository {
|
||||||
idsToRemove.append(contentsOf: remotelyDeletedIds)
|
idsToRemove.append(contentsOf: remotelyDeletedIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for remoteProfile in profilesToImport {
|
for remoteProfile in profiles {
|
||||||
do {
|
do {
|
||||||
guard processor?.isIncluded(remoteProfile) ?? true else {
|
guard processor?.isIncluded(remoteProfile) ?? true else {
|
||||||
pp_log(.App.profiles, .info, "Will delete non-included remote profile \(remoteProfile.id)")
|
pp_log(.App.profiles, .info, "Will delete non-included remote profile \(remoteProfile.id)")
|
||||||
|
@ -452,19 +464,26 @@ private extension ProfileManager {
|
||||||
} catch {
|
} catch {
|
||||||
pp_log(.App.profiles, .error, "Unable to import remote profile: \(error)")
|
pp_log(.App.profiles, .error, "Unable to import remote profile: \(error)")
|
||||||
}
|
}
|
||||||
}
|
guard !Task.isCancelled else {
|
||||||
pp_log(.App.profiles, .notice, "Finished importing remote profiles, delete stale profiles: \(idsToRemove)")
|
pp_log(.App.profiles, .info, "Cancelled import of remote profiles: \(profiles.map(\.id))")
|
||||||
try? await repository.removeProfiles(withIds: idsToRemove)
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func performSearch(_ search: String) {
|
pp_log(.App.profiles, .notice, "Finished importing remote profiles, delete stale profiles: \(idsToRemove)")
|
||||||
pp_log(.App.profiles, .notice, "Filter profiles with '\(search)'")
|
do {
|
||||||
reloadFilteredProfiles(with: search)
|
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) {
|
func reloadFilteredProfiles(with search: String) {
|
||||||
profiles = allProfiles
|
filteredProfiles = allProfiles
|
||||||
.values
|
.values
|
||||||
.filter {
|
.filter {
|
||||||
if !search.isEmpty {
|
if !search.isEmpty {
|
||||||
|
@ -475,5 +494,9 @@ private extension ProfileManager {
|
||||||
.sorted {
|
.sorted {
|
||||||
$0.name.lowercased() < $1.name.lowercased()
|
$0.name.lowercased() < $1.name.lowercased()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pp_log(.App.profiles, .notice, "Filter profiles with '\(search)' (\(filteredProfiles.count) results)")
|
||||||
|
|
||||||
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -261,7 +261,6 @@ extension IAPManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &subscriptions)
|
.store(in: &subscriptions)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
pp_log(.App.iap, .error, "Unable to fetch in-app products: \(error)")
|
pp_log(.App.iap, .error, "Unable to fetch in-app products: \(error)")
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,8 @@ public actor CoreDataRepository<CD, T>: NSObject,
|
||||||
|
|
||||||
private let observingResults: Bool
|
private let observingResults: Bool
|
||||||
|
|
||||||
|
private let beforeFetch: ((NSFetchRequest<CD>) -> Void)?
|
||||||
|
|
||||||
private nonisolated let fromMapper: (CD) throws -> T?
|
private nonisolated let fromMapper: (CD) throws -> T?
|
||||||
|
|
||||||
private nonisolated let toMapper: (T, NSManagedObjectContext) throws -> CD
|
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>
|
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(
|
public init(
|
||||||
context: NSManagedObjectContext,
|
context: NSManagedObjectContext,
|
||||||
|
@ -76,19 +77,11 @@ public actor CoreDataRepository<CD, T>: NSObject,
|
||||||
self.entityName = entityName
|
self.entityName = entityName
|
||||||
self.context = context
|
self.context = context
|
||||||
self.observingResults = observingResults
|
self.observingResults = observingResults
|
||||||
|
self.beforeFetch = beforeFetch
|
||||||
self.fromMapper = fromMapper
|
self.fromMapper = fromMapper
|
||||||
self.toMapper = toMapper
|
self.toMapper = toMapper
|
||||||
self.onResultError = onResultError
|
self.onResultError = onResultError
|
||||||
entitiesSubject = CurrentValueSubject(EntitiesResult())
|
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> {
|
public nonisolated var entitiesPublisher: AnyPublisher<EntitiesResult<T>, Never> {
|
||||||
|
@ -183,17 +176,24 @@ private extension CoreDataRepository {
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func filter(byPredicate predicate: NSPredicate?) async throws -> [T] {
|
func filter(byPredicate predicate: NSPredicate?) async throws -> [T] {
|
||||||
let request = resultsController.fetchRequest
|
let request = newFetchRequest()
|
||||||
request.predicate = predicate
|
request.predicate = predicate
|
||||||
resultsController = NSFetchedResultsController(
|
beforeFetch?(request)
|
||||||
|
|
||||||
|
let newController = try await context.perform {
|
||||||
|
let newController = NSFetchedResultsController(
|
||||||
fetchRequest: request,
|
fetchRequest: request,
|
||||||
managedObjectContext: context,
|
managedObjectContext: self.context,
|
||||||
sectionNameKeyPath: nil,
|
sectionNameKeyPath: nil,
|
||||||
cacheName: nil
|
cacheName: nil
|
||||||
)
|
)
|
||||||
resultsController.delegate = self
|
newController.delegate = self
|
||||||
try resultsController.performFetch()
|
try newController.performFetch()
|
||||||
return await sendResults(from: resultsController)
|
return newController
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsController = newController
|
||||||
|
return await sendResults(from: newController)
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
|
|
|
@ -43,6 +43,8 @@ public final class AppContext: ObservableObject {
|
||||||
|
|
||||||
public let tunnel: ExtendedTunnel
|
public let tunnel: ExtendedTunnel
|
||||||
|
|
||||||
|
private let tunnelReceiptURL: URL
|
||||||
|
|
||||||
private var launchTask: Task<Void, Error>?
|
private var launchTask: Task<Void, Error>?
|
||||||
|
|
||||||
private var pendingTask: Task<Void, Never>?
|
private var pendingTask: Task<Void, Never>?
|
||||||
|
@ -55,7 +57,8 @@ public final class AppContext: ObservableObject {
|
||||||
profileManager: ProfileManager,
|
profileManager: ProfileManager,
|
||||||
providerManager: ProviderManager,
|
providerManager: ProviderManager,
|
||||||
registry: Registry,
|
registry: Registry,
|
||||||
tunnel: ExtendedTunnel
|
tunnel: ExtendedTunnel,
|
||||||
|
tunnelReceiptURL: URL
|
||||||
) {
|
) {
|
||||||
self.iapManager = iapManager
|
self.iapManager = iapManager
|
||||||
self.migrationManager = migrationManager
|
self.migrationManager = migrationManager
|
||||||
|
@ -63,6 +66,7 @@ public final class AppContext: ObservableObject {
|
||||||
self.providerManager = providerManager
|
self.providerManager = providerManager
|
||||||
self.registry = registry
|
self.registry = registry
|
||||||
self.tunnel = tunnel
|
self.tunnel = tunnel
|
||||||
|
self.tunnelReceiptURL = tunnelReceiptURL
|
||||||
subscriptions = []
|
subscriptions = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,12 +88,14 @@ private extension AppContext {
|
||||||
func onLaunch() async throws {
|
func onLaunch() async throws {
|
||||||
pp_log(.app, .notice, "Application did launch")
|
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()
|
try await profileManager.observeLocal()
|
||||||
|
|
||||||
|
pp_log(.App.profiles, .info, "\tObserve in-app events...")
|
||||||
iapManager.observeObjects()
|
iapManager.observeObjects()
|
||||||
await iapManager.reloadReceipt()
|
await iapManager.reloadReceipt()
|
||||||
|
|
||||||
|
pp_log(.App.profiles, .info, "\tObserve eligible features...")
|
||||||
iapManager
|
iapManager
|
||||||
.$eligibleFeatures
|
.$eligibleFeatures
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
|
@ -100,6 +106,7 @@ private extension AppContext {
|
||||||
}
|
}
|
||||||
.store(in: &subscriptions)
|
.store(in: &subscriptions)
|
||||||
|
|
||||||
|
pp_log(.App.profiles, .info, "\tObserve changes in ProfileManager...")
|
||||||
profileManager
|
profileManager
|
||||||
.didChange
|
.didChange
|
||||||
.sink { [weak self] event in
|
.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)
|
// copy release receipt to tunnel for TestFlight eligibility (once is enough, it won't change)
|
||||||
if let appReceiptURL = Bundle.main.appStoreProductionReceiptURL {
|
if let appReceiptURL = Bundle.main.appStoreProductionReceiptURL {
|
||||||
let tunnelReceiptURL = BundleConfiguration.urlForBetaReceipt
|
|
||||||
do {
|
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.removeItem(at: tunnelReceiptURL)
|
||||||
try FileManager.default.copyItem(at: appReceiptURL, to: tunnelReceiptURL)
|
try FileManager.default.copyItem(at: appReceiptURL, to: tunnelReceiptURL)
|
||||||
} catch {
|
} 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 {
|
do {
|
||||||
pp_log(.app, .notice, "Fetch providers index...")
|
pp_log(.app, .info, "\tFetch providers index...")
|
||||||
try await providerManager.fetchIndex(from: API.shared)
|
try await providerManager.fetchIndex(from: API.shared)
|
||||||
} catch {
|
} 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")
|
pp_log(.app, .notice, "Application did enter foreground")
|
||||||
pendingTask = Task {
|
pendingTask = Task {
|
||||||
do {
|
do {
|
||||||
pp_log(.App.profiles, .info, "Refresh local profiles observers...")
|
pp_log(.App.profiles, .info, "\tRefresh local profiles observers...")
|
||||||
try await profileManager.observeLocal()
|
try await profileManager.observeLocal()
|
||||||
} catch {
|
} 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()
|
await iapManager.reloadReceipt()
|
||||||
|
@ -165,10 +171,10 @@ private extension AppContext {
|
||||||
// toggle sync based on .sharing eligibility
|
// toggle sync based on .sharing eligibility
|
||||||
let isEligibleForSharing = features.contains(.sharing)
|
let isEligibleForSharing = features.contains(.sharing)
|
||||||
do {
|
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)
|
try await profileManager.observeRemote(isEligibleForSharing && isCloudKitEnabled)
|
||||||
} catch {
|
} 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
|
await pendingTask?.value
|
||||||
|
@ -180,29 +186,29 @@ private extension AppContext {
|
||||||
|
|
||||||
pp_log(.app, .notice, "Application did save profile (\(profile.id))")
|
pp_log(.app, .notice, "Application did save profile (\(profile.id))")
|
||||||
guard profile.id == tunnel.currentProfile?.id else {
|
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
|
return
|
||||||
}
|
}
|
||||||
guard [.active, .activating].contains(tunnel.status) else {
|
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
|
return
|
||||||
}
|
}
|
||||||
pendingTask = Task {
|
pendingTask = Task {
|
||||||
do {
|
do {
|
||||||
if profile.isInteractive {
|
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()
|
try await tunnel.disconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
pp_log(.app, .info, "Reconnect profile \(profile.id)")
|
pp_log(.app, .info, "\tReconnect profile \(profile.id)")
|
||||||
try await tunnel.connect(with: profile)
|
try await tunnel.connect(with: profile)
|
||||||
} catch {
|
} 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()
|
try await tunnel.disconnect()
|
||||||
}
|
}
|
||||||
} catch {
|
} 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
|
await pendingTask?.value
|
||||||
|
|
|
@ -83,7 +83,8 @@ extension AppContext {
|
||||||
profileManager: profileManager,
|
profileManager: profileManager,
|
||||||
providerManager: providerManager,
|
providerManager: providerManager,
|
||||||
registry: registry,
|
registry: registry,
|
||||||
tunnel: tunnel
|
tunnel: tunnel,
|
||||||
|
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,10 +87,7 @@ extension AppContext {
|
||||||
cloudKitIdentifier: nil,
|
cloudKitIdentifier: nil,
|
||||||
author: nil
|
author: nil
|
||||||
)
|
)
|
||||||
let repository = AppData.cdProviderRepositoryV3(
|
let repository = AppData.cdProviderRepositoryV3(context: store.backgroundContext)
|
||||||
context: store.context,
|
|
||||||
backgroundContext: store.backgroundContext
|
|
||||||
)
|
|
||||||
return ProviderManager(repository: repository)
|
return ProviderManager(repository: repository)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -119,7 +116,8 @@ extension AppContext {
|
||||||
profileManager: profileManager,
|
profileManager: profileManager,
|
||||||
providerManager: providerManager,
|
providerManager: providerManager,
|
||||||
registry: .shared,
|
registry: .shared,
|
||||||
tunnel: tunnel
|
tunnel: tunnel,
|
||||||
|
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
||||||
)
|
)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue