Skip to content

Commit

Permalink
Concurrent case handling for Yorkie.tree (#611)
Browse files Browse the repository at this point in the history
Introduced a new logical timestamp for identifying the position in
local/remote editing, and ensures commutative editing in concurrent
cases.

This logical timestamp consists of {parentID, leftSiblingID}. This
allows editing at the front of text nodes by using a reference to the
parent, as well as getting rid of the local offset used to access an
element node's children previously by using the leftSiblingID.

---------

Co-authored-by: JOOHOJANG <[email protected]>
Co-authored-by: Youngteac Hong <[email protected]>
Co-authored-by: sejongk <[email protected]>
Co-authored-by: MoonGyu1 <[email protected]>
  • Loading branch information
5 people authored Aug 21, 2023
1 parent 9ea05a4 commit 990a6d6
Show file tree
Hide file tree
Showing 19 changed files with 2,860 additions and 1,621 deletions.
2 changes: 1 addition & 1 deletion public/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const network = {
(event.type == 'stream-connection-status-changed' &&
event.value == 'connected') ||
(event.type == 'document-sync-result' && event.value == 'synced') ||
event.type == 'documents-changed')
event.type == 'document-changed')
) {
network.showOnline(elem);
}
Expand Down
55 changes: 43 additions & 12 deletions src/api/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { RGATreeList } from '@yorkie-js-sdk/src/document/crdt/rga_tree_list';
import { CRDTElement } from '@yorkie-js-sdk/src/document/crdt/element';
import { CRDTObject } from '@yorkie-js-sdk/src/document/crdt/object';
import { CRDTArray } from '@yorkie-js-sdk/src/document/crdt/array';
import { CRDTTreePos } from './../document/crdt/tree';
import {
RGATreeSplit,
RGATreeSplitNode,
Expand Down Expand Up @@ -78,6 +79,7 @@ import {
TreeNode as PbTreeNode,
TreeNodes as PbTreeNodes,
TreePos as PbTreePos,
TreeNodeID as PbTreeNodeID,
} from '@yorkie-js-sdk/src/api/yorkie/v1/resources_pb';
import { IncreaseOperation } from '@yorkie-js-sdk/src/document/operation/increase_operation';
import {
Expand All @@ -87,7 +89,7 @@ import {
import {
CRDTTree,
CRDTTreeNode,
CRDTTreePos,
CRDTTreeNodeID,
} from '@yorkie-js-sdk/src/document/crdt/tree';
import { traverse } from '../util/index_tree';
import { TreeStyleOperation } from '../document/operation/tree_style_operation';
Expand Down Expand Up @@ -261,11 +263,21 @@ function toTextNodePos(pos: RGATreeSplitPos): PbTextNodePos {
*/
function toTreePos(pos: CRDTTreePos): PbTreePos {
const pbTreePos = new PbTreePos();
pbTreePos.setCreatedAt(toTimeTicket(pos.getCreatedAt()));
pbTreePos.setOffset(pos.getOffset());
pbTreePos.setParentId(toTreeNodeID(pos.getParentID()));
pbTreePos.setLeftSiblingId(toTreeNodeID(pos.getLeftSiblingID()));
return pbTreePos;
}

/**
* `toTreeNodeID` converts the given model to Protobuf format.
*/
function toTreeNodeID(treeNodeID: CRDTTreeNodeID): PbTreeNodeID {
const pbTreeNodeID = new PbTreeNodeID();
pbTreeNodeID.setCreatedAt(toTimeTicket(treeNodeID.getCreatedAt()));
pbTreeNodeID.setOffset(treeNodeID.getOffset());
return pbTreeNodeID;
}

/**
* `toOperation` converts the given model to Protobuf format.
*/
Expand Down Expand Up @@ -380,6 +392,11 @@ function toOperation(operation: Operation): PbOperation {
} else if (operation instanceof TreeEditOperation) {
const treeEditOperation = operation as TreeEditOperation;
const pbTreeEditOperation = new PbOperation.TreeEdit();
const pbCreatedAtMapByActor =
pbTreeEditOperation.getCreatedAtMapByActorMap();
for (const [key, value] of treeEditOperation.getMaxCreatedAtMapByActor()) {
pbCreatedAtMapByActor.set(key, toTimeTicket(value)!);
}
pbTreeEditOperation.setParentCreatedAt(
toTimeTicket(treeEditOperation.getParentCreatedAt()),
);
Expand Down Expand Up @@ -545,7 +562,7 @@ function toTreeNodes(node: CRDTTreeNode): Array<PbTreeNode> {
const pbTreeNodes: Array<PbTreeNode> = [];
traverse(node, (n, depth) => {
const pbTreeNode = new PbTreeNode();
pbTreeNode.setPos(toTreePos(n.pos));
pbTreeNode.setId(toTreeNodeID(n.id));
pbTreeNode.setType(n.type);
if (n.isText) {
pbTreeNode.setValue(n.value);
Expand Down Expand Up @@ -856,8 +873,6 @@ function fromElementSimple(pbElementSimple: PbJSONElementSimple): CRDTElement {
fromTimeTicket(pbElementSimple.getCreatedAt())!,
);
}

throw new YorkieError(Code.Unimplemented, `unimplemented element`);
}

/**
Expand Down Expand Up @@ -909,8 +924,18 @@ function fromTextNode(pbTextNode: PbTextNode): RGATreeSplitNode<CRDTTextValue> {
*/
function fromTreePos(pbTreePos: PbTreePos): CRDTTreePos {
return CRDTTreePos.of(
fromTimeTicket(pbTreePos.getCreatedAt())!,
pbTreePos.getOffset(),
fromTreeNodeID(pbTreePos.getParentId()!),
fromTreeNodeID(pbTreePos.getLeftSiblingId()!),
);
}

/**
* `fromTreeNodeID` converts the given Protobuf format to model format.
*/
function fromTreeNodeID(pbTreeNodeID: PbTreeNodeID): CRDTTreeNodeID {
return CRDTTreeNodeID.of(
fromTimeTicket(pbTreeNodeID.getCreatedAt())!,
pbTreeNodeID.getOffset(),
);
}

Expand All @@ -925,10 +950,8 @@ function fromTreeNodesWhenEdit(
}

const treeNodes: Array<CRDTTreeNode> = [];

pbTreeNodes.forEach((node) => {
const treeNode = fromTreeNodes(node.getContentList());

treeNodes.push(treeNode!);
});

Expand Down Expand Up @@ -971,8 +994,8 @@ function fromTreeNodes(
* `fromTreeNode` converts the given Protobuf format to model format.
*/
function fromTreeNode(pbTreeNode: PbTreeNode): CRDTTreeNode {
const pos = fromTreePos(pbTreeNode.getPos()!);
const node = CRDTTreeNode.create(pos, pbTreeNode.getType());
const id = fromTreeNodeID(pbTreeNode.getId()!);
const node = CRDTTreeNode.create(id, pbTreeNode.getType());
if (node.isText) {
node.value = pbTreeNode.getValue();
} else {
Expand All @@ -982,6 +1005,9 @@ function fromTreeNode(pbTreeNode: PbTreeNode): CRDTTreeNode {
});
node.attrs = attrs;
}

node.removedAt = fromTimeTicket(pbTreeNode.getRemovedAt());

return node;
}

Expand Down Expand Up @@ -1073,10 +1099,15 @@ function fromOperations(pbOperations: Array<PbOperation>): Array<Operation> {
);
} else if (pbOperation.hasTreeEdit()) {
const pbTreeEditOperation = pbOperation.getTreeEdit();
const createdAtMapByActor = new Map();
pbTreeEditOperation!.getCreatedAtMapByActorMap().forEach((value, key) => {
createdAtMapByActor.set(key, fromTimeTicket(value));
});
operation = TreeEditOperation.create(
fromTimeTicket(pbTreeEditOperation!.getParentCreatedAt())!,
fromTreePos(pbTreeEditOperation!.getFrom()!),
fromTreePos(pbTreeEditOperation!.getTo()!),
createdAtMapByActor,
fromTreeNodesWhenEdit(pbTreeEditOperation!.getContentsList()),
fromTimeTicket(pbTreeEditOperation!.getExecutedAt())!,
);
Expand Down
28 changes: 19 additions & 9 deletions src/api/yorkie/v1/resources.proto
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ message Operation {
TimeTicket executed_at = 6;
map<string, string> attributes = 7;
}
// NOTE(hackerwins): Select Operation is not used in the current version.
// In the previous version, it was used to represent selection of Text.
// However, it has been replaced by Presence now. It is retained for backward
// compatibility purposes.
message Select {
TimeTicket parent_created_at = 1;
TextNodePos from = 2;
Expand All @@ -117,8 +121,9 @@ message Operation {
TimeTicket parent_created_at = 1;
TreePos from = 2;
TreePos to = 3;
repeated TreeNodes contents = 4;
TimeTicket executed_at = 5;
map<string, TimeTicket> created_at_map_by_actor = 4;
repeated TreeNodes contents = 5;
TimeTicket executed_at = 6;
}
message TreeStyle {
TimeTicket parent_created_at = 1;
Expand Down Expand Up @@ -233,11 +238,11 @@ message TextNodeID {
}

message TreeNode {
TreePos pos = 1;
TreeNodeID id = 1;
string type = 2;
string value = 3;
TimeTicket removed_at = 4;
TreePos ins_prev_pos = 5;
TreeNodeID ins_prev_id = 5;
int32 depth = 6;
map<string, NodeAttr> attributes = 7;
}
Expand All @@ -246,11 +251,16 @@ message TreeNodes {
repeated TreeNode content = 1;
}

message TreePos {
message TreeNodeID {
TimeTicket created_at = 1;
int32 offset = 2;
}

message TreePos {
TreeNodeID parent_id = 1;
TreeNodeID left_sibling_id = 2;
}

/////////////////////////////////////////
// Messages for Common //
/////////////////////////////////////////
Expand Down Expand Up @@ -343,12 +353,12 @@ enum ValueType {
}

enum DocEventType {
DOC_EVENT_TYPE_DOCUMENTS_CHANGED = 0;
DOC_EVENT_TYPE_DOCUMENTS_WATCHED = 1;
DOC_EVENT_TYPE_DOCUMENTS_UNWATCHED = 2;
DOC_EVENT_TYPE_DOCUMENT_CHANGED = 0;
DOC_EVENT_TYPE_DOCUMENT_WATCHED = 1;
DOC_EVENT_TYPE_DOCUMENT_UNWATCHED = 2;
}

message DocEvent {
DocEventType type = 1;
bytes publisher = 2;
string publisher = 2;
}
76 changes: 52 additions & 24 deletions src/api/yorkie/v1/resources_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,9 @@ export namespace Operation {
hasTo(): boolean;
clearTo(): TreeEdit;

getCreatedAtMapByActorMap(): jspb.Map<string, TimeTicket>;
clearCreatedAtMapByActorMap(): TreeEdit;

getContentsList(): Array<TreeNodes>;
setContentsList(value: Array<TreeNodes>): TreeEdit;
clearContentsList(): TreeEdit;
Expand All @@ -570,6 +573,7 @@ export namespace Operation {
parentCreatedAt?: TimeTicket.AsObject,
from?: TreePos.AsObject,
to?: TreePos.AsObject,
createdAtMapByActorMap: Array<[string, TimeTicket.AsObject]>,
contentsList: Array<TreeNodes.AsObject>,
executedAt?: TimeTicket.AsObject,
}
Expand Down Expand Up @@ -1119,10 +1123,10 @@ export namespace TextNodeID {
}

export class TreeNode extends jspb.Message {
getPos(): TreePos | undefined;
setPos(value?: TreePos): TreeNode;
hasPos(): boolean;
clearPos(): TreeNode;
getId(): TreeNodeID | undefined;
setId(value?: TreeNodeID): TreeNode;
hasId(): boolean;
clearId(): TreeNode;

getType(): string;
setType(value: string): TreeNode;
Expand All @@ -1135,10 +1139,10 @@ export class TreeNode extends jspb.Message {
hasRemovedAt(): boolean;
clearRemovedAt(): TreeNode;

getInsPrevPos(): TreePos | undefined;
setInsPrevPos(value?: TreePos): TreeNode;
hasInsPrevPos(): boolean;
clearInsPrevPos(): TreeNode;
getInsPrevId(): TreeNodeID | undefined;
setInsPrevId(value?: TreeNodeID): TreeNode;
hasInsPrevId(): boolean;
clearInsPrevId(): TreeNode;

getDepth(): number;
setDepth(value: number): TreeNode;
Expand All @@ -1156,11 +1160,11 @@ export class TreeNode extends jspb.Message {

export namespace TreeNode {
export type AsObject = {
pos?: TreePos.AsObject,
id?: TreeNodeID.AsObject,
type: string,
value: string,
removedAt?: TimeTicket.AsObject,
insPrevPos?: TreePos.AsObject,
insPrevId?: TreeNodeID.AsObject,
depth: number,
attributesMap: Array<[string, NodeAttr.AsObject]>,
}
Expand All @@ -1186,14 +1190,40 @@ export namespace TreeNodes {
}
}

export class TreePos extends jspb.Message {
export class TreeNodeID extends jspb.Message {
getCreatedAt(): TimeTicket | undefined;
setCreatedAt(value?: TimeTicket): TreePos;
setCreatedAt(value?: TimeTicket): TreeNodeID;
hasCreatedAt(): boolean;
clearCreatedAt(): TreePos;
clearCreatedAt(): TreeNodeID;

getOffset(): number;
setOffset(value: number): TreePos;
setOffset(value: number): TreeNodeID;

serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): TreeNodeID.AsObject;
static toObject(includeInstance: boolean, msg: TreeNodeID): TreeNodeID.AsObject;
static serializeBinaryToWriter(message: TreeNodeID, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): TreeNodeID;
static deserializeBinaryFromReader(message: TreeNodeID, reader: jspb.BinaryReader): TreeNodeID;
}

export namespace TreeNodeID {
export type AsObject = {
createdAt?: TimeTicket.AsObject,
offset: number,
}
}

export class TreePos extends jspb.Message {
getParentId(): TreeNodeID | undefined;
setParentId(value?: TreeNodeID): TreePos;
hasParentId(): boolean;
clearParentId(): TreePos;

getLeftSiblingId(): TreeNodeID | undefined;
setLeftSiblingId(value?: TreeNodeID): TreePos;
hasLeftSiblingId(): boolean;
clearLeftSiblingId(): TreePos;

serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): TreePos.AsObject;
Expand All @@ -1205,8 +1235,8 @@ export class TreePos extends jspb.Message {

export namespace TreePos {
export type AsObject = {
createdAt?: TimeTicket.AsObject,
offset: number,
parentId?: TreeNodeID.AsObject,
leftSiblingId?: TreeNodeID.AsObject,
}
}

Expand Down Expand Up @@ -1528,10 +1558,8 @@ export class DocEvent extends jspb.Message {
getType(): DocEventType;
setType(value: DocEventType): DocEvent;

getPublisher(): Uint8Array | string;
getPublisher_asU8(): Uint8Array;
getPublisher_asB64(): string;
setPublisher(value: Uint8Array | string): DocEvent;
getPublisher(): string;
setPublisher(value: string): DocEvent;

serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): DocEvent.AsObject;
Expand All @@ -1544,7 +1572,7 @@ export class DocEvent extends jspb.Message {
export namespace DocEvent {
export type AsObject = {
type: DocEventType,
publisher: Uint8Array | string,
publisher: string,
}
}

Expand All @@ -1565,7 +1593,7 @@ export enum ValueType {
VALUE_TYPE_TREE = 13,
}
export enum DocEventType {
DOC_EVENT_TYPE_DOCUMENTS_CHANGED = 0,
DOC_EVENT_TYPE_DOCUMENTS_WATCHED = 1,
DOC_EVENT_TYPE_DOCUMENTS_UNWATCHED = 2,
DOC_EVENT_TYPE_DOCUMENT_CHANGED = 0,
DOC_EVENT_TYPE_DOCUMENT_WATCHED = 1,
DOC_EVENT_TYPE_DOCUMENT_UNWATCHED = 2,
}
Loading

0 comments on commit 990a6d6

Please sign in to comment.