259 lines
7.7 KiB
Swift
259 lines
7.7 KiB
Swift
/*
|
|
* Copyright 2022 The Yorkie Authors. All rights reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License")
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
typealias CRDTElementPair = (element: CRDTElement, parent: CRDTContainer?)
|
|
|
|
/**
|
|
* `CRDTRoot` is a structure represents the root. It has a hash table of
|
|
* all elements to find a specific element when applying remote changes
|
|
* received from server.
|
|
*
|
|
* Every element has a unique time ticket at creation, which allows us to find
|
|
* a particular element.
|
|
*/
|
|
class CRDTRoot {
|
|
private var rootObject: CRDTObject
|
|
private var elementPairMapByCreatedAt: [TimeTicket: CRDTElementPair] = [:]
|
|
private var removedElementSetByCreatedAt: Set<TimeTicket> = Set()
|
|
private var textWithGarbageSetByCreatedAt: Set<TimeTicket> = Set()
|
|
|
|
init(rootObject: CRDTObject = CRDTObject(createdAt: TimeTicket.initial)) {
|
|
self.rootObject = rootObject
|
|
self.elementPairMapByCreatedAt[self.rootObject.createdAt] = (element: self.rootObject, parent: nil)
|
|
|
|
self.rootObject.getDescendants(callback: { element, parent in
|
|
self.registerElement(element, parent: parent)
|
|
return false
|
|
})
|
|
}
|
|
|
|
/**
|
|
* `find` returns the element of given creation time.
|
|
*/
|
|
func find(createdAt: TimeTicket) -> CRDTElement? {
|
|
return self.elementPairMapByCreatedAt[createdAt]?.element
|
|
}
|
|
|
|
private let subPathPrefix = "$"
|
|
private let subPathSeparator = "."
|
|
|
|
/**
|
|
* `createSubPaths` creates an array of the sub paths for the given element.
|
|
*/
|
|
func createSubPaths(createdAt: TimeTicket) throws -> [String] {
|
|
guard let pair = self.elementPairMapByCreatedAt[createdAt] else {
|
|
return []
|
|
}
|
|
|
|
var result: [String] = []
|
|
var pairForLoop: CRDTElementPair = pair
|
|
while let parent = pairForLoop.parent {
|
|
let createdAt = pairForLoop.element.createdAt
|
|
var subPath = try parent.subPath(createdAt: createdAt)
|
|
subPath = self.escapeSubpath(subPath)
|
|
result.append(subPath)
|
|
guard let parentPair = self.elementPairMapByCreatedAt[parent.createdAt] else {
|
|
break
|
|
}
|
|
|
|
pairForLoop = parentPair
|
|
}
|
|
|
|
result.append(self.subPathPrefix)
|
|
return result.reversed()
|
|
}
|
|
|
|
private func escapeSubpath(_ target: String) -> String {
|
|
return [self.subPathPrefix, self.subPathSeparator].reduce(target) { partialResult, seq in
|
|
partialResult.replacingOccurrences(of: seq, with: "\\\(seq)")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `createPath` creates path of the given element.
|
|
*/
|
|
func createPath(createdAt: TimeTicket) throws -> String {
|
|
return try self.createSubPaths(createdAt: createdAt).joined(separator: self.subPathSeparator)
|
|
}
|
|
|
|
/**
|
|
* `registerElement` registers the given element to hash table.
|
|
*/
|
|
func registerElement(_ element: CRDTElement, parent: CRDTContainer?) {
|
|
self.elementPairMapByCreatedAt[element.createdAt] = (element, parent)
|
|
}
|
|
|
|
/**
|
|
* `deregisterElement` deregister the given element from hash table.
|
|
*/
|
|
func deregisterElement(_ element: CRDTElement) {
|
|
self.elementPairMapByCreatedAt[element.createdAt] = nil
|
|
self.removedElementSetByCreatedAt.remove(element.createdAt)
|
|
}
|
|
|
|
/**
|
|
* `registerRemovedElement` registers the given element to the hash set.
|
|
*/
|
|
func registerRemovedElement(_ element: CRDTElement) {
|
|
self.removedElementSetByCreatedAt.insert(element.createdAt)
|
|
}
|
|
|
|
/**
|
|
* `registerTextWithGarbage` registers the given text to hash set.
|
|
*/
|
|
func registerTextWithGarbage(text: CRDTTextElement) {
|
|
self.textWithGarbageSetByCreatedAt.insert(text.createdAt)
|
|
}
|
|
|
|
/**
|
|
* `elementMapSize` returns the size of element map.
|
|
*/
|
|
var elementMapSize: Int {
|
|
return self.elementPairMapByCreatedAt.count
|
|
}
|
|
|
|
/**
|
|
* `removedElementSetSize` returns the size of removed element set.
|
|
*/
|
|
var removedElementSetSize: Int {
|
|
return self.removedElementSetByCreatedAt.count
|
|
}
|
|
|
|
/**
|
|
* `getObject` returns root object.
|
|
*/
|
|
var object: CRDTObject {
|
|
return self.rootObject
|
|
}
|
|
|
|
/**
|
|
* `garbageLength` returns length of nodes which can be garbage collected.
|
|
*/
|
|
var garbageLength: Int {
|
|
var count = 0
|
|
|
|
self.removedElementSetByCreatedAt.forEach {
|
|
count += 1
|
|
guard let pair = self.elementPairMapByCreatedAt[$0],
|
|
let element = pair.element as? CRDTContainer
|
|
else {
|
|
return
|
|
}
|
|
|
|
element.getDescendants { _, _ in
|
|
count += 1
|
|
return false
|
|
}
|
|
}
|
|
|
|
self.textWithGarbageSetByCreatedAt.forEach {
|
|
guard let pair = self.elementPairMapByCreatedAt[$0],
|
|
let text = pair.element as? CRDTTextElement
|
|
else {
|
|
return
|
|
}
|
|
|
|
count += text.removedNodesLength
|
|
}
|
|
|
|
return count
|
|
}
|
|
|
|
/**
|
|
* `deepcopy` copies itself deeply.
|
|
*/
|
|
func deepcopy() -> CRDTRoot {
|
|
if let object = self.rootObject.deepcopy() as? CRDTObject {
|
|
return CRDTRoot(rootObject: object)
|
|
}
|
|
|
|
return CRDTRoot()
|
|
}
|
|
|
|
/**
|
|
* `garbageCollect` purges elements that were removed before the given time.
|
|
*/
|
|
@discardableResult
|
|
func garbageCollect(lessThanOrEqualTo ticket: TimeTicket) -> Int {
|
|
var count = 0
|
|
|
|
self.removedElementSetByCreatedAt.forEach {
|
|
guard let pair = self.elementPairMapByCreatedAt[$0],
|
|
let removedAt = pair.element.removedAt, removedAt <= ticket
|
|
else {
|
|
return
|
|
}
|
|
|
|
try? pair.parent?.delete(element: pair.element)
|
|
count += self.garbageCollectInternal(element: pair.element)
|
|
}
|
|
|
|
self.textWithGarbageSetByCreatedAt.forEach {
|
|
guard let pair = self.elementPairMapByCreatedAt[$0],
|
|
let text = pair.element as? CRDTTextElement
|
|
else {
|
|
return
|
|
}
|
|
|
|
let removedNodeCount = text.purgeTextNodesWithGarbage(ticket: ticket)
|
|
guard removedNodeCount > 0 else {
|
|
return
|
|
}
|
|
|
|
self.textWithGarbageSetByCreatedAt.remove(text.createdAt)
|
|
count += removedNodeCount
|
|
}
|
|
|
|
return count
|
|
}
|
|
|
|
private func garbageCollectInternal(element: CRDTElement) -> Int {
|
|
var count = 0
|
|
|
|
let callback: (_ element: CRDTElement, _ parent: CRDTContainer?) -> Bool = { element, _ in
|
|
self.deregisterElement(element)
|
|
count += 1
|
|
return false
|
|
}
|
|
|
|
_ = callback(element, nil)
|
|
|
|
(element as? CRDTContainer)?.getDescendants(callback: callback)
|
|
|
|
return count
|
|
}
|
|
|
|
/**
|
|
* `toJSON` returns the JSON encoding of this root object.
|
|
*/
|
|
func toJSON() -> String {
|
|
return self.rootObject.toJSON()
|
|
}
|
|
|
|
/**
|
|
* `toSortedJSON` returns the sorted JSON encoding of this root object.
|
|
*/
|
|
private func toSortedJSON() -> String {
|
|
return self.rootObject.toSortedJSON()
|
|
}
|
|
}
|
|
|
|
extension CRDTRoot: CustomDebugStringConvertible {
|
|
var debugDescription: String {
|
|
self.toSortedJSON()
|
|
}
|
|
}
|