Skip to content
This repository was archived by the owner on Dec 17, 2018. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CloudCore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -89,6 +90,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
8D8B20EA2062E2B700F27057 /* Graph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Graph.swift; sourceTree = "<group>"; };
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 = "<group>"; };
D9089D491FE14E57000FC60C /* SetupOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupOperation.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -483,6 +485,7 @@
E22A53D91E4A8743009286C0 /* CloudKitAttribute.swift */,
E2EE20061E4E6DCE0060F769 /* ServiceAttributeName.swift */,
E2E296C91E49DA0800E7D6ED /* Tokens.swift */,
8D8B20EA2062E2B700F27057 /* Graph.swift */,
);
path = Model;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
81 changes: 68 additions & 13 deletions Source/Classes/Fetch/FetchAndSaveOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
100 changes: 100 additions & 0 deletions Source/Model/Graph.swift
Original file line number Diff line number Diff line change
@@ -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<String>(vertex: $0, color: .white) }
var notDAG = false

func visit(_ node: TSNode<String>) {
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<String> {
fileprivate let vertex: String
fileprivate var color: TSColor

init(vertex: String, color: TSColor) {
self.vertex = vertex
self.color = color
}
}
}