Skip to content

Commit

Permalink
Handle concurrent editing and styling in Tree
Browse files Browse the repository at this point in the history
  • Loading branch information
humdrum committed May 17, 2024
1 parent bacbf9c commit 6462d14
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 71 deletions.
7 changes: 5 additions & 2 deletions Sources/API/Converter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,9 @@ extension Converter {
pbTreeStyleOperation.parentCreatedAt = toTimeTicket(treeStyleOperation.parentCreatedAt)
pbTreeStyleOperation.from = toTreePos(treeStyleOperation.fromPos)
pbTreeStyleOperation.to = toTreePos(treeStyleOperation.toPos)

treeStyleOperation.maxCreatedAtMapByActor.forEach { key, value in
pbTreeStyleOperation.createdAtMapByActor[key] = toTimeTicket(value)
}
treeStyleOperation.attributes.forEach { key, value in
pbTreeStyleOperation.attributes[key] = value
}
Expand Down Expand Up @@ -511,7 +513,8 @@ extension Converter {
return TreeStyleOperation(parentCreatedAt: fromTimeTicket(pbTreeStyleOperation.parentCreatedAt),
fromPos: fromTreePos(pbTreeStyleOperation.from),
toPos: fromTreePos(pbTreeStyleOperation.to),
attributes: pbTreeStyleOperation.attributes,
maxCreatedAtMapByActor: pbTreeStyleOperation.createdAtMapByActor.compactMapValues({ fromTimeTicket($0) }),
attributes: pbTreeStyleOperation.attributes,
attributesToRemove: pbTreeStyleOperation.attributesToRemove,
executedAt: fromTimeTicket(pbTreeStyleOperation.executedAt))
} else {
Expand Down
13 changes: 13 additions & 0 deletions Sources/API/V1/yorkie/v1/resources.pb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,11 @@ struct Yorkie_V1_Operation {
set {_uniqueStorage()._attributesToRemove = newValue}
}

var createdAtMapByActor: Dictionary<String,Yorkie_V1_TimeTicket> {
get {return _storage._createdAtMapByActor}
set {_uniqueStorage()._createdAtMapByActor = newValue}
}

var unknownFields = SwiftProtobuf.UnknownStorage()

init() {}
Expand Down Expand Up @@ -3344,6 +3349,7 @@ extension Yorkie_V1_Operation.TreeStyle: SwiftProtobuf.Message, SwiftProtobuf._M
4: .same(proto: "attributes"),
5: .standard(proto: "executed_at"),
6: .standard(proto: "attributes_to_remove"),
7: .standard(proto: "created_at_map_by_actor"),
]

fileprivate class _StorageClass {
Expand All @@ -3353,6 +3359,7 @@ extension Yorkie_V1_Operation.TreeStyle: SwiftProtobuf.Message, SwiftProtobuf._M
var _attributes: Dictionary<String,String> = [:]
var _executedAt: Yorkie_V1_TimeTicket? = nil
var _attributesToRemove: [String] = []
var _createdAtMapByActor: Dictionary<String,Yorkie_V1_TimeTicket> = [:]

#if swift(>=5.10)
// This property is used as the initial default value for new instances of the type.
Expand All @@ -3373,6 +3380,7 @@ extension Yorkie_V1_Operation.TreeStyle: SwiftProtobuf.Message, SwiftProtobuf._M
_attributes = source._attributes
_executedAt = source._executedAt
_attributesToRemove = source._attributesToRemove
_createdAtMapByActor = source._createdAtMapByActor
}
}

Expand All @@ -3397,6 +3405,7 @@ extension Yorkie_V1_Operation.TreeStyle: SwiftProtobuf.Message, SwiftProtobuf._M
case 4: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap<SwiftProtobuf.ProtobufString,SwiftProtobuf.ProtobufString>.self, value: &_storage._attributes) }()
case 5: try { try decoder.decodeSingularMessageField(value: &_storage._executedAt) }()
case 6: try { try decoder.decodeRepeatedStringField(value: &_storage._attributesToRemove) }()
case 7: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap<SwiftProtobuf.ProtobufString,Yorkie_V1_TimeTicket>.self, value: &_storage._createdAtMapByActor) }()
default: break
}
}
Expand Down Expand Up @@ -3427,6 +3436,9 @@ extension Yorkie_V1_Operation.TreeStyle: SwiftProtobuf.Message, SwiftProtobuf._M
if !_storage._attributesToRemove.isEmpty {
try visitor.visitRepeatedStringField(value: _storage._attributesToRemove, fieldNumber: 6)
}
if !_storage._createdAtMapByActor.isEmpty {
try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap<SwiftProtobuf.ProtobufString,Yorkie_V1_TimeTicket>.self, value: _storage._createdAtMapByActor, fieldNumber: 7)
}
}
try unknownFields.traverse(visitor: &visitor)
}
Expand All @@ -3442,6 +3454,7 @@ extension Yorkie_V1_Operation.TreeStyle: SwiftProtobuf.Message, SwiftProtobuf._M
if _storage._attributes != rhs_storage._attributes {return false}
if _storage._executedAt != rhs_storage._executedAt {return false}
if _storage._attributesToRemove != rhs_storage._attributesToRemove {return false}
if _storage._createdAtMapByActor != rhs_storage._createdAtMapByActor {return false}
return true
}
if !storagesAreEqual {return false}
Expand Down
1 change: 1 addition & 0 deletions Sources/API/V1/yorkie/v1/resources.proto
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ message Operation {
map<string, string> attributes = 4;
TimeTicket executed_at = 5;
repeated string attributes_to_remove = 6;
map<string, TimeTicket> created_at_map_by_actor = 7;
}

oneof body {
Expand Down
24 changes: 12 additions & 12 deletions Sources/Document/CRDT/CRDTText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ final class CRDTText: CRDTGCElement {
_ content: String,
_ editedAt: TimeTicket,
_ attributes: [String: String]? = nil,
_ latestCreatedAtMapByActor: [String: TimeTicket]? = nil) throws -> ([String: TimeTicket], [TextChange], RGATreeSplitPosRange)
_ maxCreatedAtMapByActor: [String: TimeTicket]? = nil) throws -> ([String: TimeTicket], [TextChange], RGATreeSplitPosRange)
{
let value = !content.isEmpty ? TextValue(content) : nil
if !content.isEmpty, let attributes {
Expand All @@ -172,11 +172,11 @@ final class CRDTText: CRDTGCElement {
}
}

let (caretPos, latestCreatedAtMap, contentChanges) = try self.rgaTreeSplit.edit(
let (caretPos, maxCreatedAtMap, contentChanges) = try self.rgaTreeSplit.edit(
range,
editedAt,
value,
latestCreatedAtMapByActor
maxCreatedAtMapByActor
)

let changes = contentChanges.compactMap { TextChange(type: .content, actor: $0.actor, from: $0.from, to: $0.to, content: $0.content?.toString) }
Expand All @@ -187,7 +187,7 @@ final class CRDTText: CRDTGCElement {
}
}

return (latestCreatedAtMap, changes, (caretPos, caretPos))
return (maxCreatedAtMap, changes, (caretPos, caretPos))
}

/**
Expand All @@ -203,7 +203,7 @@ final class CRDTText: CRDTGCElement {
func setStyle(_ range: RGATreeSplitPosRange,
_ attributes: [String: String],
_ editedAt: TimeTicket,
_ latestCreatedAtMapByActor: [String: TimeTicket] = [:]) throws -> ([String: TimeTicket], [TextChange])
_ maxCreatedAtMapByActor: [String: TimeTicket] = [:]) throws -> ([String: TimeTicket], [TextChange])
{
// 01. split nodes with from and to
let toRight = try self.rgaTreeSplit.findNodeWithSplit(range.1, editedAt).1
Expand All @@ -217,19 +217,19 @@ final class CRDTText: CRDTGCElement {
for node in nodes {
let actorID = node.createdAt.actorID

let latestCreatedAt: TimeTicket
let maxCreatedAt: TimeTicket

if latestCreatedAtMapByActor.isEmpty {
latestCreatedAt = TimeTicket.max
if maxCreatedAtMapByActor.isEmpty {
maxCreatedAt = TimeTicket.max
} else {
latestCreatedAt = latestCreatedAtMapByActor[actorID] ?? TimeTicket.initial
maxCreatedAt = maxCreatedAtMapByActor[actorID] ?? TimeTicket.initial
}

if node.canStyle(editedAt, latestCreatedAt) {
let latestCreatedAt = createdAtMapByActor[actorID]
if node.canStyle(editedAt, maxCreatedAt) {
let maxCreatedAt = createdAtMapByActor[actorID]
let createdAt = node.createdAt

if latestCreatedAt == nil || createdAt.after(latestCreatedAt!) {
if maxCreatedAt == nil || createdAt.after(maxCreatedAt!) {
createdAtMapByActor[actorID] = createdAt
}
toBeStyleds.append(node)
Expand Down
97 changes: 64 additions & 33 deletions Sources/Document/CRDT/CRDTTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,19 @@ final class CRDTTreeNode: IndexTreeNode {
/**
* `canDelete` checks if node is able to delete.
*/
func canDelete(_ editedAt: TimeTicket, _ latestCreatedAt: TimeTicket) -> Bool {
!self.createdAt.after(latestCreatedAt) && (self.removedAt == nil || editedAt.after(self.removedAt!))
func canDelete(_ editedAt: TimeTicket, _ maxCreatedAt: TimeTicket) -> Bool {
!self.createdAt.after(maxCreatedAt) && (self.removedAt == nil || editedAt.after(self.removedAt!))
}

/**
* `canStyle` checks if node is able to style.
*/
func canStyle(_ editedAt: TimeTicket, _ maxCreatedAt: TimeTicket) -> Bool {
if self.isText {
return false
}

return !self.createdAt.after(maxCreatedAt) && (self.removedAt == nil || editedAt.after(self.removedAt!))
}

/**
Expand Down Expand Up @@ -572,51 +583,71 @@ class CRDTTree: CRDTGCElement {
* `style` applies the given attributes of the given range.
*/
@discardableResult
func style(_ range: TreePosRange, _ attributes: [String: String]?, _ editedAt: TimeTicket) throws -> [TreeChange] {
try self.performChangeStyle(range, attributes, nil, editedAt)
func style(_ range: TreePosRange, _ attributes: [String: String]?, _ editedAt: TimeTicket, _ maxCreatedAtMapByActor: [String: TimeTicket]?) throws -> ([String: TimeTicket], [TreeChange]) {
let (fromParent, fromLeft) = try self.findNodesAndSplitText(range.0, editedAt)
let (toParent, toLeft) = try self.findNodesAndSplitText(range.1, editedAt)

var changes: [TreeChange] = []
let value = attributes != nil ? TreeChangeValue.attributes(attributes!) : nil
var createdAtMapByActor = [String: TimeTicket]()

try self.traverseInPosRange(fromParent, fromLeft, toParent, toLeft) { token, _ in
let (node, _) = token
let actorID = node.createdAt.actorID
var maxCreatedAt: TimeTicket? = maxCreatedAtMapByActor != nil ? maxCreatedAtMapByActor?[actorID] ?? TimeTicket.initial : TimeTicket.max

if node.canStyle(editedAt, maxCreatedAt!), !node.isText, attributes != nil {
maxCreatedAt = createdAtMapByActor[actorID]
let createdAt = node.createdAt
if maxCreatedAt == nil || createdAt.after(maxCreatedAt!) {
createdAtMapByActor[actorID] = createdAt
}

if node.attrs == nil {
node.attrs = RHT()
}
for (key, value) in attributes ?? [:] {
node.attrs?.set(key: key, value: value, executedAt: editedAt)
}

try changes.append(TreeChange(actor: editedAt.actorID,
type: .style,
from: self.toIndex(fromParent, fromLeft),
to: self.toIndex(toParent, toLeft),
fromPath: self.toPath(fromParent, fromLeft),
toPath: self.toPath(toParent, toLeft),
value: value,
splitLevel: 0) // dummy value.
)
}
}

return (createdAtMapByActor, changes)
}

/**
* `removeStyle` removes the given attributes of the given range.
*/
@discardableResult
func removeStyle(_ range: TreePosRange, _ attributesToRemove: [String], _ editedAt: TimeTicket) throws -> [TreeChange] {
try self.performChangeStyle(range, nil, attributesToRemove, editedAt)
}

private func performChangeStyle(_ range: TreePosRange, _ attributes: [String: String]?, _ attributesToRemove: [String]?, _ editedAt: TimeTicket) throws -> [TreeChange] {
let (fromParent, fromLeft) = try self.findNodesAndSplitText(range.0, editedAt)
let (toParent, toLeft) = try self.findNodesAndSplitText(range.1, editedAt)
var changes: [TreeChange] = []

let value: TreeChangeValue?
let type: TreeChangeType

if let attributes {
value = .attributes(attributes)
type = .style
} else if let attributesToRemove {
value = .attributesToRemove(attributesToRemove)
type = .removeStyle
} else {
fatalError()
}
let value = TreeChangeValue.attributesToRemove(attributesToRemove)

try self.traverseInPosRange(fromParent, fromLeft, toParent, toLeft) { token, _ in
let (node, _) = token
if node.isRemoved == false, node.isText == false {
if node.attrs == nil {
node.attrs = RHT()
}
for (key, value) in attributes ?? [:] {
node.attrs?.set(key: key, value: value, executedAt: editedAt)
}
for key in attributesToRemove ?? [] {
for key in attributesToRemove {
node.attrs?.remove(key: key, executedAt: editedAt)
}

try changes.append(TreeChange(actor: editedAt.actorID,
type: type,
type: .removeStyle,
from: self.toIndex(fromParent, fromLeft),
to: self.toIndex(toParent, toLeft),
fromPath: self.toPath(fromParent, fromLeft),
Expand All @@ -635,7 +666,7 @@ class CRDTTree: CRDTGCElement {
* If the content is undefined, the range will be removed.
*/
@discardableResult
func edit(_ range: TreePosRange, _ contents: [CRDTTreeNode]?, _ splitLevel: Int32, _ editedAt: TimeTicket, _ latestCreatedAtMapByActor: [String: TimeTicket] = [:], _ issueTimeTicket: () -> TimeTicket) throws -> ([TreeChange], [String: TimeTicket]) {
func edit(_ range: TreePosRange, _ contents: [CRDTTreeNode]?, _ splitLevel: Int32, _ editedAt: TimeTicket, _ maxCreatedAtMapByActor: [String: TimeTicket] = [:], _ issueTimeTicket: () -> TimeTicket) throws -> ([TreeChange], [String: TimeTicket]) {
// 01. find nodes from the given range and split nodes.
let (fromParent, fromLeft) = try self.findNodesAndSplitText(range.0, editedAt)
let (toParent, toLeft) = try self.findNodesAndSplitText(range.1, editedAt)
Expand All @@ -646,7 +677,7 @@ class CRDTTree: CRDTGCElement {
var nodesToBeRemoved = [CRDTTreeNode]()
var tokensToBeRemoved = [TreeToken<CRDTTreeNode>]()
var toBeMovedToFromParents = [CRDTTreeNode]()
var latestCreatedAtMap = [String: TimeTicket]()
var maxCreatedAtMap = [String: TimeTicket]()
try self.traverseInPosRange(fromParent, fromLeft, toParent, toLeft) { treeToken, ended in
// NOTE(hackerwins): If the node overlaps as a start tag with the
// range then we need to move the remaining children to fromParent.
Expand All @@ -665,16 +696,16 @@ class CRDTTree: CRDTGCElement {

let actorID = node.createdAt.actorID

let latestCreatedAt = latestCreatedAtMapByActor.isEmpty == false ? latestCreatedAtMapByActor[actorID] ?? TimeTicket.initial : TimeTicket.max
let maxCreatedAt = maxCreatedAtMapByActor.isEmpty == false ? maxCreatedAtMapByActor[actorID] ?? TimeTicket.initial : TimeTicket.max

// NOTE(sejongk): If the node is removable or its parent is going to
// be removed, then this node should be removed.
if node.canDelete(editedAt, latestCreatedAt) || nodesToBeRemoved.contains(where: { $0 === node.parent }) {
let latestCreatedAt = latestCreatedAtMap[actorID]
if node.canDelete(editedAt, maxCreatedAt) || nodesToBeRemoved.contains(where: { $0 === node.parent }) {
let maxCreatedAt = maxCreatedAtMap[actorID]
let createdAt = node.createdAt

if latestCreatedAt == nil || createdAt.after(latestCreatedAt!) {
latestCreatedAtMap[actorID] = createdAt
if maxCreatedAt == nil || createdAt.after(maxCreatedAt!) {
maxCreatedAtMap[actorID] = createdAt
}

// NOTE(hackerwins): If the node overlaps as an end token with the
Expand Down Expand Up @@ -780,7 +811,7 @@ class CRDTTree: CRDTGCElement {
}
}

return (changes, latestCreatedAtMap)
return (changes, maxCreatedAtMap)
}

/**
Expand Down
Loading

0 comments on commit 6462d14

Please sign in to comment.