From a1d42813f7f7bb1b2468c34c6ae54a895aff2119 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 27 May 2022 10:40:34 -0700 Subject: [PATCH 01/18] progress only increases, qos is configurable --- .../Classes/Caching/CloudCoreCacheManager.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index 61e37cb..cfa2268 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -161,11 +161,11 @@ class CloudCoreCacheManager: NSObject { return foundOperation } - func longLivedConfiguration() -> CKOperation.Configuration { + func longLivedConfiguration(qos: QualityOfService) -> CKOperation.Configuration { let configuration = CKOperation.Configuration() configuration.container = container configuration.isLongLived = true - configuration.qualityOfService = .utility + configuration.qualityOfService = qos return configuration } @@ -190,7 +190,7 @@ class CloudCoreCacheManager: NSObject { record["remoteStatusRaw"] = RemoteStatus.available.rawValue modifyOp = CKModifyRecordsOperation(recordsToSave: [record], recordIDsToDelete: nil) - modifyOp.configuration = self.longLivedConfiguration() + modifyOp.configuration = self.longLivedConfiguration(qos: .utility) modifyOp.savePolicy = .changedKeys cacheable.operationID = modifyOp.operationID @@ -198,7 +198,9 @@ class CloudCoreCacheManager: NSObject { modifyOp.perRecordProgressBlock = { record, progress in self.update([cacheableID]) { cacheable in - cacheable.uploadProgress = progress + if progress > cacheable.uploadProgress { + cacheable.uploadProgress = progress + } } } modifyOp.perRecordCompletionBlock = { record, error in @@ -247,7 +249,7 @@ class CloudCoreCacheManager: NSObject { guard let record = try? cacheable.restoreRecordWithSystemFields(for: .private) else { return } fetchOp = CKFetchRecordsOperation(recordIDs: [record.recordID]) - fetchOp.configuration = self.longLivedConfiguration() + fetchOp.configuration = self.longLivedConfiguration(qos: .userInitiated) fetchOp.desiredKeys = [cacheable.assetFieldName] cacheable.operationID = fetchOp.operationID @@ -255,7 +257,9 @@ class CloudCoreCacheManager: NSObject { fetchOp.perRecordProgressBlock = { record, progress in self.update([cacheableID]) { cacheable in - cacheable.downloadProgress = progress + if progress > cacheable.downloadProgress { + cacheable.downloadProgress = progress + } } } fetchOp.perRecordCompletionBlock = { record, recordID, error in From 5cb19d118a339e095cda5f6d575d65c6994816cb Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 27 May 2022 10:54:54 -0700 Subject: [PATCH 02/18] bump podspec version to 5.1.0 --- CloudCore.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CloudCore.podspec b/CloudCore.podspec index 21c72ca..16910e0 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "CloudCore" s.summary = "Framework that enables synchronization between CloudKit and Core Data." - s.version = "5.0.2" + s.version = "5.1.0" s.homepage = "https://github.com/deeje/CloudCore" s.license = 'MIT' s.author = { "deeje" => "deeje@mac.com", "Vasily Ulianov" => "vasily@me.com" } From a83107a4c99dd0c8ad544d96040584e5a9161911 Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 28 May 2022 17:40:17 -0700 Subject: [PATCH 03/18] always use persistentHistory, reduce churn on rapid saves remove old code paths --- Source/Classes/CloudCore.swift | 2 +- Source/Classes/Push/CoreDataObserver.swift | 295 ++++++++++----------- 2 files changed, 138 insertions(+), 159 deletions(-) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index c62b294..fc7ab90 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -248,7 +248,7 @@ open class CloudCore { // Zone wasn't found, we need to create it self.queue.cancelAllOperations() - let setupOperation = SetupOperation(container: container, uploadAllData: !(coreDataObserver?.usePersistentHistoryForPush)!) + let setupOperation = SetupOperation(container: container, uploadAllData: true) // arg, why is this a question?! // for completeness, pull again let pullOperation = PullChangesOperation(persistentContainer: container) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index ba12ff8..7690c9d 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -24,32 +24,31 @@ class CoreDataObserver { // Used for errors delegation weak var delegate: CloudCoreDelegate? - var usePersistentHistoryForPush = false var isOnline = true { didSet { - if isOnline != oldValue && isOnline == true && usePersistentHistoryForPush == true { + if isOnline != oldValue && isOnline == true { processPersistentHistory() } } } + var processTimer: Timer? + public init(container: NSPersistentContainer) { self.container = container converter.errorBlock = { [weak self] in self?.delegate?.error(error: $0, module: .some(.pushToCloud)) } - if #available(iOS 11.0, watchOS 4.0, tvOS 11.0, OSX 10.13, *) { - let storeDescription = container.persistentStoreDescriptions.first - if let persistentHistoryNumber = storeDescription?.options[NSPersistentHistoryTrackingKey] as? NSNumber - { - usePersistentHistoryForPush = persistentHistoryNumber.boolValue - } - - if usePersistentHistoryForPush { - processPersistentHistory() - } + var usePersistentHistoryForPush = false + if let storeDescription = container.persistentStoreDescriptions.first, + let persistentHistoryNumber = storeDescription.options[NSPersistentHistoryTrackingKey] as? NSNumber + { + usePersistentHistoryForPush = persistentHistoryNumber.boolValue } + assert(usePersistentHistoryForPush) + + processPersistentHistory() } /// Observe Core Data willSave and didSave notifications @@ -122,176 +121,156 @@ class CoreDataObserver { return success } - @objc private func willSave(notification: Notification) { - guard let context = notification.object as? NSManagedObjectContext else { return } - guard shouldProcess(context) else { return } + func process(_ transaction: NSPersistentHistoryTransaction, in moc: NSManagedObjectContext) -> Bool { + var success = true + + if transaction.contextName != CloudCore.config.pushContextName { return success } - if usePersistentHistoryForPush { - context.insertedObjects.forEach { (inserted) in - if let serviceAttributeNames = inserted.entity.serviceAttributeNames { - for scope in serviceAttributeNames.scopes { - let _ = try? inserted.setRecordInformation(for: scope) + if let changes = transaction.changes { + var insertedObjects = Set() + var updatedObject = Set() + var deletedRecordIDs: [RecordIDWithDatabase] = [] + var operationIDs: [String] = [] + + for change in changes { + switch change.changeType { + case .insert: + if let inserted = try? moc.existingObject(with: change.changedObjectID) { + insertedObjects.insert(inserted) + } + + case .update: + if let inserted = try? moc.existingObject(with: change.changedObjectID) { + if let updatedProperties = change.updatedProperties { + let updatedPropertyNames: [String] = updatedProperties.map { (propertyDescription) in + return propertyDescription.name + } + inserted.updatedPropertyNames = updatedPropertyNames + } + updatedObject.insert(inserted) } + + case .delete: + if change.tombstone != nil { + if let privateRecordData = change.tombstone!["privateRecordData"] as? Data { + let ckRecord = CKRecord(archivedData: privateRecordData) + let database = ckRecord?.recordID.zoneID.ownerName == CKCurrentUserDefaultName ? CloudCore.config.container.privateCloudDatabase : CloudCore.config.container.sharedCloudDatabase + let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, database) + deletedRecordIDs.append(recordIDWithDatabase) + } + if let publicRecordData = change.tombstone!["publicRecordData"] as? Data { + let ckRecord = CKRecord(archivedData: publicRecordData) + let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, CloudCore.config.container.publicCloudDatabase) + deletedRecordIDs.append(recordIDWithDatabase) + } + if let operationID = change.tombstone!["operationID"] as? String { + operationIDs.append(operationID) + } + } + + default: + break } } - } else { - converter.prepareOperationsFor(inserted: context.insertedObjects, - updated: context.updatedObjects, - deleted: context.deletedObjects) - } - } - - @objc private func didSave(notification: Notification) { - guard let context = notification.object as? NSManagedObjectContext else { return } - guard shouldProcess(context) else { return } - - if usePersistentHistoryForPush == true { - DispatchQueue.main.async { [weak self] in - guard let observer = self else { return } - observer.processPersistentHistory() + + self.converter.prepareOperationsFor(inserted: insertedObjects, + updated: updatedObject, + deleted: deletedRecordIDs) + + try? moc.save() + + if self.converter.hasPendingOperations { + success = self.processChanges() + } + + // check for cached assets + if success == true { + let insertedIDs = insertedObjects.map { $0.objectID } + + for insertedID in insertedIDs { + guard let cacheable = try? moc.existingObject(with: insertedID) as? CloudCoreCacheable, + cacheable.cacheState == .local + else { continue } + + cacheable.cacheState = .upload + } + + try? moc.save() } - } else { - guard converter.hasPendingOperations else { return } - DispatchQueue.global(qos: .utility).async { [weak self] in - guard let observer = self else { return } - _ = observer.processChanges() + if !operationIDs.isEmpty { + CloudCore.cacheManager?.cancelOperations(with: operationIDs) } } - } - func processPersistentHistory() { + return success + } + + @objc func processPersistentHistory() { #if os(iOS) guard isOnline else { return } #endif - if #available(iOS 11.0, watchOSApplicationExtension 4.0, tvOS 11.0, OSX 10.13, *) { + container.performBackgroundTask { moc in + moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - func process(_ transaction: NSPersistentHistoryTransaction, in moc: NSManagedObjectContext) -> Bool { - var success = true - - if transaction.contextName != CloudCore.config.pushContextName { return success } + let settings = UserDefaults.standard + do { + var token: NSPersistentHistoryToken? = nil + if let data = settings.object(forKey: CloudCore.config.persistentHistoryTokenKey) as? Data { + token = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSPersistentHistoryToken.classForKeyedUnarchiver()], from: data) as? NSPersistentHistoryToken + } + let historyRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: token) + let historyResult = try moc.execute(historyRequest) as! NSPersistentHistoryResult - if let changes = transaction.changes { - var insertedObjects = Set() - var updatedObject = Set() - var deletedRecordIDs: [RecordIDWithDatabase] = [] - var operationIDs: [String] = [] - - for change in changes { - switch change.changeType { - case .insert: - if let inserted = try? moc.existingObject(with: change.changedObjectID) { - insertedObjects.insert(inserted) - } - - case .update: - if let inserted = try? moc.existingObject(with: change.changedObjectID) { - if let updatedProperties = change.updatedProperties { - let updatedPropertyNames: [String] = updatedProperties.map { (propertyDescription) in - return propertyDescription.name - } - inserted.updatedPropertyNames = updatedPropertyNames - } - updatedObject.insert(inserted) - } - - case .delete: - if change.tombstone != nil { - if let privateRecordData = change.tombstone!["privateRecordData"] as? Data { - let ckRecord = CKRecord(archivedData: privateRecordData) - let database = ckRecord?.recordID.zoneID.ownerName == CKCurrentUserDefaultName ? CloudCore.config.container.privateCloudDatabase : CloudCore.config.container.sharedCloudDatabase - let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, database) - deletedRecordIDs.append(recordIDWithDatabase) - } - if let publicRecordData = change.tombstone!["publicRecordData"] as? Data { - let ckRecord = CKRecord(archivedData: publicRecordData) - let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, CloudCore.config.container.publicCloudDatabase) - deletedRecordIDs.append(recordIDWithDatabase) - } - if let operationID = change.tombstone!["operationID"] as? String { - operationIDs.append(operationID) - } - } + if let history = historyResult.result as? [NSPersistentHistoryTransaction] { + for transaction in history { + if self.process(transaction, in: moc) { + let deleteRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) + try moc.execute(deleteRequest) - default: + let data = try NSKeyedArchiver.archivedData(withRootObject: transaction.token, requiringSecureCoding: false) + settings.set(data, forKey: CloudCore.config.persistentHistoryTokenKey) + } else { break } } - - self.converter.prepareOperationsFor(inserted: insertedObjects, - updated: updatedObject, - deleted: deletedRecordIDs) - - try? moc.save() - - if self.converter.hasPendingOperations { - success = self.processChanges() - } - - // check for cached assets - if success == true { - let insertedIDs = insertedObjects.map { $0.objectID } - container.performBackgroundTask { moc in - moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - do { - for insertedID in insertedIDs { - guard let cacheable = try moc.existingObject(with: insertedID) as? CloudCoreCacheable, - cacheable.cacheState == .local - else { continue } - - cacheable.cacheState = .upload - } - - try moc.save() - } catch { - self.delegate?.error(error: error, module: .some(.pushToCloud)) - } - } - } - - if !operationIDs.isEmpty { - CloudCore.cacheManager?.cancelOperations(with: operationIDs) - } } - - return success + } catch { + let nserror = error as NSError + switch nserror.code { + case NSPersistentHistoryTokenExpiredError: + settings.set(nil, forKey: CloudCore.config.persistentHistoryTokenKey) + default: + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } } - - container.performBackgroundTask { (moc) in - let settings = UserDefaults.standard - do { - var token: NSPersistentHistoryToken? = nil - if let data = settings.object(forKey: CloudCore.config.persistentHistoryTokenKey) as? Data { - token = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSPersistentHistoryToken.classForKeyedUnarchiver()], from: data) as? NSPersistentHistoryToken - } - let historyRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: token) - let historyResult = try moc.execute(historyRequest) as! NSPersistentHistoryResult - - if let history = historyResult.result as? [NSPersistentHistoryTransaction] { - for transaction in history { - if process(transaction, in: moc) { - let deleteRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) - try moc.execute(deleteRequest) - - let data = try NSKeyedArchiver.archivedData(withRootObject: transaction.token, requiringSecureCoding: false) - settings.set(data, forKey: CloudCore.config.persistentHistoryTokenKey) - } else { - break - } - } - } - } catch { - let nserror = error as NSError - switch nserror.code { - case NSPersistentHistoryTokenExpiredError: - settings.set(nil, forKey: CloudCore.config.persistentHistoryTokenKey) - default: - fatalError("Unresolved error \(nserror), \(nserror.userInfo)") - } + } + } + + @objc private func willSave(notification: Notification) { + guard let context = notification.object as? NSManagedObjectContext else { return } + guard shouldProcess(context) else { return } + + context.insertedObjects.forEach { (inserted) in + if let serviceAttributeNames = inserted.entity.serviceAttributeNames { + for scope in serviceAttributeNames.scopes { + let _ = try? inserted.setRecordInformation(for: scope) } } } + } + + @objc private func didSave(notification: Notification) { + guard let context = notification.object as? NSManagedObjectContext else { return } + guard shouldProcess(context) else { return } + + DispatchQueue.main.async { + self.processTimer?.invalidate() + self.processTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in + self.processPersistentHistory() + } + } } private func handle(error: Error, parentContext: NSManagedObjectContext) { From 32ed3f96e0506886b175c62f9300af35a4cb3c97 Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 28 May 2022 17:41:08 -0700 Subject: [PATCH 04/18] need to update removeStatus locally to stay in sync --- Source/Classes/Caching/CloudCoreCacheManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index cfa2268..2535020 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -207,6 +207,7 @@ class CloudCoreCacheManager: NSObject { self.update([cacheableID]) { cacheable in cacheable.uploadProgress = 0 cacheable.cacheState = (error == nil) ? .cached : .local + cacheable.remoteStatus = (error == nil) ? .available : .pending cacheable.lastErrorMessage = error?.localizedDescription } From 5ab9a31dac5627105a03a4da2972ae2aeaf36e7b Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 28 May 2022 18:31:50 -0700 Subject: [PATCH 05/18] simplify the .upload triggering --- Source/Classes/Push/CoreDataObserver.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 7690c9d..36e1085 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -185,17 +185,17 @@ class CoreDataObserver { // check for cached assets if success == true { - let insertedIDs = insertedObjects.map { $0.objectID } - - for insertedID in insertedIDs { - guard let cacheable = try? moc.existingObject(with: insertedID) as? CloudCoreCacheable, - cacheable.cacheState == .local - else { continue } + moc.perform { + for insertedObject in insertedObjects { + guard let cacheable = insertedObject as? CloudCoreCacheable, + cacheable.cacheState == .local + else { continue } + + cacheable.cacheState = .upload + } - cacheable.cacheState = .upload + try? moc.save() } - - try? moc.save() } if !operationIDs.isEmpty { From 3e56f14dc2359de6e9de92e588824b557678c352 Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 28 May 2022 18:32:09 -0700 Subject: [PATCH 06/18] increase delay in sync to 5 sec after last save --- Source/Classes/Push/CoreDataObserver.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 36e1085..02ecdc0 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -267,7 +267,7 @@ class CoreDataObserver { DispatchQueue.main.async { self.processTimer?.invalidate() - self.processTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in + self.processTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in self.processPersistentHistory() } } From d1e0855577ac356c2992f902d9b7e5c0dcfd788e Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 29 May 2022 09:54:32 -0700 Subject: [PATCH 07/18] cacheable ops can already be in running --- .../Caching/CloudCoreCacheManager.swift | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index 2535020..2e27817 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -215,7 +215,7 @@ class CloudCoreCacheManager: NSObject { CloudCore.delegate?.error(error: error, module: .cacheToCloud) if let cloudError = error as? CKError, - cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, +// cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { @@ -226,10 +226,16 @@ class CloudCoreCacheManager: NSObject { } modifyOp.modifyRecordsCompletionBlock = { records, recordIDs, error in } modifyOp.longLivedOperationWasPersistedBlock = { } - container.privateCloudDatabase.add(modifyOp) + if !modifyOp.isExecuting { + container.privateCloudDatabase.add(modifyOp) + } - cacheable.cacheState = .uploading - try? context.save() + if cacheable.cacheState != .uploading { + cacheable.cacheState = .uploading + } + if context.hasChanges { + try? context.save() + } } } @@ -282,7 +288,7 @@ class CloudCoreCacheManager: NSObject { CloudCore.delegate?.error(error: error, module: .cacheFromCloud) if let cloudError = error as? CKError, - cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, +// cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { @@ -292,10 +298,16 @@ class CloudCoreCacheManager: NSObject { } } fetchOp.longLivedOperationWasPersistedBlock = { } - container.privateCloudDatabase.add(fetchOp) + if !fetchOp.isExecuting { + container.privateCloudDatabase.add(fetchOp) + } - cacheable.cacheState = .downloading - try? context.save() + if cacheable.cacheState != .downloading { + cacheable.cacheState = .downloading + } + if context.hasChanges { + try? context.save() + } } } From 15c51735fd5a770fc2ca590431d0508604062d70 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 30 May 2022 15:09:53 -0700 Subject: [PATCH 08/18] restart all long-lived ops if one errors out --- Source/Classes/Caching/CloudCoreCacheManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index 2e27817..cd56858 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -219,7 +219,7 @@ class CloudCoreCacheManager: NSObject { let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { - self.upload(cacheableID: cacheableID) + self.restoreDanglingOperations() } } } From cacc1221d8ed0d9d28677ab561fd2b1a93c1642b Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 30 May 2022 16:01:40 -0700 Subject: [PATCH 09/18] if CKErrorRetryAfterKey, wait to push more changes --- Source/Classes/Push/CoreDataObserver.swift | 63 ++++++++++++++++------ 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 02ecdc0..e820ea2 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -19,7 +19,13 @@ class CoreDataObserver { static let syncContextName = "CloudCoreSync" - let processSemaphore = DispatchSemaphore(value: 1) + var processContext: NSManagedObjectContext! + static let processContextName = "CloudCoreHistory" + var processTimer: Timer? + var pauseUntil: Date? + + var isProcessing = false + var processAgain = true // Used for errors delegation weak var delegate: CloudCoreDelegate? @@ -32,8 +38,6 @@ class CoreDataObserver { } } - var processTimer: Timer? - public init(container: NSPersistentContainer) { self.container = container converter.errorBlock = { [weak self] in @@ -48,6 +52,11 @@ class CoreDataObserver { } assert(usePersistentHistoryForPush) + processContext = container.newBackgroundContext() + processContext.name = CoreDataObserver.processContextName + processContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + processContext.automaticallyMergesChangesFromParent = true + processPersistentHistory() } @@ -83,11 +92,6 @@ class CoreDataObserver { } func processChanges() -> Bool { - processSemaphore.wait() - defer { - processSemaphore.signal() - } - var success = true CloudCore.delegate?.willSyncToCloud() @@ -211,9 +215,17 @@ class CoreDataObserver { guard isOnline else { return } #endif - container.performBackgroundTask { moc in - moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + if isProcessing { + processAgain = true + return + } + + let backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "CloudCore.processPersistentHistory") + + isProcessing = true + + processContext.perform { let settings = UserDefaults.standard do { var token: NSPersistentHistoryToken? = nil @@ -221,13 +233,13 @@ class CoreDataObserver { token = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSPersistentHistoryToken.classForKeyedUnarchiver()], from: data) as? NSPersistentHistoryToken } let historyRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: token) - let historyResult = try moc.execute(historyRequest) as! NSPersistentHistoryResult + let historyResult = try self.processContext.execute(historyRequest) as! NSPersistentHistoryResult if let history = historyResult.result as? [NSPersistentHistoryTransaction] { for transaction in history { - if self.process(transaction, in: moc) { + if self.process(transaction, in: self.processContext) { let deleteRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) - try moc.execute(deleteRequest) + try self.processContext.execute(deleteRequest) let data = try NSKeyedArchiver.archivedData(withRootObject: transaction.token, requiringSecureCoding: false) settings.set(data, forKey: CloudCore.config.persistentHistoryTokenKey) @@ -245,6 +257,18 @@ class CoreDataObserver { fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } + + UIApplication.shared.endBackgroundTask(backgroundTask) + + DispatchQueue.main.async { + self.isProcessing = false + + if self.processAgain { + self.processAgain = false + + self.processPersistentHistory() + } + } } } @@ -265,9 +289,12 @@ class CoreDataObserver { guard let context = notification.object as? NSManagedObjectContext else { return } guard shouldProcess(context) else { return } + // we've been asked to retry later + if let date = pauseUntil, date.timeIntervalSinceNow > 0 { return } + DispatchQueue.main.async { self.processTimer?.invalidate() - self.processTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in + self.processTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in self.processPersistentHistory() } } @@ -280,13 +307,15 @@ class CoreDataObserver { } switch cloudError.code { - case .requestRateLimited, .zoneBusy: + case .requestRateLimited, .zoneBusy, .serviceUnavailable: pushOperationQueue.cancelAllOperations() if let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { + pauseUntil = Date(timeIntervalSinceNow: number.doubleValue) DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { [weak self] in - guard let observer = self else { return } - observer.processPersistentHistory() + guard let self = self else { return } + + self.processPersistentHistory() } } From 2ab656ec5d1b90fc03dd4c733bdd290320837d80 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 30 May 2022 18:06:27 -0700 Subject: [PATCH 10/18] centralize the pause timer --- .../Caching/CloudCoreCacheManager.swift | 56 ++++++++++++------- Source/Classes/CloudCore.swift | 20 +++++++ Source/Classes/Push/CoreDataObserver.swift | 14 ++--- 3 files changed, 60 insertions(+), 30 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index cd56858..f6787bf 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -41,7 +41,7 @@ class CloudCoreCacheManager: NSObject { super.init() - restoreDanglingOperations() + restartOperations() configureObservers() } @@ -109,18 +109,20 @@ class CloudCoreCacheManager: NSObject { } } - func restoreDanglingOperations() { + func restartOperations() { let context = backgroundContext context.perform { for name in self.cacheableClassNames { - // restore existing ops + // retart new & existing ops + let upload = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.upload.rawValue) let uploading = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.uploading.rawValue) + let download = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.download.rawValue) let downloading = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.downloading.rawValue) - let existing = NSCompoundPredicate(orPredicateWithSubpredicates: [uploading, downloading]) + let newOrExisting = NSCompoundPredicate(orPredicateWithSubpredicates: [upload, uploading, download, downloading]) let restoreRequest = NSFetchRequest(entityName: name) - restoreRequest.predicate = existing - if let cacheables = try? context.fetch(restoreRequest) as? [CloudCoreCacheable] { + restoreRequest.predicate = newOrExisting + if let cacheables = try? context.fetch(restoreRequest) as? [CloudCoreCacheable], !cacheables.isEmpty { self.process(cacheables: cacheables) } @@ -130,7 +132,7 @@ class CloudCoreCacheManager: NSObject { let failedToUpload = NSCompoundPredicate(orPredicateWithSubpredicates: [hasError, isLocal]) let restartRequest = NSFetchRequest(entityName: name) restartRequest.predicate = failedToUpload - if let cacheables = try? context.fetch(restartRequest) as? [CloudCoreCacheable] { + if let cacheables = try? context.fetch(restartRequest) as? [CloudCoreCacheable], !cacheables.isEmpty { let cacheableIDs = cacheables.map { $0.objectID } self.update(cacheableIDs) { cacheable in cacheable.lastErrorMessage = nil @@ -171,6 +173,11 @@ class CloudCoreCacheManager: NSObject { } func upload(cacheableID: NSManagedObjectID) { + // we've been asked to retry later + if let date = CloudCore.pauseUntil, + date.timeIntervalSinceNow > 0 + { return } + let container = container let context = backgroundContext @@ -186,6 +193,19 @@ class CloudCoreCacheManager: NSObject { { guard let record = try? cacheable.restoreRecordWithSystemFields(for: .private) else { return } + /* + var newRecord: CKRecord? + let semaphore = DispatchSemaphore(value: 0) + container.privateCloudDatabase.fetch(withRecordID: record.recordID) { record, error in + newRecord = record + + semaphore.signal() + } + semaphore.wait() + + guard let record = newRecord else { return } + */ + record[cacheable.assetFieldName] = CKAsset(fileURL: cacheable.url) record["remoteStatusRaw"] = RemoteStatus.available.rawValue @@ -215,12 +235,9 @@ class CloudCoreCacheManager: NSObject { CloudCore.delegate?.error(error: error, module: .cacheToCloud) if let cloudError = error as? CKError, -// cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { - self.restoreDanglingOperations() - } + CloudCore.pauseUntil = Date(timeIntervalSinceNow: number.doubleValue) } } } @@ -240,6 +257,11 @@ class CloudCoreCacheManager: NSObject { } func download(cacheableID: NSManagedObjectID) { + // we've been asked to retry later + if let date = CloudCore.pauseUntil, + date.timeIntervalSinceNow > 0 + { return } + let container = container let context = backgroundContext @@ -288,12 +310,9 @@ class CloudCoreCacheManager: NSObject { CloudCore.delegate?.error(error: error, module: .cacheFromCloud) if let cloudError = error as? CKError, -// cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { - self.download(cacheableID: cacheableID) - } + CloudCore.pauseUntil = Date(timeIntervalSinceNow: number.doubleValue) } } } @@ -312,14 +331,9 @@ class CloudCoreCacheManager: NSObject { } func unload(cacheableID: NSManagedObjectID) { - let context = backgroundContext - - context.perform { - guard let cacheable = try? context.existingObject(with: cacheableID) as? CloudCoreCacheable else { return } - + update([cacheableID]) { cacheable in cacheable.removeLocal() cacheable.cacheState = .remote - try? context.save() } } diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index fc7ab90..3e5eef1 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -85,6 +85,26 @@ open class CloudCore { return q }() + // if CloudKit says to retry later… + private static var pauseTimer: Timer? + static var pauseUntil: Date? { + didSet { + DispatchQueue.main.async { + CloudCore.pauseTimer?.invalidate() + if let fireDate = CloudCore.pauseUntil { + let interval = fireDate.timeIntervalSinceNow + print("pausing for \(interval) seconds") + CloudCore.pauseTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { timer in + CloudCore.pauseUntil = nil + + CloudCore.coreDataObserver?.processPersistentHistory() + CloudCore.cacheManager?.restartOperations() + } + } + } + } + } + // MARK: - Methods /// Enable CloudKit and Core Data synchronization diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index e820ea2..2d95285 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -22,7 +22,6 @@ class CoreDataObserver { var processContext: NSManagedObjectContext! static let processContextName = "CloudCoreHistory" var processTimer: Timer? - var pauseUntil: Date? var isProcessing = false var processAgain = true @@ -290,11 +289,13 @@ class CoreDataObserver { guard shouldProcess(context) else { return } // we've been asked to retry later - if let date = pauseUntil, date.timeIntervalSinceNow > 0 { return } + if let date = CloudCore.pauseUntil, + date.timeIntervalSinceNow > 0 + { return } DispatchQueue.main.async { self.processTimer?.invalidate() - self.processTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in + self.processTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in self.processPersistentHistory() } } @@ -311,12 +312,7 @@ class CoreDataObserver { pushOperationQueue.cancelAllOperations() if let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { - pauseUntil = Date(timeIntervalSinceNow: number.doubleValue) - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { [weak self] in - guard let self = self else { return } - - self.processPersistentHistory() - } + CloudCore.pauseUntil = Date(timeIntervalSinceNow: number.doubleValue) } // Zone was accidentally deleted (NOT PURGED), we need to reupload all data accroding Apple Guidelines From 950234bceedd78f91def1d70fb6ca78df1dc7e54 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 30 May 2022 18:47:56 -0700 Subject: [PATCH 11/18] share MOC across CoreDataObserver & CacheManager for processing the push of history and upload of cacheables --- .../Caching/CloudCoreCacheManager.swift | 18 +++++------- Source/Classes/CloudCore.swift | 19 +++++++++---- Source/Classes/Push/CoreDataObserver.swift | 28 ++++++++----------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index f6787bf..3dd37ae 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -14,19 +14,15 @@ import Network class CloudCoreCacheManager: NSObject { private let persistentContainer: NSPersistentContainer - private let backgroundContext: NSManagedObjectContext + private let processContext: NSManagedObjectContext private let container: CKContainer private let cacheableClassNames: [String] private var frcs: [NSFetchedResultsController] = [] - public init(persistentContainer: NSPersistentContainer) { + public init(persistentContainer: NSPersistentContainer, processContext: NSManagedObjectContext) { self.persistentContainer = persistentContainer - - let backgroundContext = persistentContainer.newBackgroundContext() - backgroundContext.automaticallyMergesChangesFromParent = true - backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - self.backgroundContext = backgroundContext + self.processContext = processContext self.container = CloudCore.config.container @@ -80,7 +76,7 @@ class CloudCoreCacheManager: NSObject { } private func configureObservers() { - let context = backgroundContext + let context = processContext context.perform { for name in self.cacheableClassNames { @@ -110,7 +106,7 @@ class CloudCoreCacheManager: NSObject { } func restartOperations() { - let context = backgroundContext + let context = processContext context.perform { for name in self.cacheableClassNames { @@ -179,7 +175,7 @@ class CloudCoreCacheManager: NSObject { { return } let container = container - let context = backgroundContext + let context = processContext context.perform { guard let cacheable = try? context.existingObject(with: cacheableID) as? CloudCoreCacheable else { return } @@ -263,7 +259,7 @@ class CloudCoreCacheManager: NSObject { { return } let container = container - let context = backgroundContext + let context = processContext context.perform { guard let cacheable = try? context.existingObject(with: cacheableID) as? CloudCoreCacheable else { return } diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 3e5eef1..03fa829 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -51,6 +51,8 @@ open class CloudCore { // MARK: - Properties + private(set) static var processContext: NSManagedObjectContext! + private(set) static var coreDataObserver: CoreDataObserver? private(set) static var cacheManager: CloudCoreCacheManager? public static var isOnline: Bool { @@ -111,23 +113,30 @@ open class CloudCore { /// /// - Parameters: /// - container: `NSPersistentContainer` that will be used to save data - public static func enable(persistentContainer container: NSPersistentContainer) { + public static func enable(persistentContainer: NSPersistentContainer) { + // share a MOC between CoreDataObserver and CacheManager + let processContext = persistentContainer.newBackgroundContext() + processContext.name = "CloudCoreProcess" + processContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + processContext.automaticallyMergesChangesFromParent = true + self.processContext = processContext + // Listen for local changes - let observer = CoreDataObserver(container: container) + let observer = CoreDataObserver(persistentContainer: persistentContainer, processContext: processContext) observer.delegate = self.delegate observer.start() self.coreDataObserver = observer - self.cacheManager = CloudCoreCacheManager(persistentContainer: container) + self.cacheManager = CloudCoreCacheManager(persistentContainer: persistentContainer, processContext: processContext) // Subscribe (subscription may be outdated/removed) let subscribeOperation = SubscribeOperation() subscribeOperation.errorBlock = { - handle(subscriptionError: $0, container: container) + handle(subscriptionError: $0, container: persistentContainer) } // Fetch updated data (e.g. push notifications weren't received) - let pullOperation = PullChangesOperation(persistentContainer: container) + let pullOperation = PullChangesOperation(persistentContainer: persistentContainer) pullOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pullFromCloud)) } diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 2d95285..e226048 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -12,15 +12,14 @@ import CloudKit /// Class responsible for taking action on Core Data changes class CoreDataObserver { - var container: NSPersistentContainer - + var persistentContainer: NSPersistentContainer + var processContext: NSManagedObjectContext + let converter = ObjectToRecordConverter() let pushOperationQueue = PushOperationQueue() - static let syncContextName = "CloudCoreSync" + static let pushContextName = "CloudCorePush" - var processContext: NSManagedObjectContext! - static let processContextName = "CloudCoreHistory" var processTimer: Timer? var isProcessing = false @@ -37,25 +36,22 @@ class CoreDataObserver { } } - public init(container: NSPersistentContainer) { - self.container = container + public init(persistentContainer: NSPersistentContainer, processContext: NSManagedObjectContext) { + self.persistentContainer = persistentContainer + self.processContext = processContext + converter.errorBlock = { [weak self] in self?.delegate?.error(error: $0, module: .some(.pushToCloud)) } var usePersistentHistoryForPush = false - if let storeDescription = container.persistentStoreDescriptions.first, + if let storeDescription = persistentContainer.persistentStoreDescriptions.first, let persistentHistoryNumber = storeDescription.options[NSPersistentHistoryTrackingKey] as? NSNumber { usePersistentHistoryForPush = persistentHistoryNumber.boolValue } assert(usePersistentHistoryForPush) - processContext = container.newBackgroundContext() - processContext.name = CoreDataObserver.processContextName - processContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - processContext.automaticallyMergesChangesFromParent = true - processPersistentHistory() } @@ -95,8 +91,8 @@ class CoreDataObserver { CloudCore.delegate?.willSyncToCloud() - let backgroundContext = container.newBackgroundContext() - backgroundContext.name = CoreDataObserver.syncContextName + let backgroundContext = persistentContainer.newBackgroundContext() + backgroundContext.name = CoreDataObserver.pushContextName let records = converter.processPendingOperations(in: backgroundContext) pushOperationQueue.errorBlock = { @@ -350,7 +346,7 @@ class CoreDataObserver { resetZoneOperations.append(subscribeOperation) // Upload all local data - let uploadOperation = PushAllLocalDataOperation(parentContext: parentContext, managedObjectModel: container.managedObjectModel) + let uploadOperation = PushAllLocalDataOperation(parentContext: parentContext, managedObjectModel: persistentContainer.managedObjectModel) uploadOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pushToCloud)) } uploadOperation.addDependency(createZoneOperation) resetZoneOperations.append(uploadOperation) From 4b93c612822766a7187112bba9ffce0faf2f7776 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 31 May 2022 14:38:09 -0700 Subject: [PATCH 12/18] push operations are atomic --- Source/Classes/Push/PushOperationQueue.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Classes/Push/PushOperationQueue.swift b/Source/Classes/Push/PushOperationQueue.swift index b8a3083..8e83ac9 100644 --- a/Source/Classes/Push/PushOperationQueue.swift +++ b/Source/Classes/Push/PushOperationQueue.swift @@ -50,6 +50,7 @@ class PushOperationQueue: OperationQueue { modifyRecords.database = database modifyRecords.savePolicy = .changedKeys modifyRecords.qualityOfService = .userInitiated + modifyRecords.isAtomic = true modifyRecords.perRecordCompletionBlock = { record, error in if let error = error { From 997898de411783cce97db3dbf7808dcbeae619a2 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 31 May 2022 15:01:04 -0700 Subject: [PATCH 13/18] rapid inserts could cause converter to fail why use yet another background context?! --- Source/Classes/Push/CoreDataObserver.swift | 1 + .../ObjectToRecord/ObjectToRecordConverter.swift | 2 +- .../ObjectToRecord/ObjectToRecordOperation.swift | 14 +++++--------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index e226048..e9e46a3 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -93,6 +93,7 @@ class CoreDataObserver { let backgroundContext = persistentContainer.newBackgroundContext() backgroundContext.name = CoreDataObserver.pushContextName + backgroundContext.automaticallyMergesChangesFromParent = true let records = converter.processPendingOperations(in: backgroundContext) pushOperationQueue.errorBlock = { diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index 06d5c6d..bbfee95 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -123,7 +123,7 @@ class ObjectToRecordConverter { /// - attention: Don't call this method from same context's `perfom`, that will cause deadlock func processPendingOperations(in context: NSManagedObjectContext) -> (recordsToSave: [RecordWithDatabase], recordIDsToDelete: [RecordIDWithDatabase]) { for operation in pendingConvertOperations { - operation.parentContext = context + operation.managedObjectContext = context operationQueue.addOperation(operation) } diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index 20556a6..c51f2e3 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -10,8 +10,7 @@ import CloudKit import CoreData class ObjectToRecordOperation: Operation { - /// Need to set before starting operation, child context from it will be created - var parentContext: NSManagedObjectContext? + var managedObjectContext: NSManagedObjectContext? // Set on init let scope: CKDatabase.Scope @@ -37,7 +36,7 @@ class ObjectToRecordOperation: Operation { override func main() { if self.isCancelled { return } - guard let parentContext = parentContext else { + guard let context = managedObjectContext else { let error = CloudCoreError.coreData("CloudCore framework error") errorCompletionBlock?(error) return @@ -53,13 +52,10 @@ class ObjectToRecordOperation: Operation { } #endif - let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - childContext.performAndWait { - childContext.parent = parentContext - + context.performAndWait { do { - try self.fillRecordWithData(using: childContext) - try childContext.save() + try self.fillRecordWithData(using: context) + try context.save() self.conversionCompletionBlock?(self.record) } catch { self.errorCompletionBlock?(error) From d0b3cecb405ed48cf15881b122435761dec1feb6 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 31 May 2022 15:01:41 -0700 Subject: [PATCH 14/18] (remove testing code) --- Source/Classes/Caching/CloudCoreCacheManager.swift | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index 3dd37ae..29f6894 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -189,19 +189,6 @@ class CloudCoreCacheManager: NSObject { { guard let record = try? cacheable.restoreRecordWithSystemFields(for: .private) else { return } - /* - var newRecord: CKRecord? - let semaphore = DispatchSemaphore(value: 0) - container.privateCloudDatabase.fetch(withRecordID: record.recordID) { record, error in - newRecord = record - - semaphore.signal() - } - semaphore.wait() - - guard let record = newRecord else { return } - */ - record[cacheable.assetFieldName] = CKAsset(fileURL: cacheable.url) record["remoteStatusRaw"] = RemoteStatus.available.rawValue From 899df48e8e481b8882c40a492de9b37f7e78c648 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 31 May 2022 15:16:51 -0700 Subject: [PATCH 15/18] more cleanup of ObjectToRecordOperation --- .../Push/ObjectToRecord/ObjectToRecordOperation.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index c51f2e3..0c95ed2 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -54,7 +54,7 @@ class ObjectToRecordOperation: Operation { context.performAndWait { do { - try self.fillRecordWithData(using: context) + try self.fillRecordWithData() try context.save() self.conversionCompletionBlock?(self.record) } catch { @@ -63,8 +63,8 @@ class ObjectToRecordOperation: Operation { } } - private func fillRecordWithData(using context: NSManagedObjectContext) throws { - guard let managedObject = try fetchObject(for: record, using: context) else { + private func fillRecordWithData() throws { + guard let managedObject = try fetchObject(for: record) else { throw CloudCoreError.coreData("Unable to find managed object for record: \(record)") } @@ -99,12 +99,12 @@ class ObjectToRecordOperation: Operation { } } - private func fetchObject(for record: CKRecord, using context: NSManagedObjectContext) throws -> NSManagedObject? { + private func fetchObject(for record: CKRecord) throws -> NSManagedObject? { let entityName = record.recordType let fetchRequest = NSFetchRequest(entityName: entityName) fetchRequest.predicate = NSPredicate(format: serviceAttributeNames.recordName + " == %@", record.recordID.recordName) - return try context.fetch(fetchRequest).first as? NSManagedObject + return try managedObjectContext?.fetch(fetchRequest).first as? NSManagedObject } } From 85098a4a2fe58c2b8eace08284fcae88754f2990 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 31 May 2022 16:47:11 -0700 Subject: [PATCH 16/18] background tasks are only on iOS --- Source/Classes/Push/CoreDataObserver.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index e9e46a3..9c2ef7e 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -217,7 +217,9 @@ class CoreDataObserver { return } + #if TARGET_OS_IOS let backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "CloudCore.processPersistentHistory") + #endif isProcessing = true @@ -254,7 +256,9 @@ class CoreDataObserver { } } + #if TARGET_OS_IOS UIApplication.shared.endBackgroundTask(backgroundTask) + #endif DispatchQueue.main.async { self.isProcessing = false From 258b7b0511ee030d086348029cd88c2a0805bbd8 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 1 Jun 2022 12:49:05 -0700 Subject: [PATCH 17/18] update ReadMe to include links to MediaBook app --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b628480..5702d7b 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # CloudCore ![Platform](https://img.shields.io/cocoapods/p/CloudCore.svg?style=flat) -![Status](https://img.shields.io/badge/status-beta-orange.svg) +![Status](https://img.shields.io/badge/status-production-green.svg) ![Swift](https://img.shields.io/badge/swift-5.0-orange.svg) **CloudCore** is an advanced sync engine for CloudKit and Core Data. @@ -303,6 +303,9 @@ You can find example application at [Example](/Example/) directory, which has be * **refresh** button calls `pull` to fetch data from Cloud. That is only useful for simulators because Simulator unable to receive push notifications * Use [CloudKit dashboard](https://icloud.developer.apple.com/dashboard/) to make changes and see it at application, and make change in application and see ones in dashboard. Don't forget to refresh dashboard's page because it doesn't update data on-the-fly. +## Example app using Cacheable Assets +[MediaBook](https://github.com/deeje/MediaBook) is a production-level iOS app being developed, which demonstrates how to handle cacheable assets in collection views. + ## Tests CloudKit objects can't be mocked up, that's why there are 2 different types of tests: From fb9b5e4983f81ad5d86b64e863f0827350553713 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 1 Jun 2022 12:53:33 -0700 Subject: [PATCH 18/18] NSPersistentHistoryTracking is now required --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5702d7b..12e5f62 100755 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ func application(_ application: UIApplication, didReceiveRemoteNotification user } ``` -6. If you want to enable offline support, **enable NSPersistentHistoryTracking** when you initialize your Core Data stack +6. **Enable NSPersistentHistoryTracking** when you initialize your Core Data stack ```swift lazy var persistentContainer: NSPersistentContainer = { @@ -136,7 +136,7 @@ lazy var persistentContainer: NSPersistentContainer = { }() ``` -7. To identify changes from your app that should be pushed, **save** from a background ManagedObjectContext named `CloudCorePushContext`, or use the convenience function performBackgroundPushTask +7. To identify changes from your app that should be pushed, **save** from the convenience function performBackgroundPushTask ```swift persistentContainer.performBackgroundPushTask { moc in