mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2024-12-26 03:12:37 +00:00
d3e344670b
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
174 lines
5.9 KiB
Swift
174 lines
5.9 KiB
Swift
//
|
|
// CDProviderRepositoryV3.swift
|
|
// Passepartout
|
|
//
|
|
// Created by Davide De Rosa on 10/26/24.
|
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
|
//
|
|
// https://github.com/passepartoutvpn
|
|
//
|
|
// This file is part of Passepartout.
|
|
//
|
|
// Passepartout is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Passepartout is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
//
|
|
|
|
import AppData
|
|
import Combine
|
|
import CommonUtils
|
|
import CoreData
|
|
import Foundation
|
|
import PassepartoutKit
|
|
|
|
extension AppData {
|
|
public static func cdProviderRepositoryV3(context: NSManagedObjectContext) -> ProviderRepository {
|
|
CDProviderRepositoryV3(context: context)
|
|
}
|
|
}
|
|
|
|
actor CDProviderRepositoryV3: NSObject, ProviderRepository {
|
|
private nonisolated let context: 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) {
|
|
self.context = context
|
|
providersSubject = CurrentValueSubject([])
|
|
lastUpdateSubject = CurrentValueSubject([:])
|
|
|
|
let request = CDProviderV3.fetchRequest()
|
|
request.sortDescriptors = [
|
|
NSSortDescriptor(key: "providerId", ascending: true)
|
|
]
|
|
providersController = .init(
|
|
fetchRequest: request,
|
|
managedObjectContext: context,
|
|
sectionNameKeyPath: nil,
|
|
cacheName: nil
|
|
)
|
|
|
|
super.init()
|
|
|
|
Task {
|
|
try await context.perform { [weak self] in
|
|
self?.providersController.delegate = self
|
|
try self?.providersController.performFetch()
|
|
}
|
|
}
|
|
}
|
|
|
|
nonisolated var indexPublisher: AnyPublisher<[ProviderMetadata], Never> {
|
|
providersSubject
|
|
.removeDuplicates()
|
|
.eraseToAnyPublisher()
|
|
}
|
|
|
|
nonisolated var lastUpdatePublisher: AnyPublisher<[ProviderID: Date], Never> {
|
|
lastUpdateSubject
|
|
.removeDuplicates()
|
|
.eraseToAnyPublisher()
|
|
}
|
|
|
|
func store(_ index: [ProviderMetadata]) async throws {
|
|
try await context.perform { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
do {
|
|
// fetch existing for last update and deletion
|
|
let request = CDProviderV3.fetchRequest()
|
|
let results = try request.execute()
|
|
let lastUpdatesByProvider = results.reduce(into: [:]) {
|
|
$0[$1.providerId] = $1.lastUpdate
|
|
}
|
|
results.forEach(context.delete)
|
|
|
|
// replace but retain last update
|
|
let mapper = CoreDataMapper(context: context)
|
|
index.forEach {
|
|
let lastUpdate = lastUpdatesByProvider[$0.id.rawValue]
|
|
mapper.cdProvider(from: $0, lastUpdate: lastUpdate)
|
|
}
|
|
|
|
try context.save()
|
|
} catch {
|
|
context.rollback()
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
func store(_ infrastructure: VPNInfrastructure, for providerId: ProviderID) async throws {
|
|
try await context.perform { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
do {
|
|
let predicate = providerId.predicate
|
|
|
|
// signal update of related provider
|
|
let providerRequest = CDProviderV3.fetchRequest()
|
|
providerRequest.predicate = predicate
|
|
let providers = try providerRequest.execute()
|
|
if let provider = providers.first {
|
|
provider.lastUpdate = infrastructure.lastUpdate
|
|
}
|
|
|
|
// delete all provider entities
|
|
let serverRequest = CDVPNServerV3.fetchRequest()
|
|
serverRequest.predicate = predicate
|
|
let servers = try serverRequest.execute()
|
|
servers.forEach(context.delete)
|
|
|
|
let presetRequest = CDVPNPresetV3.fetchRequest()
|
|
presetRequest.predicate = predicate
|
|
let presets = try presetRequest.execute()
|
|
presets.forEach(context.delete)
|
|
|
|
// create new entities
|
|
let mapper = CoreDataMapper(context: context)
|
|
try infrastructure.servers.forEach {
|
|
try mapper.cdServer(from: $0)
|
|
}
|
|
try infrastructure.presets.forEach {
|
|
try mapper.cdPreset(from: $0)
|
|
}
|
|
|
|
try context.save()
|
|
} catch {
|
|
context.rollback()
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
nonisolated func vpnServerRepository(for providerId: ProviderID) -> VPNProviderServerRepository {
|
|
CDVPNProviderServerRepositoryV3(context: context, providerId: providerId)
|
|
}
|
|
}
|
|
|
|
extension CDProviderRepositoryV3: NSFetchedResultsControllerDelegate {
|
|
nonisolated func controllerDidChangeContent(_ controller: NSFetchedResultsController<any NSFetchRequestResult>) {
|
|
guard let entities = controller.fetchedObjects as? [CDProviderV3] else {
|
|
return
|
|
}
|
|
let mapper = DomainMapper()
|
|
providersSubject.send(entities.compactMap(mapper.provider(from:)))
|
|
lastUpdateSubject.send(mapper.lastUpdate(from: entities))
|
|
}
|
|
}
|