/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import Deferred import Foundation import Shared import Storage import XCGLogger private let log = Logger.syncLogger // Because generic protocols in Swift are a pain in the ass. public protocol BookmarkStorer: class { // TODO: this should probably return a timestamp. func applyUpstreamCompletionOp(_ op: UpstreamCompletionOp, itemSources: ItemSources, trackingTimesInto local: LocalOverrideCompletionOp) -> Deferred> } open class UpstreamCompletionOp: PerhapsNoOp { // Upload these records from the buffer, but with these child lists. open var amendChildrenFromBuffer: [GUID: [GUID]] = [:] // Upload these records from the mirror, but with these child lists. open var amendChildrenFromMirror: [GUID: [GUID]] = [:] // Upload these records from local, but with these child lists. open var amendChildrenFromLocal: [GUID: [GUID]] = [:] // Upload these records as-is. open var records: [Record] = [] open let ifUnmodifiedSince: Timestamp? open var isNoOp: Bool { return records.isEmpty } public init(ifUnmodifiedSince: Timestamp?=nil) { self.ifUnmodifiedSince = ifUnmodifiedSince } } open class BookmarksMergeResult: PerhapsNoOp { let uploadCompletion: UpstreamCompletionOp let overrideCompletion: LocalOverrideCompletionOp let bufferCompletion: BufferCompletionOp let itemSources: ItemSources open var isNoOp: Bool { return self.uploadCompletion.isNoOp && self.overrideCompletion.isNoOp && self.bufferCompletion.isNoOp } func applyToClient(_ client: BookmarkStorer, storage: SyncableBookmarks, buffer: BookmarkBufferStorage) -> Success { return client.applyUpstreamCompletionOp(self.uploadCompletion, itemSources: self.itemSources, trackingTimesInto: self.overrideCompletion) >>> { storage.applyLocalOverrideCompletionOp(self.overrideCompletion, itemSources: self.itemSources) } >>> { buffer.applyBufferCompletionOp(self.bufferCompletion, itemSources: self.itemSources) } } init(uploadCompletion: UpstreamCompletionOp, overrideCompletion: LocalOverrideCompletionOp, bufferCompletion: BufferCompletionOp, itemSources: ItemSources) { self.uploadCompletion = uploadCompletion self.overrideCompletion = overrideCompletion self.bufferCompletion = bufferCompletion self.itemSources = itemSources } static func NoOp(_ itemSources: ItemSources) -> BookmarksMergeResult { return BookmarksMergeResult(uploadCompletion: UpstreamCompletionOp(), overrideCompletion: LocalOverrideCompletionOp(), bufferCompletion: BufferCompletionOp(), itemSources: itemSources) } } // MARK: - Errors. open class BookmarksMergeError: MaybeErrorType, SyncPingFailureFormattable { fileprivate let error: Error? init(error: Error?=nil) { self.error = error } open var description: String { return "Merge error: \(self.error ??? "nil")" } open var failureReasonName: SyncPingFailureReasonName { return .otherError } } open class BookmarksMergeConsistencyError: BookmarksMergeError { override open var description: String { return "Merge consistency error" } } open class BookmarksMergeErrorTreeIsUnrooted: BookmarksMergeConsistencyError { open let roots: Set public init(roots: Set) { self.roots = roots } override open var description: String { return "Tree is unrooted: roots are \(self.roots)" } } enum MergeState { case unknown // Default state. case unchanged // Nothing changed: no work needed. case remote // Take the associated remote value. case local // Take the associated local value. case new(value: T) // Take this synthesized value. var isUnchanged: Bool { if case .unchanged = self { return true } return false } var isUnknown: Bool { if case .unknown = self { return true } return false } var label: String { switch self { case .unknown: return "Unknown" case .unchanged: return "Unchanged" case .remote: return "Remote" case .local: return "Local" case .new: return "New" } } } func ==(lhs: MergeState, rhs: MergeState) -> Bool { switch (lhs, rhs) { case (.unknown, .unknown): return true case (.unchanged, .unchanged): return true case (.remote, .remote): return true case (.local, .local): return true case let (.new(lh), .new(rh)): return lh == rh default: return false } } /** * Using this: * * You get one for the root. Then you give it children for the roots * from the mirror. * * Then you walk those, populating the remote and local nodes by looking * at the left/right trees. * * By comparing left and right, and doing value-based comparisons if necessary, * a merge state is decided and assigned for both value and structure. * * One then walks both left and right child structures (both to ensure that * all nodes on both left and right will be visited!) recursively. */ class MergedTreeNode { let guid: GUID let mirror: BookmarkTreeNode? var remote: BookmarkTreeNode? var local: BookmarkTreeNode? var hasLocal: Bool { return self.local != nil } var hasMirror: Bool { return self.mirror != nil } var hasRemote: Bool { return self.remote != nil } var valueState: MergeState = MergeState.unknown var structureState: MergeState = MergeState.unknown var hasDecidedChildren: Bool { return !self.structureState.isUnknown } var mergedChildren: [MergedTreeNode]? // One-sided constructors. static func forRemote(_ remote: BookmarkTreeNode, mirror: BookmarkTreeNode?=nil) -> MergedTreeNode { let n = MergedTreeNode(guid: remote.recordGUID, mirror: mirror, structureState: MergeState.remote) n.remote = remote n.valueState = MergeState.remote return n } static func forLocal(_ local: BookmarkTreeNode, mirror: BookmarkTreeNode?=nil) -> MergedTreeNode { let n = MergedTreeNode(guid: local.recordGUID, mirror: mirror, structureState: MergeState.local) n.local = local n.valueState = MergeState.local return n } static func forUnchanged(_ mirror: BookmarkTreeNode) -> MergedTreeNode { let n = MergedTreeNode(guid: mirror.recordGUID, mirror: mirror, structureState: MergeState.unchanged) n.valueState = MergeState.unchanged return n } init(guid: GUID, mirror: BookmarkTreeNode?, structureState: MergeState) { self.guid = guid self.mirror = mirror self.structureState = structureState } init(guid: GUID, mirror: BookmarkTreeNode?) { self.guid = guid self.mirror = mirror } // N.B., you cannot recurse down `decidedStructure`: you'll depart from the // merged tree. You need to use `mergedChildren` instead. fileprivate var decidedStructure: BookmarkTreeNode? { switch self.structureState { case .unknown: return nil case .unchanged: return self.mirror case .remote: return self.remote case .local: return self.local case let .new(node): return node } } func asUnmergedTreeNode() -> BookmarkTreeNode { return self.decidedStructure ?? BookmarkTreeNode.unknown(guid: self.guid) } // Recursive. Starts returning Unknown when nodes haven't been processed. func asMergedTreeNode() -> BookmarkTreeNode { guard let decided = self.decidedStructure, let merged = self.mergedChildren else { return BookmarkTreeNode.unknown(guid: self.guid) } if case .folder = decided { let children = merged.map { $0.asMergedTreeNode() } return BookmarkTreeNode.folder(guid: self.guid, children: children) } return decided } var isFolder: Bool { return self.mergedChildren != nil } func dump(_ indent: Int) { precondition(indent < 200) let r: Character = "R" let l: Character = "L" let m: Character = "M" let ind = indenting(indent) print(ind, "[V: ", box(self.remote, r), box(self.mirror, m), box(self.local, l), self.guid, self.valueState.label, "]") guard self.isFolder else { return } print(ind, "[S: ", self.structureState.label, "]") if let children = self.mergedChildren { print(ind, " ..") for child in children { child.dump(indent + 2) } } } } private func box(_ x: T?, _ c: Character) -> Character { if x == nil { return "□" } return c } private func indenting(_ by: Int) -> String { return String(repeating: " ", count: by) } class MergedTree { var root: MergedTreeNode var deleteLocally: Set = Set() var deleteRemotely: Set = Set() var deleteFromMirror: Set = Set() var acceptLocalDeletion: Set = Set() var acceptRemoteDeletion: Set = Set() var allGUIDs: Set { var out = Set([self.root.guid]) func acc(_ node: MergedTreeNode) { guard let children = node.mergedChildren else { return } out.formUnion(Set(children.map { $0.guid })) children.forEach(acc) } acc(self.root) return out } init(mirrorRoot: BookmarkTreeNode) { self.root = MergedTreeNode(guid: mirrorRoot.recordGUID, mirror: mirrorRoot, structureState: MergeState.unchanged) self.root.valueState = MergeState.unchanged } func dump() { print("Deleted locally: \(self.deleteLocally.joined(separator: ", "))") print("Deleted remotely: \(self.deleteRemotely.joined(separator: ", "))") print("Deleted from mirror: \(self.deleteFromMirror.joined(separator: ", "))") print("Accepted local deletions: \(self.acceptLocalDeletion.joined(separator: ", "))") print("Accepted remote deletions: \(self.acceptRemoteDeletion.joined(separator: ", "))") print("Root: ") self.root.dump(0) } }