Skip to content

Commit e086ad7

Browse files
authored
Handle concurrent editing and styling in Tree (#803)
We will use maxCreatedAtMapByActor until the introduction of VersionVector.
1 parent ba635e6 commit e086ad7

File tree

10 files changed

+121
-47
lines changed

10 files changed

+121
-47
lines changed

src/api/converter.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,10 @@ function toOperation(operation: Operation): PbOperation {
444444
);
445445
pbTreeStyleOperation.from = toTreePos(treeStyleOperation.getFromPos());
446446
pbTreeStyleOperation.to = toTreePos(treeStyleOperation.getToPos());
447+
const pbCreatedAtMapByActor = pbTreeStyleOperation.createdAtMapByActor;
448+
for (const [key, value] of treeStyleOperation.getMaxCreatedAtMapByActor()) {
449+
pbCreatedAtMapByActor[key] = toTimeTicket(value)!;
450+
}
447451

448452
const attributesToRemove = treeStyleOperation.getAttributesToRemove();
449453
if (attributesToRemove.length > 0) {
@@ -1177,6 +1181,12 @@ function fromOperation(pbOperation: PbOperation): Operation | undefined {
11771181
const pbTreeStyleOperation = pbOperation.body.value;
11781182
const attributes = new Map();
11791183
const attributesToRemove = pbTreeStyleOperation.attributesToRemove;
1184+
const createdAtMapByActor = new Map();
1185+
Object.entries(pbTreeStyleOperation!.createdAtMapByActor).forEach(
1186+
([key, value]) => {
1187+
createdAtMapByActor.set(key, fromTimeTicket(value));
1188+
},
1189+
);
11801190

11811191
if (attributesToRemove.length > 0) {
11821192
return TreeStyleOperation.createTreeRemoveStyleOperation(
@@ -1196,6 +1206,7 @@ function fromOperation(pbOperation: PbOperation): Operation | undefined {
11961206
fromTimeTicket(pbTreeStyleOperation!.parentCreatedAt)!,
11971207
fromTreePos(pbTreeStyleOperation!.from!),
11981208
fromTreePos(pbTreeStyleOperation!.to!),
1209+
createdAtMapByActor,
11991210
attributes,
12001211
fromTimeTicket(pbTreeStyleOperation!.executedAt)!,
12011212
);

src/api/yorkie/v1/resources.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ message Operation {
134134
map<string, string> attributes = 4;
135135
TimeTicket executed_at = 5;
136136
repeated string attributes_to_remove = 6;
137+
map<string, TimeTicket> created_at_map_by_actor = 7;
137138
}
138139

139140
oneof body {

src/api/yorkie/v1/resources_pb.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,11 @@ export declare class Operation_TreeStyle extends Message<Operation_TreeStyle> {
788788
*/
789789
attributesToRemove: string[];
790790

791+
/**
792+
* @generated from field: map<string, yorkie.v1.TimeTicket> created_at_map_by_actor = 7;
793+
*/
794+
createdAtMapByActor: { [key: string]: TimeTicket };
795+
791796
constructor(data?: PartialMessage<Operation_TreeStyle>);
792797

793798
static readonly runtime: typeof proto3;

src/api/yorkie/v1/resources_pb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ const Operation_TreeStyle = proto3.makeMessageType(
286286
{ no: 4, name: "attributes", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "scalar", T: 9 /* ScalarType.STRING */} },
287287
{ no: 5, name: "executed_at", kind: "message", T: TimeTicket },
288288
{ no: 6, name: "attributes_to_remove", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
289+
{ no: 7, name: "created_at_map_by_actor", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "message", T: TimeTicket} },
289290
],
290291
{localName: "Operation_TreeStyle"},
291292
);

src/document/crdt/rga_tree_split.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -437,19 +437,19 @@ export class RGATreeSplitNode<
437437
/**
438438
* `canDelete` checks if node is able to delete.
439439
*/
440-
public canDelete(editedAt: TimeTicket, latestCreatedAt: TimeTicket): boolean {
440+
public canDelete(editedAt: TimeTicket, maxCreatedAt: TimeTicket): boolean {
441441
return (
442-
!this.getCreatedAt().after(latestCreatedAt) &&
442+
!this.getCreatedAt().after(maxCreatedAt) &&
443443
(!this.removedAt || editedAt.after(this.removedAt))
444444
);
445445
}
446446

447447
/**
448448
* `canStyle` checks if node is able to set style.
449449
*/
450-
public canStyle(editedAt: TimeTicket, latestCreatedAt: TimeTicket): boolean {
450+
public canStyle(editedAt: TimeTicket, maxCreatedAt: TimeTicket): boolean {
451451
return (
452-
!this.getCreatedAt().after(latestCreatedAt) &&
452+
!this.getCreatedAt().after(maxCreatedAt) &&
453453
(!this.removedAt || editedAt.after(this.removedAt))
454454
);
455455
}
@@ -532,23 +532,23 @@ export class RGATreeSplit<T extends RGATreeSplitValue> {
532532
* @param range - range of RGATreeSplitNode
533533
* @param editedAt - edited time
534534
* @param value - value
535-
* @param latestCreatedAtMapByActor - latestCreatedAtMapByActor
535+
* @param maxCreatedAtMapByActor - maxCreatedAtMapByActor
536536
* @returns `[RGATreeSplitPos, Map<string, TimeTicket>, Array<Change>]`
537537
*/
538538
public edit(
539539
range: RGATreeSplitPosRange,
540540
editedAt: TimeTicket,
541541
value?: T,
542-
latestCreatedAtMapByActor?: Map<string, TimeTicket>,
542+
maxCreatedAtMapByActor?: Map<string, TimeTicket>,
543543
): [RGATreeSplitPos, Map<string, TimeTicket>, Array<ValueChange<T>>] {
544544
// 01. split nodes with from and to
545545
const [toLeft, toRight] = this.findNodeWithSplit(range[1], editedAt);
546546
const [fromLeft, fromRight] = this.findNodeWithSplit(range[0], editedAt);
547547

548548
// 02. delete between from and to
549549
const nodesToDelete = this.findBetween(fromRight, toRight);
550-
const [changes, latestCreatedAtMap, removedNodeMapByNodeKey] =
551-
this.deleteNodes(nodesToDelete, editedAt, latestCreatedAtMapByActor);
550+
const [changes, maxCreatedAtMap, removedNodeMapByNodeKey] =
551+
this.deleteNodes(nodesToDelete, editedAt, maxCreatedAtMapByActor);
552552

553553
const caretID = toRight ? toRight.getID() : toLeft.getID();
554554
let caretPos = RGATreeSplitPos.of(caretID, 0);
@@ -584,7 +584,7 @@ export class RGATreeSplit<T extends RGATreeSplitValue> {
584584
this.removedNodeMap.set(key, removedNode);
585585
}
586586

587-
return [caretPos, latestCreatedAtMap, changes];
587+
return [caretPos, maxCreatedAtMap, changes];
588588
}
589589

590590
/**
@@ -845,7 +845,7 @@ export class RGATreeSplit<T extends RGATreeSplitValue> {
845845
private deleteNodes(
846846
candidates: Array<RGATreeSplitNode<T>>,
847847
editedAt: TimeTicket,
848-
latestCreatedAtMapByActor?: Map<string, TimeTicket>,
848+
maxCreatedAtMapByActor?: Map<string, TimeTicket>,
849849
): [
850850
Array<ValueChange<T>>,
851851
Map<string, TimeTicket>,
@@ -861,7 +861,7 @@ export class RGATreeSplit<T extends RGATreeSplitValue> {
861861
const [nodesToDelete, nodesToKeep] = this.filterNodes(
862862
candidates,
863863
editedAt,
864-
latestCreatedAtMapByActor,
864+
maxCreatedAtMapByActor,
865865
);
866866

867867
const createdAtMapByActor = new Map();
@@ -889,9 +889,9 @@ export class RGATreeSplit<T extends RGATreeSplitValue> {
889889
private filterNodes(
890890
candidates: Array<RGATreeSplitNode<T>>,
891891
editedAt: TimeTicket,
892-
latestCreatedAtMapByActor?: Map<string, TimeTicket>,
892+
maxCreatedAtMapByActor?: Map<string, TimeTicket>,
893893
): [Array<RGATreeSplitNode<T>>, Array<RGATreeSplitNode<T> | undefined>] {
894-
const isRemote = !!latestCreatedAtMapByActor;
894+
const isRemote = !!maxCreatedAtMapByActor;
895895
const nodesToDelete: Array<RGATreeSplitNode<T>> = [];
896896
const nodesToKeep: Array<RGATreeSplitNode<T> | undefined> = [];
897897

@@ -901,13 +901,13 @@ export class RGATreeSplit<T extends RGATreeSplitValue> {
901901
for (const node of candidates) {
902902
const actorID = node.getCreatedAt().getActorID();
903903

904-
const latestCreatedAt = isRemote
905-
? latestCreatedAtMapByActor!.has(actorID)
906-
? latestCreatedAtMapByActor!.get(actorID)
904+
const maxCreatedAt = isRemote
905+
? maxCreatedAtMapByActor!.has(actorID)
906+
? maxCreatedAtMapByActor!.get(actorID)
907907
: InitialTimeTicket
908908
: MaxTimeTicket;
909909

910-
if (node.canDelete(editedAt, latestCreatedAt!)) {
910+
if (node.canDelete(editedAt, maxCreatedAt!)) {
911911
nodesToDelete.push(node);
912912
} else {
913913
nodesToKeep.push(node);

src/document/crdt/text.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export class CRDTText<A extends Indexable = Indexable> extends CRDTGCElement {
191191
content: string,
192192
editedAt: TimeTicket,
193193
attributes?: Record<string, string>,
194-
latestCreatedAtMapByActor?: Map<string, TimeTicket>,
194+
maxCreatedAtMapByActor?: Map<string, TimeTicket>,
195195
): [Map<string, TimeTicket>, Array<TextChange<A>>, RGATreeSplitPosRange] {
196196
const crdtTextValue = content ? CRDTTextValue.create(content) : undefined;
197197
if (crdtTextValue && attributes) {
@@ -200,11 +200,11 @@ export class CRDTText<A extends Indexable = Indexable> extends CRDTGCElement {
200200
}
201201
}
202202

203-
const [caretPos, latestCreatedAtMap, valueChanges] = this.rgaTreeSplit.edit(
203+
const [caretPos, maxCreatedAtMap, valueChanges] = this.rgaTreeSplit.edit(
204204
range,
205205
editedAt,
206206
crdtTextValue,
207-
latestCreatedAtMapByActor,
207+
maxCreatedAtMapByActor,
208208
);
209209

210210
const changes: Array<TextChange<A>> = valueChanges.map((change) => ({
@@ -221,7 +221,7 @@ export class CRDTText<A extends Indexable = Indexable> extends CRDTGCElement {
221221
type: TextChangeType.Content,
222222
}));
223223

224-
return [latestCreatedAtMap, changes, [caretPos, caretPos]];
224+
return [maxCreatedAtMap, changes, [caretPos, caretPos]];
225225
}
226226

227227
/**
@@ -238,7 +238,7 @@ export class CRDTText<A extends Indexable = Indexable> extends CRDTGCElement {
238238
range: RGATreeSplitPosRange,
239239
attributes: Record<string, string>,
240240
editedAt: TimeTicket,
241-
latestCreatedAtMapByActor?: Map<string, TimeTicket>,
241+
maxCreatedAtMapByActor?: Map<string, TimeTicket>,
242242
): [Map<string, TimeTicket>, Array<TextChange<A>>] {
243243
// 01. split nodes with from and to
244244
const [, toRight] = this.rgaTreeSplit.findNodeWithSplit(range[1], editedAt);
@@ -256,16 +256,16 @@ export class CRDTText<A extends Indexable = Indexable> extends CRDTGCElement {
256256
for (const node of nodes) {
257257
const actorID = node.getCreatedAt().getActorID();
258258

259-
const latestCreatedAt = latestCreatedAtMapByActor?.size
260-
? latestCreatedAtMapByActor!.has(actorID)
261-
? latestCreatedAtMapByActor!.get(actorID)!
259+
const maxCreatedAt = maxCreatedAtMapByActor?.size
260+
? maxCreatedAtMapByActor!.has(actorID)
261+
? maxCreatedAtMapByActor!.get(actorID)!
262262
: InitialTimeTicket
263263
: MaxTimeTicket;
264264

265-
if (node.canStyle(editedAt, latestCreatedAt)) {
266-
const latestCreatedAt = createdAtMapByActor.get(actorID);
265+
if (node.canStyle(editedAt, maxCreatedAt)) {
266+
const maxCreatedAt = createdAtMapByActor.get(actorID);
267267
const createdAt = node.getCreatedAt();
268-
if (!latestCreatedAt || createdAt.after(latestCreatedAt)) {
268+
if (!maxCreatedAt || createdAt.after(maxCreatedAt)) {
269269
createdAtMapByActor.set(actorID, createdAt);
270270
}
271271
toBeStyleds.push(node);

src/document/crdt/tree.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -577,9 +577,19 @@ export class CRDTTreeNode extends IndexTreeNode<CRDTTreeNode> {
577577
/**
578578
* `canDelete` checks if node is able to delete.
579579
*/
580-
public canDelete(editedAt: TimeTicket, latestCreatedAt: TimeTicket): boolean {
580+
public canDelete(editedAt: TimeTicket, maxCreatedAt: TimeTicket): boolean {
581581
return (
582-
!this.getCreatedAt().after(latestCreatedAt) &&
582+
!this.getCreatedAt().after(maxCreatedAt) &&
583+
(!this.removedAt || editedAt.after(this.removedAt))
584+
);
585+
}
586+
587+
/**
588+
* `canStyle` checks if node is able to style.
589+
*/
590+
public canStyle(editedAt: TimeTicket, maxCreatedAt: TimeTicket): boolean {
591+
return (
592+
!this.getCreatedAt().after(maxCreatedAt) &&
583593
(!this.removedAt || editedAt.after(this.removedAt))
584594
);
585595
}
@@ -747,7 +757,8 @@ export class CRDTTree extends CRDTGCElement {
747757
range: [CRDTTreePos, CRDTTreePos],
748758
attributes: { [key: string]: string } | undefined,
749759
editedAt: TimeTicket,
750-
) {
760+
maxCreatedAtMapByActor: Map<string, TimeTicket> | undefined,
761+
): [Map<string, TimeTicket>, Array<TreeChange>] {
751762
const [fromParent, fromLeft] = this.findNodesAndSplitText(
752763
range[0],
753764
editedAt,
@@ -756,13 +767,30 @@ export class CRDTTree extends CRDTGCElement {
756767

757768
const changes: Array<TreeChange> = [];
758769
const value = attributes ? parseObjectValues(attributes) : undefined;
770+
const createdAtMapByActor = new Map<string, TimeTicket>();
759771
this.traverseInPosRange(
760772
fromParent,
761773
fromLeft,
762774
toParent,
763775
toLeft,
764776
([node]) => {
765-
if (!node.isRemoved && !node.isText && attributes) {
777+
const actorID = node.getCreatedAt().getActorID();
778+
let maxCreatedAt: TimeTicket | undefined = maxCreatedAtMapByActor
779+
? maxCreatedAtMapByActor!.has(actorID)
780+
? maxCreatedAtMapByActor!.get(actorID)!
781+
: InitialTimeTicket
782+
: MaxTimeTicket;
783+
784+
if (
785+
node.canStyle(editedAt, maxCreatedAt) &&
786+
!node.isText &&
787+
attributes
788+
) {
789+
maxCreatedAt = createdAtMapByActor!.get(actorID);
790+
const createdAt = node.getCreatedAt();
791+
if (!maxCreatedAt || createdAt.after(maxCreatedAt)) {
792+
createdAtMapByActor.set(actorID, createdAt);
793+
}
766794
if (!node.attrs) {
767795
node.attrs = new RHT();
768796
}
@@ -784,7 +812,7 @@ export class CRDTTree extends CRDTGCElement {
784812
},
785813
);
786814

787-
return changes;
815+
return [createdAtMapByActor, changes];
788816
}
789817

790818
/**
@@ -844,7 +872,7 @@ export class CRDTTree extends CRDTGCElement {
844872
splitLevel: number,
845873
editedAt: TimeTicket,
846874
issueTimeTicket: (() => TimeTicket) | undefined,
847-
latestCreatedAtMapByActor?: Map<string, TimeTicket>,
875+
maxCreatedAtMapByActor?: Map<string, TimeTicket>,
848876
): [Array<TreeChange>, Map<string, TimeTicket>] {
849877
// 01. find nodes from the given range and split nodes.
850878
const [fromParent, fromLeft] = this.findNodesAndSplitText(
@@ -859,7 +887,7 @@ export class CRDTTree extends CRDTGCElement {
859887
const nodesToBeRemoved: Array<CRDTTreeNode> = [];
860888
const tokensToBeRemoved: Array<TreeToken<CRDTTreeNode>> = [];
861889
const toBeMovedToFromParents: Array<CRDTTreeNode> = [];
862-
const latestCreatedAtMap = new Map<string, TimeTicket>();
890+
const maxCreatedAtMap = new Map<string, TimeTicket>();
863891
this.traverseInPosRange(
864892
fromParent,
865893
fromLeft,
@@ -883,23 +911,23 @@ export class CRDTTree extends CRDTGCElement {
883911
}
884912

885913
const actorID = node.getCreatedAt().getActorID();
886-
const latestCreatedAt = latestCreatedAtMapByActor
887-
? latestCreatedAtMapByActor!.has(actorID)
888-
? latestCreatedAtMapByActor!.get(actorID)!
914+
const maxCreatedAt = maxCreatedAtMapByActor
915+
? maxCreatedAtMapByActor!.has(actorID)
916+
? maxCreatedAtMapByActor!.get(actorID)!
889917
: InitialTimeTicket
890918
: MaxTimeTicket;
891919

892920
// NOTE(sejongk): If the node is removable or its parent is going to
893921
// be removed, then this node should be removed.
894922
if (
895-
node.canDelete(editedAt, latestCreatedAt) ||
923+
node.canDelete(editedAt, maxCreatedAt) ||
896924
nodesToBeRemoved.includes(node.parent!)
897925
) {
898-
const latestCreatedAt = latestCreatedAtMap.get(actorID);
926+
const maxCreatedAt = maxCreatedAtMap.get(actorID);
899927
const createdAt = node.getCreatedAt();
900928

901-
if (!latestCreatedAt || createdAt.after(latestCreatedAt)) {
902-
latestCreatedAtMap.set(actorID, createdAt);
929+
if (!maxCreatedAt || createdAt.after(maxCreatedAt)) {
930+
maxCreatedAtMap.set(actorID, createdAt);
903931
}
904932

905933
// NOTE(hackerwins): If the node overlaps as an end token with the
@@ -1003,7 +1031,7 @@ export class CRDTTree extends CRDTGCElement {
10031031
}
10041032
}
10051033

1006-
return [changes, latestCreatedAtMap];
1034+
return [changes, maxCreatedAtMap];
10071035
}
10081036

10091037
/**

0 commit comments

Comments
 (0)