From 614943c2073504d82230548fa4e807b228263c7f Mon Sep 17 00:00:00 2001 From: Juan Ignacio Date: Wed, 21 Mar 2018 16:39:46 -0300 Subject: [PATCH] Adding relationship order in CKFetchRecordZoneChangesOperation --- CloudCore.xcodeproj/project.pbxproj | 4 + .../Classes/Fetch/FetchAndSaveOperation.swift | 81 +++++++++++--- .../FetchRecordZoneChangesOperation.swift | 23 ++-- Source/Model/Graph.swift | 100 ++++++++++++++++++ 4 files changed, 185 insertions(+), 23 deletions(-) create mode 100644 Source/Model/Graph.swift diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index 6a51003b..3e087e13 100755 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 8D8B20EB2062E2B700F27057 /* Graph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8B20EA2062E2B700F27057 /* Graph.swift */; }; D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9089D491FE14E57000FC60C /* SetupOperation.swift */; }; D97465F81FE319930060EA66 /* CloudCoreDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F71FE319930060EA66 /* CloudCoreDelegate.swift */; }; D97465FA1FE31A650060EA66 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F91FE31A650060EA66 /* Module.swift */; }; @@ -89,6 +90,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 8D8B20EA2062E2B700F27057 /* Graph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Graph.swift; sourceTree = ""; }; D5B2E89F1C3A780C00C0327D /* CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D5C6298B1C3A8BBD007F7B7C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D9089D491FE14E57000FC60C /* SetupOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupOperation.swift; sourceTree = ""; }; @@ -483,6 +485,7 @@ E22A53D91E4A8743009286C0 /* CloudKitAttribute.swift */, E2EE20061E4E6DCE0060F769 /* ServiceAttributeName.swift */, E2E296C91E49DA0800E7D6ED /* Tokens.swift */, + 8D8B20EA2062E2B700F27057 /* Graph.swift */, ); path = Model; sourceTree = ""; @@ -715,6 +718,7 @@ E2C02A0E1E4C99AD001B2871 /* ObjectToRecordConverter.swift in Sources */, D985DE9D1FDFF9D400236870 /* PurgeLocalDatabaseOperation.swift in Sources */, E2FA74481E769D9400C3489D /* RecordIDWithDatabase.swift in Sources */, + 8D8B20EB2062E2B700F27057 /* Graph.swift in Sources */, E29BB2211E4344E80020F5B6 /* CKRecord.swift in Sources */, E2D390081E4A49350019BBCD /* NSEntityDescription.swift in Sources */, E29BB2231E4346FF0020F5B6 /* NSManagedObject.swift in Sources */, diff --git a/Source/Classes/Fetch/FetchAndSaveOperation.swift b/Source/Classes/Fetch/FetchAndSaveOperation.swift index 50995fc2..c521316e 100644 --- a/Source/Classes/Fetch/FetchAndSaveOperation.swift +++ b/Source/Classes/Fetch/FetchAndSaveOperation.swift @@ -67,25 +67,80 @@ public class FetchAndSaveOperation: Operation { CloudCore.delegate?.didSyncFromCloud() } + + func dependencyGraph() -> Graph { + + let cloudCoreEnabledEntities = self.persistentContainer.managedObjectModel.cloudCoreEnabledEntities + let entityNames = cloudCoreEnabledEntities.flatMap({$0.name}) + let dependencyGraph: Graph = Graph(vertices: entityNames) + + for entityDescription in cloudCoreEnabledEntities { + guard let name = entityDescription.name else {continue} + + for relationshipDescription in entityDescription.relationshipsByName { + guard let destinationEntityName = relationshipDescription.value.destinationEntity?.name else {continue} + + dependencyGraph.addEdge(from: destinationEntityName, to: name) + } + } + return dependencyGraph + } + + + func createConvertOperationsByRecordType(records:[CKRecord], context:NSManagedObjectContext) -> [String:[Operation]] { + var operationsByRecordType = [String:[Operation]]() + for record in records { + // Convert and write CKRecord To NSManagedObject Operation + let convertOperation = RecordToCoreDataOperation(parentContext: context, record: record) + convertOperation.errorBlock = { self.errorBlock?($0) } + + var auxOperations = operationsByRecordType[record.recordType] ?? [Operation]() + auxOperations.append(convertOperation) + operationsByRecordType[record.recordType] = auxOperations + } + return operationsByRecordType + } + private func addRecordZoneChangesOperation(recordZoneIDs: [CKRecordZoneID], database: CKDatabase, context: NSManagedObjectContext) { if recordZoneIDs.isEmpty { return } let recordZoneChangesOperation = FetchRecordZoneChangesOperation(from: database, recordZoneIDs: recordZoneIDs, tokens: tokens) - recordZoneChangesOperation.recordChangedBlock = { - // Convert and write CKRecord To NSManagedObject Operation - let convertOperation = RecordToCoreDataOperation(parentContext: context, record: $0) - convertOperation.errorBlock = { self.errorBlock?($0) } - self.queue.addOperation(convertOperation) - } - - recordZoneChangesOperation.recordWithIDWasDeletedBlock = { - // Delete NSManagedObject with specified recordID Operation - let deleteOperation = DeleteFromCoreDataOperation(parentContext: context, recordID: $0) - deleteOperation.errorBlock = { self.errorBlock?($0) } - self.queue.addOperation(deleteOperation) - } + recordZoneChangesOperation.recordChangedCompletionBlock = { changedRecords, deletedRecordIDs in + + // Record Changes + let operationsByRecordType = self.createConvertOperationsByRecordType(records: changedRecords, context: context) + let dependencyGraph = self.dependencyGraph() + + if let topologicalSort = dependencyGraph.topologicalSort() { + for recordType in topologicalSort { + guard let operations = operationsByRecordType[recordType] else {continue} + operations.forEach { changeOperation in + self.queue.addOperation(changeOperation) + } + } + } else { + + // Perform all changes many times to fill the all the relationships. + let cycleLength = dependencyGraph.detectCycles().flatMap({$0.count}).sorted().last ?? 2 + for _ in 0...cycleLength { + changedRecords.forEach {record in + let convertOperation = RecordToCoreDataOperation(parentContext: context, record: record) + convertOperation.errorBlock = { self.errorBlock?($0) } + self.queue.addOperation(convertOperation) + } + } + } + + // Record Deletions + deletedRecordIDs.forEach { + // Delete NSManagedObject with specified recordID Operation + let deleteOperation = DeleteFromCoreDataOperation(parentContext: context, recordID: $0) + deleteOperation.errorBlock = { self.errorBlock?($0) } + self.queue.addOperation(deleteOperation) + } + } recordZoneChangesOperation.errorBlock = { zoneID, error in self.handle(recordZoneChangesError: error, in: zoneID, database: database, context: context) diff --git a/Source/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift b/Source/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift index 67f0a407..e6c63ed2 100644 --- a/Source/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift +++ b/Source/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift @@ -14,10 +14,11 @@ class FetchRecordZoneChangesOperation: Operation { let recordZoneIDs: [CKRecordZoneID] let database: CKDatabase // - + + var updatedRecords = [CKRecord]() + var deletedRecordIDs = [CKRecordID]() + var recordChangedCompletionBlock: ((_ changed:[CKRecord],_ deleted:[CKRecordID]) -> Void)? var errorBlock: ((CKRecordZoneID, Error) -> Void)? - var recordChangedBlock: ((CKRecord) -> Void)? - var recordWithIDWasDeletedBlock: ((CKRecordID) -> Void)? private let optionsByRecordZoneID: [CKRecordZoneID: CKFetchRecordZoneChangesOptions] private let fetchQueue = OperationQueue() @@ -53,12 +54,12 @@ class FetchRecordZoneChangesOperation: Operation { // Init Fetch Operation let fetchOperation = CKFetchRecordZoneChangesOperation(recordZoneIDs: recordZoneIDs, optionsByRecordZoneID: optionsByRecordZoneID) - fetchOperation.recordChangedBlock = { - self.recordChangedBlock?($0) - } - fetchOperation.recordWithIDWasDeletedBlock = { recordID, _ in - self.recordWithIDWasDeletedBlock?(recordID) - } + fetchOperation.recordChangedBlock = { + self.updatedRecords.append($0) + } + fetchOperation.recordWithIDWasDeletedBlock = { recordID, _ in + self.deletedRecordIDs.append(recordID) + } fetchOperation.recordZoneFetchCompletionBlock = { zoneId, serverChangeToken, clientChangeTokenData, isMore, error in self.tokens.tokensByRecordZoneID[zoneId] = serverChangeToken @@ -69,7 +70,9 @@ class FetchRecordZoneChangesOperation: Operation { if isMore { let moreOperation = self.makeFetchOperation(optionsByRecordZoneID: optionsByRecordZoneID) self.fetchQueue.addOperation(moreOperation) - } + } else { + self.recordChangedCompletionBlock?(self.updatedRecords, self.deletedRecordIDs) + } } fetchOperation.qualityOfService = self.qualityOfService diff --git a/Source/Model/Graph.swift b/Source/Model/Graph.swift new file mode 100644 index 00000000..217c98d8 --- /dev/null +++ b/Source/Model/Graph.swift @@ -0,0 +1,100 @@ +// +// Graph.swift +// CloudCore +// +// Created by Juan Ignacio on 3/21/18. +// Copyright © 2018 Vasily Ulianov. All rights reserved. +// + +// Part of this code are from the SwiftGraph project: https://github.com/davecom/SwiftGraph +class Graph { + + var vertices : [String] + var edges = [String:[String]]() + + init(vertices:[String]) { + self.vertices = vertices + vertices.forEach { + self.edges[$0] = [String]() + } + } + + func addEdge(from:String, to:String) { + edges[from]!.append(to) + } + + func indexOfVertex(_ v:String) -> Int? { + return vertices.index(of: v) + } + + func neighborsForVertex(_ v:String) -> [String]? { + return edges[v] + } + + func detectCycles() -> [[String]] { + var cycles = [[String]]() // store of all found cycles + var openPaths: [[String]] = vertices.map{ [$0] } // initial open paths are single vertex lists + + while openPaths.count > 0 { + let openPath = openPaths.removeFirst() // queue pop() + //if openPath.count > maxK { return cycles } // do we want to stop at a certain length k + if let tail = openPath.last, let head = openPath.first { + let neighbors = neighborsForVertex(tail)! + for neighbor in neighbors { + + if neighbor == head { + cycles.append(openPath + [neighbor]) // found a cycle + } else if !openPath.contains(neighbor) && indexOfVertex(neighbor)! > indexOfVertex(head)! { + openPaths.append(openPath + [neighbor]) // another open path to explore + } + } + } + } + return cycles + } + + + func topologicalSort() -> [String]? { + var sortedVertices = [String]() + let tsNodes = vertices.map{ TSNode(vertex: $0, color: .white) } + var notDAG = false + + func visit(_ node: TSNode) { + guard node.color != .gray else { + notDAG = true + return + } + if node.color == .white { + node.color = .gray + for inode in tsNodes where (neighborsForVertex(node.vertex)?.contains(inode.vertex))! { + visit(inode) + } + node.color = .black + sortedVertices.insert(node.vertex, at: 0) + } + } + + for node in tsNodes where node.color == .white { + visit(node) + } + + if notDAG { + return nil + } + + return sortedVertices + } + + fileprivate enum TSColor { case black, gray, white } + + fileprivate class TSNode { + fileprivate let vertex: String + fileprivate var color: TSColor + + init(vertex: String, color: TSColor) { + self.vertex = vertex + self.color = color + } + } +} +