diff --git a/public/util.js b/public/util.js index 0b196ffac..042a2e8d8 100644 --- a/public/util.js +++ b/public/util.js @@ -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); } diff --git a/src/api/converter.ts b/src/api/converter.ts index 8d8ff58ca..dbccacb64 100644 --- a/src/api/converter.ts +++ b/src/api/converter.ts @@ -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, @@ -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 { @@ -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'; @@ -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. */ @@ -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()), ); @@ -545,7 +562,7 @@ function toTreeNodes(node: CRDTTreeNode): Array { const pbTreeNodes: Array = []; 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); @@ -856,8 +873,6 @@ function fromElementSimple(pbElementSimple: PbJSONElementSimple): CRDTElement { fromTimeTicket(pbElementSimple.getCreatedAt())!, ); } - - throw new YorkieError(Code.Unimplemented, `unimplemented element`); } /** @@ -909,8 +924,18 @@ function fromTextNode(pbTextNode: PbTextNode): RGATreeSplitNode { */ 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(), ); } @@ -925,10 +950,8 @@ function fromTreeNodesWhenEdit( } const treeNodes: Array = []; - pbTreeNodes.forEach((node) => { const treeNode = fromTreeNodes(node.getContentList()); - treeNodes.push(treeNode!); }); @@ -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 { @@ -982,6 +1005,9 @@ function fromTreeNode(pbTreeNode: PbTreeNode): CRDTTreeNode { }); node.attrs = attrs; } + + node.removedAt = fromTimeTicket(pbTreeNode.getRemovedAt()); + return node; } @@ -1073,10 +1099,15 @@ function fromOperations(pbOperations: Array): Array { ); } 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())!, ); diff --git a/src/api/yorkie/v1/resources.proto b/src/api/yorkie/v1/resources.proto index 46d879aad..fd09dd601 100644 --- a/src/api/yorkie/v1/resources.proto +++ b/src/api/yorkie/v1/resources.proto @@ -95,6 +95,10 @@ message Operation { TimeTicket executed_at = 6; map 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; @@ -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 created_at_map_by_actor = 4; + repeated TreeNodes contents = 5; + TimeTicket executed_at = 6; } message TreeStyle { TimeTicket parent_created_at = 1; @@ -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 attributes = 7; } @@ -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 // ///////////////////////////////////////// @@ -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; } diff --git a/src/api/yorkie/v1/resources_pb.d.ts b/src/api/yorkie/v1/resources_pb.d.ts index 1e9de4055..13ebf3f42 100644 --- a/src/api/yorkie/v1/resources_pb.d.ts +++ b/src/api/yorkie/v1/resources_pb.d.ts @@ -547,6 +547,9 @@ export namespace Operation { hasTo(): boolean; clearTo(): TreeEdit; + getCreatedAtMapByActorMap(): jspb.Map; + clearCreatedAtMapByActorMap(): TreeEdit; + getContentsList(): Array; setContentsList(value: Array): TreeEdit; clearContentsList(): TreeEdit; @@ -570,6 +573,7 @@ export namespace Operation { parentCreatedAt?: TimeTicket.AsObject, from?: TreePos.AsObject, to?: TreePos.AsObject, + createdAtMapByActorMap: Array<[string, TimeTicket.AsObject]>, contentsList: Array, executedAt?: TimeTicket.AsObject, } @@ -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; @@ -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; @@ -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]>, } @@ -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; @@ -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, } } @@ -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; @@ -1544,7 +1572,7 @@ export class DocEvent extends jspb.Message { export namespace DocEvent { export type AsObject = { type: DocEventType, - publisher: Uint8Array | string, + publisher: string, } } @@ -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, } diff --git a/src/api/yorkie/v1/resources_pb.js b/src/api/yorkie/v1/resources_pb.js index a7ab78451..86467b0bf 100644 --- a/src/api/yorkie/v1/resources_pb.js +++ b/src/api/yorkie/v1/resources_pb.js @@ -13,13 +13,7 @@ var jspb = require('google-protobuf'); var goog = jspb; -var global = - (typeof globalThis !== 'undefined' && globalThis) || - (typeof window !== 'undefined' && window) || - (typeof global !== 'undefined' && global) || - (typeof self !== 'undefined' && self) || - (function () { return this; }).call(null) || - Function('return this')(); +var global = (function() { return this || window || global || self || Function('return this')(); }).call(null); var google_protobuf_timestamp_pb = require('google-protobuf/google/protobuf/timestamp_pb.js'); goog.object.extend(proto, google_protobuf_timestamp_pb); @@ -66,6 +60,7 @@ goog.exportSymbol('proto.yorkie.v1.TextNodeID', null, global); goog.exportSymbol('proto.yorkie.v1.TextNodePos', null, global); goog.exportSymbol('proto.yorkie.v1.TimeTicket', null, global); goog.exportSymbol('proto.yorkie.v1.TreeNode', null, global); +goog.exportSymbol('proto.yorkie.v1.TreeNodeID', null, global); goog.exportSymbol('proto.yorkie.v1.TreeNodes', null, global); goog.exportSymbol('proto.yorkie.v1.TreePos', null, global); goog.exportSymbol('proto.yorkie.v1.UpdatableProjectFields', null, global); @@ -702,6 +697,27 @@ if (goog.DEBUG && !COMPILED) { */ proto.yorkie.v1.TreeNodes.displayName = 'proto.yorkie.v1.TreeNodes'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.yorkie.v1.TreeNodeID = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.yorkie.v1.TreeNodeID, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.yorkie.v1.TreeNodeID.displayName = 'proto.yorkie.v1.TreeNodeID'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -1135,8 +1151,7 @@ proto.yorkie.v1.Snapshot.prototype.getPresencesMap = function(opt_noLazyCreate) */ proto.yorkie.v1.Snapshot.prototype.clearPresencesMap = function() { this.getPresencesMap().clear(); - return this; -}; + return this;}; @@ -3789,8 +3804,7 @@ proto.yorkie.v1.Operation.Edit.prototype.getCreatedAtMapByActorMap = function(op */ proto.yorkie.v1.Operation.Edit.prototype.clearCreatedAtMapByActorMap = function() { this.getCreatedAtMapByActorMap().clear(); - return this; -}; + return this;}; /** @@ -3867,8 +3881,7 @@ proto.yorkie.v1.Operation.Edit.prototype.getAttributesMap = function(opt_noLazyC */ proto.yorkie.v1.Operation.Edit.prototype.clearAttributesMap = function() { this.getAttributesMap().clear(); - return this; -}; + return this;}; @@ -4472,8 +4485,7 @@ proto.yorkie.v1.Operation.Style.prototype.getAttributesMap = function(opt_noLazy */ proto.yorkie.v1.Operation.Style.prototype.clearAttributesMap = function() { this.getAttributesMap().clear(); - return this; -}; + return this;}; /** @@ -4772,7 +4784,7 @@ proto.yorkie.v1.Operation.Increase.prototype.hasExecutedAt = function() { * @private {!Array} * @const */ -proto.yorkie.v1.Operation.TreeEdit.repeatedFields_ = [4]; +proto.yorkie.v1.Operation.TreeEdit.repeatedFields_ = [5]; @@ -4808,6 +4820,7 @@ proto.yorkie.v1.Operation.TreeEdit.toObject = function(includeInstance, msg) { parentCreatedAt: (f = msg.getParentCreatedAt()) && proto.yorkie.v1.TimeTicket.toObject(includeInstance, f), from: (f = msg.getFrom()) && proto.yorkie.v1.TreePos.toObject(includeInstance, f), to: (f = msg.getTo()) && proto.yorkie.v1.TreePos.toObject(includeInstance, f), + createdAtMapByActorMap: (f = msg.getCreatedAtMapByActorMap()) ? f.toObject(includeInstance, proto.yorkie.v1.TimeTicket.toObject) : [], contentsList: jspb.Message.toObjectList(msg.getContentsList(), proto.yorkie.v1.TreeNodes.toObject, includeInstance), executedAt: (f = msg.getExecutedAt()) && proto.yorkie.v1.TimeTicket.toObject(includeInstance, f) @@ -4863,11 +4876,17 @@ proto.yorkie.v1.Operation.TreeEdit.deserializeBinaryFromReader = function(msg, r msg.setTo(value); break; case 4: + var value = msg.getCreatedAtMapByActorMap(); + reader.readMessage(value, function(message, reader) { + jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readMessage, proto.yorkie.v1.TimeTicket.deserializeBinaryFromReader, "", new proto.yorkie.v1.TimeTicket()); + }); + break; + case 5: var value = new proto.yorkie.v1.TreeNodes; reader.readMessage(value,proto.yorkie.v1.TreeNodes.deserializeBinaryFromReader); msg.addContents(value); break; - case 5: + case 6: var value = new proto.yorkie.v1.TimeTicket; reader.readMessage(value,proto.yorkie.v1.TimeTicket.deserializeBinaryFromReader); msg.setExecutedAt(value); @@ -4925,10 +4944,14 @@ proto.yorkie.v1.Operation.TreeEdit.serializeBinaryToWriter = function(message, w proto.yorkie.v1.TreePos.serializeBinaryToWriter ); } + f = message.getCreatedAtMapByActorMap(true); + if (f && f.getLength() > 0) { + f.serializeBinary(4, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeMessage, proto.yorkie.v1.TimeTicket.serializeBinaryToWriter); + } f = message.getContentsList(); if (f.length > 0) { writer.writeRepeatedMessage( - 4, + 5, f, proto.yorkie.v1.TreeNodes.serializeBinaryToWriter ); @@ -4936,7 +4959,7 @@ proto.yorkie.v1.Operation.TreeEdit.serializeBinaryToWriter = function(message, w f = message.getExecutedAt(); if (f != null) { writer.writeMessage( - 5, + 6, f, proto.yorkie.v1.TimeTicket.serializeBinaryToWriter ); @@ -5056,12 +5079,34 @@ proto.yorkie.v1.Operation.TreeEdit.prototype.hasTo = function() { /** - * repeated TreeNodes contents = 4; + * map created_at_map_by_actor = 4; + * @param {boolean=} opt_noLazyCreate Do not create the map if + * empty, instead returning `undefined` + * @return {!jspb.Map} + */ +proto.yorkie.v1.Operation.TreeEdit.prototype.getCreatedAtMapByActorMap = function(opt_noLazyCreate) { + return /** @type {!jspb.Map} */ ( + jspb.Message.getMapField(this, 4, opt_noLazyCreate, + proto.yorkie.v1.TimeTicket)); +}; + + +/** + * Clears values from the map. The map will be non-null. + * @return {!proto.yorkie.v1.Operation.TreeEdit} returns this + */ +proto.yorkie.v1.Operation.TreeEdit.prototype.clearCreatedAtMapByActorMap = function() { + this.getCreatedAtMapByActorMap().clear(); + return this;}; + + +/** + * repeated TreeNodes contents = 5; * @return {!Array} */ proto.yorkie.v1.Operation.TreeEdit.prototype.getContentsList = function() { return /** @type{!Array} */ ( - jspb.Message.getRepeatedWrapperField(this, proto.yorkie.v1.TreeNodes, 4)); + jspb.Message.getRepeatedWrapperField(this, proto.yorkie.v1.TreeNodes, 5)); }; @@ -5070,7 +5115,7 @@ proto.yorkie.v1.Operation.TreeEdit.prototype.getContentsList = function() { * @return {!proto.yorkie.v1.Operation.TreeEdit} returns this */ proto.yorkie.v1.Operation.TreeEdit.prototype.setContentsList = function(value) { - return jspb.Message.setRepeatedWrapperField(this, 4, value); + return jspb.Message.setRepeatedWrapperField(this, 5, value); }; @@ -5080,7 +5125,7 @@ proto.yorkie.v1.Operation.TreeEdit.prototype.setContentsList = function(value) { * @return {!proto.yorkie.v1.TreeNodes} */ proto.yorkie.v1.Operation.TreeEdit.prototype.addContents = function(opt_value, opt_index) { - return jspb.Message.addToRepeatedWrapperField(this, 4, opt_value, proto.yorkie.v1.TreeNodes, opt_index); + return jspb.Message.addToRepeatedWrapperField(this, 5, opt_value, proto.yorkie.v1.TreeNodes, opt_index); }; @@ -5094,12 +5139,12 @@ proto.yorkie.v1.Operation.TreeEdit.prototype.clearContentsList = function() { /** - * optional TimeTicket executed_at = 5; + * optional TimeTicket executed_at = 6; * @return {?proto.yorkie.v1.TimeTicket} */ proto.yorkie.v1.Operation.TreeEdit.prototype.getExecutedAt = function() { return /** @type{?proto.yorkie.v1.TimeTicket} */ ( - jspb.Message.getWrapperField(this, proto.yorkie.v1.TimeTicket, 5)); + jspb.Message.getWrapperField(this, proto.yorkie.v1.TimeTicket, 6)); }; @@ -5108,7 +5153,7 @@ proto.yorkie.v1.Operation.TreeEdit.prototype.getExecutedAt = function() { * @return {!proto.yorkie.v1.Operation.TreeEdit} returns this */ proto.yorkie.v1.Operation.TreeEdit.prototype.setExecutedAt = function(value) { - return jspb.Message.setWrapperField(this, 5, value); + return jspb.Message.setWrapperField(this, 6, value); }; @@ -5126,7 +5171,7 @@ proto.yorkie.v1.Operation.TreeEdit.prototype.clearExecutedAt = function() { * @return {boolean} */ proto.yorkie.v1.Operation.TreeEdit.prototype.hasExecutedAt = function() { - return jspb.Message.getField(this, 5) != null; + return jspb.Message.getField(this, 6) != null; }; @@ -5427,8 +5472,7 @@ proto.yorkie.v1.Operation.TreeStyle.prototype.getAttributesMap = function(opt_no */ proto.yorkie.v1.Operation.TreeStyle.prototype.clearAttributesMap = function() { this.getAttributesMap().clear(); - return this; -}; + return this;}; /** @@ -9414,8 +9458,7 @@ proto.yorkie.v1.TextNode.prototype.getAttributesMap = function(opt_noLazyCreate) */ proto.yorkie.v1.TextNode.prototype.clearAttributesMap = function() { this.getAttributesMap().clear(); - return this; -}; + return this;}; @@ -9631,11 +9674,11 @@ proto.yorkie.v1.TreeNode.prototype.toObject = function(opt_includeInstance) { */ proto.yorkie.v1.TreeNode.toObject = function(includeInstance, msg) { var f, obj = { - pos: (f = msg.getPos()) && proto.yorkie.v1.TreePos.toObject(includeInstance, f), + id: (f = msg.getId()) && proto.yorkie.v1.TreeNodeID.toObject(includeInstance, f), type: jspb.Message.getFieldWithDefault(msg, 2, ""), value: jspb.Message.getFieldWithDefault(msg, 3, ""), removedAt: (f = msg.getRemovedAt()) && proto.yorkie.v1.TimeTicket.toObject(includeInstance, f), - insPrevPos: (f = msg.getInsPrevPos()) && proto.yorkie.v1.TreePos.toObject(includeInstance, f), + insPrevId: (f = msg.getInsPrevId()) && proto.yorkie.v1.TreeNodeID.toObject(includeInstance, f), depth: jspb.Message.getFieldWithDefault(msg, 6, 0), attributesMap: (f = msg.getAttributesMap()) ? f.toObject(includeInstance, proto.yorkie.v1.NodeAttr.toObject) : [] }; @@ -9675,9 +9718,9 @@ proto.yorkie.v1.TreeNode.deserializeBinaryFromReader = function(msg, reader) { var field = reader.getFieldNumber(); switch (field) { case 1: - var value = new proto.yorkie.v1.TreePos; - reader.readMessage(value,proto.yorkie.v1.TreePos.deserializeBinaryFromReader); - msg.setPos(value); + var value = new proto.yorkie.v1.TreeNodeID; + reader.readMessage(value,proto.yorkie.v1.TreeNodeID.deserializeBinaryFromReader); + msg.setId(value); break; case 2: var value = /** @type {string} */ (reader.readString()); @@ -9693,9 +9736,9 @@ proto.yorkie.v1.TreeNode.deserializeBinaryFromReader = function(msg, reader) { msg.setRemovedAt(value); break; case 5: - var value = new proto.yorkie.v1.TreePos; - reader.readMessage(value,proto.yorkie.v1.TreePos.deserializeBinaryFromReader); - msg.setInsPrevPos(value); + var value = new proto.yorkie.v1.TreeNodeID; + reader.readMessage(value,proto.yorkie.v1.TreeNodeID.deserializeBinaryFromReader); + msg.setInsPrevId(value); break; case 6: var value = /** @type {number} */ (reader.readInt32()); @@ -9736,12 +9779,12 @@ proto.yorkie.v1.TreeNode.prototype.serializeBinary = function() { */ proto.yorkie.v1.TreeNode.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getPos(); + f = message.getId(); if (f != null) { writer.writeMessage( 1, f, - proto.yorkie.v1.TreePos.serializeBinaryToWriter + proto.yorkie.v1.TreeNodeID.serializeBinaryToWriter ); } f = message.getType(); @@ -9766,12 +9809,12 @@ proto.yorkie.v1.TreeNode.serializeBinaryToWriter = function(message, writer) { proto.yorkie.v1.TimeTicket.serializeBinaryToWriter ); } - f = message.getInsPrevPos(); + f = message.getInsPrevId(); if (f != null) { writer.writeMessage( 5, f, - proto.yorkie.v1.TreePos.serializeBinaryToWriter + proto.yorkie.v1.TreeNodeID.serializeBinaryToWriter ); } f = message.getDepth(); @@ -9789,20 +9832,20 @@ proto.yorkie.v1.TreeNode.serializeBinaryToWriter = function(message, writer) { /** - * optional TreePos pos = 1; - * @return {?proto.yorkie.v1.TreePos} + * optional TreeNodeID id = 1; + * @return {?proto.yorkie.v1.TreeNodeID} */ -proto.yorkie.v1.TreeNode.prototype.getPos = function() { - return /** @type{?proto.yorkie.v1.TreePos} */ ( - jspb.Message.getWrapperField(this, proto.yorkie.v1.TreePos, 1)); +proto.yorkie.v1.TreeNode.prototype.getId = function() { + return /** @type{?proto.yorkie.v1.TreeNodeID} */ ( + jspb.Message.getWrapperField(this, proto.yorkie.v1.TreeNodeID, 1)); }; /** - * @param {?proto.yorkie.v1.TreePos|undefined} value + * @param {?proto.yorkie.v1.TreeNodeID|undefined} value * @return {!proto.yorkie.v1.TreeNode} returns this */ -proto.yorkie.v1.TreeNode.prototype.setPos = function(value) { +proto.yorkie.v1.TreeNode.prototype.setId = function(value) { return jspb.Message.setWrapperField(this, 1, value); }; @@ -9811,8 +9854,8 @@ proto.yorkie.v1.TreeNode.prototype.setPos = function(value) { * Clears the message field making it undefined. * @return {!proto.yorkie.v1.TreeNode} returns this */ -proto.yorkie.v1.TreeNode.prototype.clearPos = function() { - return this.setPos(undefined); +proto.yorkie.v1.TreeNode.prototype.clearId = function() { + return this.setId(undefined); }; @@ -9820,7 +9863,7 @@ proto.yorkie.v1.TreeNode.prototype.clearPos = function() { * Returns whether this field is set. * @return {boolean} */ -proto.yorkie.v1.TreeNode.prototype.hasPos = function() { +proto.yorkie.v1.TreeNode.prototype.hasId = function() { return jspb.Message.getField(this, 1) != null; }; @@ -9899,20 +9942,20 @@ proto.yorkie.v1.TreeNode.prototype.hasRemovedAt = function() { /** - * optional TreePos ins_prev_pos = 5; - * @return {?proto.yorkie.v1.TreePos} + * optional TreeNodeID ins_prev_id = 5; + * @return {?proto.yorkie.v1.TreeNodeID} */ -proto.yorkie.v1.TreeNode.prototype.getInsPrevPos = function() { - return /** @type{?proto.yorkie.v1.TreePos} */ ( - jspb.Message.getWrapperField(this, proto.yorkie.v1.TreePos, 5)); +proto.yorkie.v1.TreeNode.prototype.getInsPrevId = function() { + return /** @type{?proto.yorkie.v1.TreeNodeID} */ ( + jspb.Message.getWrapperField(this, proto.yorkie.v1.TreeNodeID, 5)); }; /** - * @param {?proto.yorkie.v1.TreePos|undefined} value + * @param {?proto.yorkie.v1.TreeNodeID|undefined} value * @return {!proto.yorkie.v1.TreeNode} returns this */ -proto.yorkie.v1.TreeNode.prototype.setInsPrevPos = function(value) { +proto.yorkie.v1.TreeNode.prototype.setInsPrevId = function(value) { return jspb.Message.setWrapperField(this, 5, value); }; @@ -9921,8 +9964,8 @@ proto.yorkie.v1.TreeNode.prototype.setInsPrevPos = function(value) { * Clears the message field making it undefined. * @return {!proto.yorkie.v1.TreeNode} returns this */ -proto.yorkie.v1.TreeNode.prototype.clearInsPrevPos = function() { - return this.setInsPrevPos(undefined); +proto.yorkie.v1.TreeNode.prototype.clearInsPrevId = function() { + return this.setInsPrevId(undefined); }; @@ -9930,7 +9973,7 @@ proto.yorkie.v1.TreeNode.prototype.clearInsPrevPos = function() { * Returns whether this field is set. * @return {boolean} */ -proto.yorkie.v1.TreeNode.prototype.hasInsPrevPos = function() { +proto.yorkie.v1.TreeNode.prototype.hasInsPrevId = function() { return jspb.Message.getField(this, 5) != null; }; @@ -9972,8 +10015,7 @@ proto.yorkie.v1.TreeNode.prototype.getAttributesMap = function(opt_noLazyCreate) */ proto.yorkie.v1.TreeNode.prototype.clearAttributesMap = function() { this.getAttributesMap().clear(); - return this; -}; + return this;}; @@ -10152,8 +10194,8 @@ if (jspb.Message.GENERATE_TO_OBJECT) { * http://goto/soy-param-migration * @return {!Object} */ -proto.yorkie.v1.TreePos.prototype.toObject = function(opt_includeInstance) { - return proto.yorkie.v1.TreePos.toObject(opt_includeInstance, this); +proto.yorkie.v1.TreeNodeID.prototype.toObject = function(opt_includeInstance) { + return proto.yorkie.v1.TreeNodeID.toObject(opt_includeInstance, this); }; @@ -10162,11 +10204,11 @@ proto.yorkie.v1.TreePos.prototype.toObject = function(opt_includeInstance) { * @param {boolean|undefined} includeInstance Deprecated. Whether to include * the JSPB instance for transitional soy proto support: * http://goto/soy-param-migration - * @param {!proto.yorkie.v1.TreePos} msg The msg instance to transform. + * @param {!proto.yorkie.v1.TreeNodeID} msg The msg instance to transform. * @return {!Object} * @suppress {unusedLocalVariables} f is only used for nested messages */ -proto.yorkie.v1.TreePos.toObject = function(includeInstance, msg) { +proto.yorkie.v1.TreeNodeID.toObject = function(includeInstance, msg) { var f, obj = { createdAt: (f = msg.getCreatedAt()) && proto.yorkie.v1.TimeTicket.toObject(includeInstance, f), offset: jspb.Message.getFieldWithDefault(msg, 2, 0) @@ -10183,23 +10225,23 @@ proto.yorkie.v1.TreePos.toObject = function(includeInstance, msg) { /** * Deserializes binary data (in protobuf wire format). * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.yorkie.v1.TreePos} + * @return {!proto.yorkie.v1.TreeNodeID} */ -proto.yorkie.v1.TreePos.deserializeBinary = function(bytes) { +proto.yorkie.v1.TreeNodeID.deserializeBinary = function(bytes) { var reader = new jspb.BinaryReader(bytes); - var msg = new proto.yorkie.v1.TreePos; - return proto.yorkie.v1.TreePos.deserializeBinaryFromReader(msg, reader); + var msg = new proto.yorkie.v1.TreeNodeID; + return proto.yorkie.v1.TreeNodeID.deserializeBinaryFromReader(msg, reader); }; /** * Deserializes binary data (in protobuf wire format) from the * given reader into the given message object. - * @param {!proto.yorkie.v1.TreePos} msg The message object to deserialize into. + * @param {!proto.yorkie.v1.TreeNodeID} msg The message object to deserialize into. * @param {!jspb.BinaryReader} reader The BinaryReader to use. - * @return {!proto.yorkie.v1.TreePos} + * @return {!proto.yorkie.v1.TreeNodeID} */ -proto.yorkie.v1.TreePos.deserializeBinaryFromReader = function(msg, reader) { +proto.yorkie.v1.TreeNodeID.deserializeBinaryFromReader = function(msg, reader) { while (reader.nextField()) { if (reader.isEndGroup()) { break; @@ -10228,9 +10270,9 @@ proto.yorkie.v1.TreePos.deserializeBinaryFromReader = function(msg, reader) { * Serializes the message to binary data (in protobuf wire format). * @return {!Uint8Array} */ -proto.yorkie.v1.TreePos.prototype.serializeBinary = function() { +proto.yorkie.v1.TreeNodeID.prototype.serializeBinary = function() { var writer = new jspb.BinaryWriter(); - proto.yorkie.v1.TreePos.serializeBinaryToWriter(this, writer); + proto.yorkie.v1.TreeNodeID.serializeBinaryToWriter(this, writer); return writer.getResultBuffer(); }; @@ -10238,11 +10280,11 @@ proto.yorkie.v1.TreePos.prototype.serializeBinary = function() { /** * Serializes the given message to binary data (in protobuf wire * format), writing to the given BinaryWriter. - * @param {!proto.yorkie.v1.TreePos} message + * @param {!proto.yorkie.v1.TreeNodeID} message * @param {!jspb.BinaryWriter} writer * @suppress {unusedLocalVariables} f is only used for nested messages */ -proto.yorkie.v1.TreePos.serializeBinaryToWriter = function(message, writer) { +proto.yorkie.v1.TreeNodeID.serializeBinaryToWriter = function(message, writer) { var f = undefined; f = message.getCreatedAt(); if (f != null) { @@ -10266,7 +10308,7 @@ proto.yorkie.v1.TreePos.serializeBinaryToWriter = function(message, writer) { * optional TimeTicket created_at = 1; * @return {?proto.yorkie.v1.TimeTicket} */ -proto.yorkie.v1.TreePos.prototype.getCreatedAt = function() { +proto.yorkie.v1.TreeNodeID.prototype.getCreatedAt = function() { return /** @type{?proto.yorkie.v1.TimeTicket} */ ( jspb.Message.getWrapperField(this, proto.yorkie.v1.TimeTicket, 1)); }; @@ -10274,18 +10316,18 @@ proto.yorkie.v1.TreePos.prototype.getCreatedAt = function() { /** * @param {?proto.yorkie.v1.TimeTicket|undefined} value - * @return {!proto.yorkie.v1.TreePos} returns this + * @return {!proto.yorkie.v1.TreeNodeID} returns this */ -proto.yorkie.v1.TreePos.prototype.setCreatedAt = function(value) { +proto.yorkie.v1.TreeNodeID.prototype.setCreatedAt = function(value) { return jspb.Message.setWrapperField(this, 1, value); }; /** * Clears the message field making it undefined. - * @return {!proto.yorkie.v1.TreePos} returns this + * @return {!proto.yorkie.v1.TreeNodeID} returns this */ -proto.yorkie.v1.TreePos.prototype.clearCreatedAt = function() { +proto.yorkie.v1.TreeNodeID.prototype.clearCreatedAt = function() { return this.setCreatedAt(undefined); }; @@ -10294,7 +10336,7 @@ proto.yorkie.v1.TreePos.prototype.clearCreatedAt = function() { * Returns whether this field is set. * @return {boolean} */ -proto.yorkie.v1.TreePos.prototype.hasCreatedAt = function() { +proto.yorkie.v1.TreeNodeID.prototype.hasCreatedAt = function() { return jspb.Message.getField(this, 1) != null; }; @@ -10303,16 +10345,16 @@ proto.yorkie.v1.TreePos.prototype.hasCreatedAt = function() { * optional int32 offset = 2; * @return {number} */ -proto.yorkie.v1.TreePos.prototype.getOffset = function() { +proto.yorkie.v1.TreeNodeID.prototype.getOffset = function() { return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); }; /** * @param {number} value - * @return {!proto.yorkie.v1.TreePos} returns this + * @return {!proto.yorkie.v1.TreeNodeID} returns this */ -proto.yorkie.v1.TreePos.prototype.setOffset = function(value) { +proto.yorkie.v1.TreeNodeID.prototype.setOffset = function(value) { return jspb.Message.setProto3IntField(this, 2, value); }; @@ -10320,6 +10362,208 @@ proto.yorkie.v1.TreePos.prototype.setOffset = function(value) { +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.yorkie.v1.TreePos.prototype.toObject = function(opt_includeInstance) { + return proto.yorkie.v1.TreePos.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.yorkie.v1.TreePos} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.yorkie.v1.TreePos.toObject = function(includeInstance, msg) { + var f, obj = { + parentId: (f = msg.getParentId()) && proto.yorkie.v1.TreeNodeID.toObject(includeInstance, f), + leftSiblingId: (f = msg.getLeftSiblingId()) && proto.yorkie.v1.TreeNodeID.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.yorkie.v1.TreePos} + */ +proto.yorkie.v1.TreePos.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.yorkie.v1.TreePos; + return proto.yorkie.v1.TreePos.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.yorkie.v1.TreePos} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.yorkie.v1.TreePos} + */ +proto.yorkie.v1.TreePos.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.yorkie.v1.TreeNodeID; + reader.readMessage(value,proto.yorkie.v1.TreeNodeID.deserializeBinaryFromReader); + msg.setParentId(value); + break; + case 2: + var value = new proto.yorkie.v1.TreeNodeID; + reader.readMessage(value,proto.yorkie.v1.TreeNodeID.deserializeBinaryFromReader); + msg.setLeftSiblingId(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.yorkie.v1.TreePos.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.yorkie.v1.TreePos.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.yorkie.v1.TreePos} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.yorkie.v1.TreePos.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getParentId(); + if (f != null) { + writer.writeMessage( + 1, + f, + proto.yorkie.v1.TreeNodeID.serializeBinaryToWriter + ); + } + f = message.getLeftSiblingId(); + if (f != null) { + writer.writeMessage( + 2, + f, + proto.yorkie.v1.TreeNodeID.serializeBinaryToWriter + ); + } +}; + + +/** + * optional TreeNodeID parent_id = 1; + * @return {?proto.yorkie.v1.TreeNodeID} + */ +proto.yorkie.v1.TreePos.prototype.getParentId = function() { + return /** @type{?proto.yorkie.v1.TreeNodeID} */ ( + jspb.Message.getWrapperField(this, proto.yorkie.v1.TreeNodeID, 1)); +}; + + +/** + * @param {?proto.yorkie.v1.TreeNodeID|undefined} value + * @return {!proto.yorkie.v1.TreePos} returns this +*/ +proto.yorkie.v1.TreePos.prototype.setParentId = function(value) { + return jspb.Message.setWrapperField(this, 1, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.yorkie.v1.TreePos} returns this + */ +proto.yorkie.v1.TreePos.prototype.clearParentId = function() { + return this.setParentId(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.yorkie.v1.TreePos.prototype.hasParentId = function() { + return jspb.Message.getField(this, 1) != null; +}; + + +/** + * optional TreeNodeID left_sibling_id = 2; + * @return {?proto.yorkie.v1.TreeNodeID} + */ +proto.yorkie.v1.TreePos.prototype.getLeftSiblingId = function() { + return /** @type{?proto.yorkie.v1.TreeNodeID} */ ( + jspb.Message.getWrapperField(this, proto.yorkie.v1.TreeNodeID, 2)); +}; + + +/** + * @param {?proto.yorkie.v1.TreeNodeID|undefined} value + * @return {!proto.yorkie.v1.TreePos} returns this +*/ +proto.yorkie.v1.TreePos.prototype.setLeftSiblingId = function(value) { + return jspb.Message.setWrapperField(this, 2, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.yorkie.v1.TreePos} returns this + */ +proto.yorkie.v1.TreePos.prototype.clearLeftSiblingId = function() { + return this.setLeftSiblingId(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.yorkie.v1.TreePos.prototype.hasLeftSiblingId = function() { + return jspb.Message.getField(this, 2) != null; +}; + + + + + if (jspb.Message.GENERATE_TO_OBJECT) { /** * Creates an object representation of this proto. @@ -12090,8 +12334,7 @@ proto.yorkie.v1.Presence.prototype.getDataMap = function(opt_noLazyCreate) { */ proto.yorkie.v1.Presence.prototype.clearDataMap = function() { this.getDataMap().clear(); - return this; -}; + return this;}; @@ -12712,7 +12955,7 @@ proto.yorkie.v1.DocEvent.prototype.toObject = function(opt_includeInstance) { proto.yorkie.v1.DocEvent.toObject = function(includeInstance, msg) { var f, obj = { type: jspb.Message.getFieldWithDefault(msg, 1, 0), - publisher: msg.getPublisher_asB64() + publisher: jspb.Message.getFieldWithDefault(msg, 2, "") }; if (includeInstance) { @@ -12754,7 +12997,7 @@ proto.yorkie.v1.DocEvent.deserializeBinaryFromReader = function(msg, reader) { msg.setType(value); break; case 2: - var value = /** @type {!Uint8Array} */ (reader.readBytes()); + var value = /** @type {string} */ (reader.readString()); msg.setPublisher(value); break; default: @@ -12793,9 +13036,9 @@ proto.yorkie.v1.DocEvent.serializeBinaryToWriter = function(message, writer) { f ); } - f = message.getPublisher_asU8(); + f = message.getPublisher(); if (f.length > 0) { - writer.writeBytes( + writer.writeString( 2, f ); @@ -12822,7 +13065,7 @@ proto.yorkie.v1.DocEvent.prototype.setType = function(value) { /** - * optional bytes publisher = 2; + * optional string publisher = 2; * @return {string} */ proto.yorkie.v1.DocEvent.prototype.getPublisher = function() { @@ -12831,35 +13074,11 @@ proto.yorkie.v1.DocEvent.prototype.getPublisher = function() { /** - * optional bytes publisher = 2; - * This is a type-conversion wrapper around `getPublisher()` - * @return {string} - */ -proto.yorkie.v1.DocEvent.prototype.getPublisher_asB64 = function() { - return /** @type {string} */ (jspb.Message.bytesAsB64( - this.getPublisher())); -}; - - -/** - * optional bytes publisher = 2; - * Note that Uint8Array is not supported on all browsers. - * @see http://caniuse.com/Uint8Array - * This is a type-conversion wrapper around `getPublisher()` - * @return {!Uint8Array} - */ -proto.yorkie.v1.DocEvent.prototype.getPublisher_asU8 = function() { - return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( - this.getPublisher())); -}; - - -/** - * @param {!(string|Uint8Array)} value + * @param {string} value * @return {!proto.yorkie.v1.DocEvent} returns this */ proto.yorkie.v1.DocEvent.prototype.setPublisher = function(value) { - return jspb.Message.setProto3BytesField(this, 2, value); + return jspb.Message.setProto3StringField(this, 2, value); }; @@ -12887,9 +13106,9 @@ proto.yorkie.v1.ValueType = { * @enum {number} */ proto.yorkie.v1.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 }; goog.object.extend(exports, proto.yorkie.v1); diff --git a/src/api/yorkie/v1/yorkie.proto b/src/api/yorkie/v1/yorkie.proto index b4f0eda6a..639ddd37d 100644 --- a/src/api/yorkie/v1/yorkie.proto +++ b/src/api/yorkie/v1/yorkie.proto @@ -42,49 +42,45 @@ message ActivateClientRequest { } message ActivateClientResponse { - string client_key = 1; - bytes client_id = 2; + string client_id = 1; } message DeactivateClientRequest { - bytes client_id = 1; + string client_id = 1; } message DeactivateClientResponse { - bytes client_id = 1; } message AttachDocumentRequest { - bytes client_id = 1; + string client_id = 1; ChangePack change_pack = 2; } message AttachDocumentResponse { - bytes client_id = 1; - string document_id = 2; - ChangePack change_pack = 3; + string document_id = 1; + ChangePack change_pack = 2; } message DetachDocumentRequest { - bytes client_id = 1; + string client_id = 1; string document_id = 2; ChangePack change_pack = 3; bool remove_if_not_attached = 4; } message DetachDocumentResponse { - string client_key = 1; ChangePack change_pack = 2; } message WatchDocumentRequest { - bytes client_id = 1; + string client_id = 1; string document_id = 2; } message WatchDocumentResponse { message Initialization { - repeated bytes client_ids = 1; + repeated string client_ids = 1; } oneof body { @@ -94,24 +90,22 @@ message WatchDocumentResponse { } message RemoveDocumentRequest { - bytes client_id = 1; + string client_id = 1; string document_id = 2; ChangePack change_pack = 3; } message RemoveDocumentResponse { - string client_key = 1; - ChangePack change_pack = 2; + ChangePack change_pack = 1; } message PushPullChangesRequest { - bytes client_id = 1; + string client_id = 1; string document_id = 2; ChangePack change_pack = 3; bool push_only = 4; } message PushPullChangesResponse { - bytes client_id = 1; - ChangePack change_pack = 2; + ChangePack change_pack = 1; } diff --git a/src/api/yorkie/v1/yorkie_grpc_web_pb.js b/src/api/yorkie/v1/yorkie_grpc_web_pb.js index a8e2e7513..2eef83bd0 100644 --- a/src/api/yorkie/v1/yorkie_grpc_web_pb.js +++ b/src/api/yorkie/v1/yorkie_grpc_web_pb.js @@ -4,11 +4,7 @@ * @public */ -// Code generated by protoc-gen-grpc-web. DO NOT EDIT. -// versions: -// protoc-gen-grpc-web v1.4.2 -// protoc v4.23.2 -// source: yorkie/v1/yorkie.proto +// GENERATED CODE -- DO NOT EDIT! /* eslint-disable */ @@ -46,7 +42,7 @@ proto.yorkie.v1.YorkieServiceClient = /** * @private @const {string} The hostname */ - this.hostname_ = hostname.replace(/\/+$/, ''); + this.hostname_ = hostname; }; @@ -72,7 +68,7 @@ proto.yorkie.v1.YorkieServicePromiseClient = /** * @private @const {string} The hostname */ - this.hostname_ = hostname.replace(/\/+$/, ''); + this.hostname_ = hostname; }; diff --git a/src/api/yorkie/v1/yorkie_pb.d.ts b/src/api/yorkie/v1/yorkie_pb.d.ts index a7b572fa1..7038d71de 100644 --- a/src/api/yorkie/v1/yorkie_pb.d.ts +++ b/src/api/yorkie/v1/yorkie_pb.d.ts @@ -22,13 +22,8 @@ export namespace ActivateClientRequest { } export class ActivateClientResponse extends jspb.Message { - getClientKey(): string; - setClientKey(value: string): ActivateClientResponse; - - getClientId(): Uint8Array | string; - getClientId_asU8(): Uint8Array; - getClientId_asB64(): string; - setClientId(value: Uint8Array | string): ActivateClientResponse; + getClientId(): string; + setClientId(value: string): ActivateClientResponse; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ActivateClientResponse.AsObject; @@ -40,16 +35,13 @@ export class ActivateClientResponse extends jspb.Message { export namespace ActivateClientResponse { export type AsObject = { - clientKey: string, - clientId: Uint8Array | string, + clientId: string, } } export class DeactivateClientRequest extends jspb.Message { - getClientId(): Uint8Array | string; - getClientId_asU8(): Uint8Array; - getClientId_asB64(): string; - setClientId(value: Uint8Array | string): DeactivateClientRequest; + getClientId(): string; + setClientId(value: string): DeactivateClientRequest; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): DeactivateClientRequest.AsObject; @@ -61,16 +53,11 @@ export class DeactivateClientRequest extends jspb.Message { export namespace DeactivateClientRequest { export type AsObject = { - clientId: Uint8Array | string, + clientId: string, } } export class DeactivateClientResponse extends jspb.Message { - getClientId(): Uint8Array | string; - getClientId_asU8(): Uint8Array; - getClientId_asB64(): string; - setClientId(value: Uint8Array | string): DeactivateClientResponse; - serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): DeactivateClientResponse.AsObject; static toObject(includeInstance: boolean, msg: DeactivateClientResponse): DeactivateClientResponse.AsObject; @@ -81,15 +68,12 @@ export class DeactivateClientResponse extends jspb.Message { export namespace DeactivateClientResponse { export type AsObject = { - clientId: Uint8Array | string, } } export class AttachDocumentRequest extends jspb.Message { - getClientId(): Uint8Array | string; - getClientId_asU8(): Uint8Array; - getClientId_asB64(): string; - setClientId(value: Uint8Array | string): AttachDocumentRequest; + getClientId(): string; + setClientId(value: string): AttachDocumentRequest; getChangePack(): yorkie_v1_resources_pb.ChangePack | undefined; setChangePack(value?: yorkie_v1_resources_pb.ChangePack): AttachDocumentRequest; @@ -106,17 +90,12 @@ export class AttachDocumentRequest extends jspb.Message { export namespace AttachDocumentRequest { export type AsObject = { - clientId: Uint8Array | string, + clientId: string, changePack?: yorkie_v1_resources_pb.ChangePack.AsObject, } } export class AttachDocumentResponse extends jspb.Message { - getClientId(): Uint8Array | string; - getClientId_asU8(): Uint8Array; - getClientId_asB64(): string; - setClientId(value: Uint8Array | string): AttachDocumentResponse; - getDocumentId(): string; setDocumentId(value: string): AttachDocumentResponse; @@ -135,17 +114,14 @@ export class AttachDocumentResponse extends jspb.Message { export namespace AttachDocumentResponse { export type AsObject = { - clientId: Uint8Array | string, documentId: string, changePack?: yorkie_v1_resources_pb.ChangePack.AsObject, } } export class DetachDocumentRequest extends jspb.Message { - getClientId(): Uint8Array | string; - getClientId_asU8(): Uint8Array; - getClientId_asB64(): string; - setClientId(value: Uint8Array | string): DetachDocumentRequest; + getClientId(): string; + setClientId(value: string): DetachDocumentRequest; getDocumentId(): string; setDocumentId(value: string): DetachDocumentRequest; @@ -168,7 +144,7 @@ export class DetachDocumentRequest extends jspb.Message { export namespace DetachDocumentRequest { export type AsObject = { - clientId: Uint8Array | string, + clientId: string, documentId: string, changePack?: yorkie_v1_resources_pb.ChangePack.AsObject, removeIfNotAttached: boolean, @@ -176,9 +152,6 @@ export namespace DetachDocumentRequest { } export class DetachDocumentResponse extends jspb.Message { - getClientKey(): string; - setClientKey(value: string): DetachDocumentResponse; - getChangePack(): yorkie_v1_resources_pb.ChangePack | undefined; setChangePack(value?: yorkie_v1_resources_pb.ChangePack): DetachDocumentResponse; hasChangePack(): boolean; @@ -194,16 +167,13 @@ export class DetachDocumentResponse extends jspb.Message { export namespace DetachDocumentResponse { export type AsObject = { - clientKey: string, changePack?: yorkie_v1_resources_pb.ChangePack.AsObject, } } export class WatchDocumentRequest extends jspb.Message { - getClientId(): Uint8Array | string; - getClientId_asU8(): Uint8Array; - getClientId_asB64(): string; - setClientId(value: Uint8Array | string): WatchDocumentRequest; + getClientId(): string; + setClientId(value: string): WatchDocumentRequest; getDocumentId(): string; setDocumentId(value: string): WatchDocumentRequest; @@ -218,7 +188,7 @@ export class WatchDocumentRequest extends jspb.Message { export namespace WatchDocumentRequest { export type AsObject = { - clientId: Uint8Array | string, + clientId: string, documentId: string, } } @@ -251,10 +221,10 @@ export namespace WatchDocumentResponse { } export class Initialization extends jspb.Message { - getClientIdsList(): Array; - setClientIdsList(value: Array): Initialization; + getClientIdsList(): Array; + setClientIdsList(value: Array): Initialization; clearClientIdsList(): Initialization; - addClientIds(value: Uint8Array | string, index?: number): Initialization; + addClientIds(value: string, index?: number): Initialization; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): Initialization.AsObject; @@ -266,7 +236,7 @@ export namespace WatchDocumentResponse { export namespace Initialization { export type AsObject = { - clientIdsList: Array, + clientIdsList: Array, } } @@ -279,10 +249,8 @@ export namespace WatchDocumentResponse { } export class RemoveDocumentRequest extends jspb.Message { - getClientId(): Uint8Array | string; - getClientId_asU8(): Uint8Array; - getClientId_asB64(): string; - setClientId(value: Uint8Array | string): RemoveDocumentRequest; + getClientId(): string; + setClientId(value: string): RemoveDocumentRequest; getDocumentId(): string; setDocumentId(value: string): RemoveDocumentRequest; @@ -302,16 +270,13 @@ export class RemoveDocumentRequest extends jspb.Message { export namespace RemoveDocumentRequest { export type AsObject = { - clientId: Uint8Array | string, + clientId: string, documentId: string, changePack?: yorkie_v1_resources_pb.ChangePack.AsObject, } } export class RemoveDocumentResponse extends jspb.Message { - getClientKey(): string; - setClientKey(value: string): RemoveDocumentResponse; - getChangePack(): yorkie_v1_resources_pb.ChangePack | undefined; setChangePack(value?: yorkie_v1_resources_pb.ChangePack): RemoveDocumentResponse; hasChangePack(): boolean; @@ -327,16 +292,13 @@ export class RemoveDocumentResponse extends jspb.Message { export namespace RemoveDocumentResponse { export type AsObject = { - clientKey: string, changePack?: yorkie_v1_resources_pb.ChangePack.AsObject, } } export class PushPullChangesRequest extends jspb.Message { - getClientId(): Uint8Array | string; - getClientId_asU8(): Uint8Array; - getClientId_asB64(): string; - setClientId(value: Uint8Array | string): PushPullChangesRequest; + getClientId(): string; + setClientId(value: string): PushPullChangesRequest; getDocumentId(): string; setDocumentId(value: string): PushPullChangesRequest; @@ -359,7 +321,7 @@ export class PushPullChangesRequest extends jspb.Message { export namespace PushPullChangesRequest { export type AsObject = { - clientId: Uint8Array | string, + clientId: string, documentId: string, changePack?: yorkie_v1_resources_pb.ChangePack.AsObject, pushOnly: boolean, @@ -367,11 +329,6 @@ export namespace PushPullChangesRequest { } export class PushPullChangesResponse extends jspb.Message { - getClientId(): Uint8Array | string; - getClientId_asU8(): Uint8Array; - getClientId_asB64(): string; - setClientId(value: Uint8Array | string): PushPullChangesResponse; - getChangePack(): yorkie_v1_resources_pb.ChangePack | undefined; setChangePack(value?: yorkie_v1_resources_pb.ChangePack): PushPullChangesResponse; hasChangePack(): boolean; @@ -387,7 +344,6 @@ export class PushPullChangesResponse extends jspb.Message { export namespace PushPullChangesResponse { export type AsObject = { - clientId: Uint8Array | string, changePack?: yorkie_v1_resources_pb.ChangePack.AsObject, } } diff --git a/src/api/yorkie/v1/yorkie_pb.js b/src/api/yorkie/v1/yorkie_pb.js index e8d8c1015..ab01e8d1f 100644 --- a/src/api/yorkie/v1/yorkie_pb.js +++ b/src/api/yorkie/v1/yorkie_pb.js @@ -13,13 +13,7 @@ var jspb = require('google-protobuf'); var goog = jspb; -var global = - (typeof globalThis !== 'undefined' && globalThis) || - (typeof window !== 'undefined' && window) || - (typeof global !== 'undefined' && global) || - (typeof self !== 'undefined' && self) || - (function () { return this; }).call(null) || - Function('return this')(); +var global = (function() { return this || window || global || self || Function('return this')(); }).call(null); var yorkie_v1_resources_pb = require('../../yorkie/v1/resources_pb.js'); goog.object.extend(proto, yorkie_v1_resources_pb); @@ -516,8 +510,7 @@ proto.yorkie.v1.ActivateClientResponse.prototype.toObject = function(opt_include */ proto.yorkie.v1.ActivateClientResponse.toObject = function(includeInstance, msg) { var f, obj = { - clientKey: jspb.Message.getFieldWithDefault(msg, 1, ""), - clientId: msg.getClientId_asB64() + clientId: jspb.Message.getFieldWithDefault(msg, 1, "") }; if (includeInstance) { @@ -556,10 +549,6 @@ proto.yorkie.v1.ActivateClientResponse.deserializeBinaryFromReader = function(ms switch (field) { case 1: var value = /** @type {string} */ (reader.readString()); - msg.setClientKey(value); - break; - case 2: - var value = /** @type {!Uint8Array} */ (reader.readBytes()); msg.setClientId(value); break; default: @@ -591,28 +580,21 @@ proto.yorkie.v1.ActivateClientResponse.prototype.serializeBinary = function() { */ proto.yorkie.v1.ActivateClientResponse.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getClientKey(); + f = message.getClientId(); if (f.length > 0) { writer.writeString( 1, f ); } - f = message.getClientId_asU8(); - if (f.length > 0) { - writer.writeBytes( - 2, - f - ); - } }; /** - * optional string client_key = 1; + * optional string client_id = 1; * @return {string} */ -proto.yorkie.v1.ActivateClientResponse.prototype.getClientKey = function() { +proto.yorkie.v1.ActivateClientResponse.prototype.getClientId = function() { return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); }; @@ -621,50 +603,8 @@ proto.yorkie.v1.ActivateClientResponse.prototype.getClientKey = function() { * @param {string} value * @return {!proto.yorkie.v1.ActivateClientResponse} returns this */ -proto.yorkie.v1.ActivateClientResponse.prototype.setClientKey = function(value) { - return jspb.Message.setProto3StringField(this, 1, value); -}; - - -/** - * optional bytes client_id = 2; - * @return {string} - */ -proto.yorkie.v1.ActivateClientResponse.prototype.getClientId = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); -}; - - -/** - * optional bytes client_id = 2; - * This is a type-conversion wrapper around `getClientId()` - * @return {string} - */ -proto.yorkie.v1.ActivateClientResponse.prototype.getClientId_asB64 = function() { - return /** @type {string} */ (jspb.Message.bytesAsB64( - this.getClientId())); -}; - - -/** - * optional bytes client_id = 2; - * Note that Uint8Array is not supported on all browsers. - * @see http://caniuse.com/Uint8Array - * This is a type-conversion wrapper around `getClientId()` - * @return {!Uint8Array} - */ -proto.yorkie.v1.ActivateClientResponse.prototype.getClientId_asU8 = function() { - return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( - this.getClientId())); -}; - - -/** - * @param {!(string|Uint8Array)} value - * @return {!proto.yorkie.v1.ActivateClientResponse} returns this - */ proto.yorkie.v1.ActivateClientResponse.prototype.setClientId = function(value) { - return jspb.Message.setProto3BytesField(this, 2, value); + return jspb.Message.setProto3StringField(this, 1, value); }; @@ -700,7 +640,7 @@ proto.yorkie.v1.DeactivateClientRequest.prototype.toObject = function(opt_includ */ proto.yorkie.v1.DeactivateClientRequest.toObject = function(includeInstance, msg) { var f, obj = { - clientId: msg.getClientId_asB64() + clientId: jspb.Message.getFieldWithDefault(msg, 1, "") }; if (includeInstance) { @@ -738,7 +678,7 @@ proto.yorkie.v1.DeactivateClientRequest.deserializeBinaryFromReader = function(m var field = reader.getFieldNumber(); switch (field) { case 1: - var value = /** @type {!Uint8Array} */ (reader.readBytes()); + var value = /** @type {string} */ (reader.readString()); msg.setClientId(value); break; default: @@ -770,9 +710,9 @@ proto.yorkie.v1.DeactivateClientRequest.prototype.serializeBinary = function() { */ proto.yorkie.v1.DeactivateClientRequest.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getClientId_asU8(); + f = message.getClientId(); if (f.length > 0) { - writer.writeBytes( + writer.writeString( 1, f ); @@ -781,7 +721,7 @@ proto.yorkie.v1.DeactivateClientRequest.serializeBinaryToWriter = function(messa /** - * optional bytes client_id = 1; + * optional string client_id = 1; * @return {string} */ proto.yorkie.v1.DeactivateClientRequest.prototype.getClientId = function() { @@ -790,35 +730,11 @@ proto.yorkie.v1.DeactivateClientRequest.prototype.getClientId = function() { /** - * optional bytes client_id = 1; - * This is a type-conversion wrapper around `getClientId()` - * @return {string} - */ -proto.yorkie.v1.DeactivateClientRequest.prototype.getClientId_asB64 = function() { - return /** @type {string} */ (jspb.Message.bytesAsB64( - this.getClientId())); -}; - - -/** - * optional bytes client_id = 1; - * Note that Uint8Array is not supported on all browsers. - * @see http://caniuse.com/Uint8Array - * This is a type-conversion wrapper around `getClientId()` - * @return {!Uint8Array} - */ -proto.yorkie.v1.DeactivateClientRequest.prototype.getClientId_asU8 = function() { - return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( - this.getClientId())); -}; - - -/** - * @param {!(string|Uint8Array)} value + * @param {string} value * @return {!proto.yorkie.v1.DeactivateClientRequest} returns this */ proto.yorkie.v1.DeactivateClientRequest.prototype.setClientId = function(value) { - return jspb.Message.setProto3BytesField(this, 1, value); + return jspb.Message.setProto3StringField(this, 1, value); }; @@ -854,7 +770,7 @@ proto.yorkie.v1.DeactivateClientResponse.prototype.toObject = function(opt_inclu */ proto.yorkie.v1.DeactivateClientResponse.toObject = function(includeInstance, msg) { var f, obj = { - clientId: msg.getClientId_asB64() + }; if (includeInstance) { @@ -891,10 +807,6 @@ proto.yorkie.v1.DeactivateClientResponse.deserializeBinaryFromReader = function( } var field = reader.getFieldNumber(); switch (field) { - case 1: - var value = /** @type {!Uint8Array} */ (reader.readBytes()); - msg.setClientId(value); - break; default: reader.skipField(); break; @@ -924,55 +836,6 @@ proto.yorkie.v1.DeactivateClientResponse.prototype.serializeBinary = function() */ proto.yorkie.v1.DeactivateClientResponse.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getClientId_asU8(); - if (f.length > 0) { - writer.writeBytes( - 1, - f - ); - } -}; - - -/** - * optional bytes client_id = 1; - * @return {string} - */ -proto.yorkie.v1.DeactivateClientResponse.prototype.getClientId = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); -}; - - -/** - * optional bytes client_id = 1; - * This is a type-conversion wrapper around `getClientId()` - * @return {string} - */ -proto.yorkie.v1.DeactivateClientResponse.prototype.getClientId_asB64 = function() { - return /** @type {string} */ (jspb.Message.bytesAsB64( - this.getClientId())); -}; - - -/** - * optional bytes client_id = 1; - * Note that Uint8Array is not supported on all browsers. - * @see http://caniuse.com/Uint8Array - * This is a type-conversion wrapper around `getClientId()` - * @return {!Uint8Array} - */ -proto.yorkie.v1.DeactivateClientResponse.prototype.getClientId_asU8 = function() { - return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( - this.getClientId())); -}; - - -/** - * @param {!(string|Uint8Array)} value - * @return {!proto.yorkie.v1.DeactivateClientResponse} returns this - */ -proto.yorkie.v1.DeactivateClientResponse.prototype.setClientId = function(value) { - return jspb.Message.setProto3BytesField(this, 1, value); }; @@ -1008,7 +871,7 @@ proto.yorkie.v1.AttachDocumentRequest.prototype.toObject = function(opt_includeI */ proto.yorkie.v1.AttachDocumentRequest.toObject = function(includeInstance, msg) { var f, obj = { - clientId: msg.getClientId_asB64(), + clientId: jspb.Message.getFieldWithDefault(msg, 1, ""), changePack: (f = msg.getChangePack()) && yorkie_v1_resources_pb.ChangePack.toObject(includeInstance, f) }; @@ -1047,7 +910,7 @@ proto.yorkie.v1.AttachDocumentRequest.deserializeBinaryFromReader = function(msg var field = reader.getFieldNumber(); switch (field) { case 1: - var value = /** @type {!Uint8Array} */ (reader.readBytes()); + var value = /** @type {string} */ (reader.readString()); msg.setClientId(value); break; case 2: @@ -1084,9 +947,9 @@ proto.yorkie.v1.AttachDocumentRequest.prototype.serializeBinary = function() { */ proto.yorkie.v1.AttachDocumentRequest.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getClientId_asU8(); + f = message.getClientId(); if (f.length > 0) { - writer.writeBytes( + writer.writeString( 1, f ); @@ -1103,7 +966,7 @@ proto.yorkie.v1.AttachDocumentRequest.serializeBinaryToWriter = function(message /** - * optional bytes client_id = 1; + * optional string client_id = 1; * @return {string} */ proto.yorkie.v1.AttachDocumentRequest.prototype.getClientId = function() { @@ -1112,35 +975,11 @@ proto.yorkie.v1.AttachDocumentRequest.prototype.getClientId = function() { /** - * optional bytes client_id = 1; - * This is a type-conversion wrapper around `getClientId()` - * @return {string} - */ -proto.yorkie.v1.AttachDocumentRequest.prototype.getClientId_asB64 = function() { - return /** @type {string} */ (jspb.Message.bytesAsB64( - this.getClientId())); -}; - - -/** - * optional bytes client_id = 1; - * Note that Uint8Array is not supported on all browsers. - * @see http://caniuse.com/Uint8Array - * This is a type-conversion wrapper around `getClientId()` - * @return {!Uint8Array} - */ -proto.yorkie.v1.AttachDocumentRequest.prototype.getClientId_asU8 = function() { - return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( - this.getClientId())); -}; - - -/** - * @param {!(string|Uint8Array)} value + * @param {string} value * @return {!proto.yorkie.v1.AttachDocumentRequest} returns this */ proto.yorkie.v1.AttachDocumentRequest.prototype.setClientId = function(value) { - return jspb.Message.setProto3BytesField(this, 1, value); + return jspb.Message.setProto3StringField(this, 1, value); }; @@ -1213,8 +1052,7 @@ proto.yorkie.v1.AttachDocumentResponse.prototype.toObject = function(opt_include */ proto.yorkie.v1.AttachDocumentResponse.toObject = function(includeInstance, msg) { var f, obj = { - clientId: msg.getClientId_asB64(), - documentId: jspb.Message.getFieldWithDefault(msg, 2, ""), + documentId: jspb.Message.getFieldWithDefault(msg, 1, ""), changePack: (f = msg.getChangePack()) && yorkie_v1_resources_pb.ChangePack.toObject(includeInstance, f) }; @@ -1253,14 +1091,10 @@ proto.yorkie.v1.AttachDocumentResponse.deserializeBinaryFromReader = function(ms var field = reader.getFieldNumber(); switch (field) { case 1: - var value = /** @type {!Uint8Array} */ (reader.readBytes()); - msg.setClientId(value); - break; - case 2: var value = /** @type {string} */ (reader.readString()); msg.setDocumentId(value); break; - case 3: + case 2: var value = new yorkie_v1_resources_pb.ChangePack; reader.readMessage(value,yorkie_v1_resources_pb.ChangePack.deserializeBinaryFromReader); msg.setChangePack(value); @@ -1294,24 +1128,17 @@ proto.yorkie.v1.AttachDocumentResponse.prototype.serializeBinary = function() { */ proto.yorkie.v1.AttachDocumentResponse.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getClientId_asU8(); - if (f.length > 0) { - writer.writeBytes( - 1, - f - ); - } f = message.getDocumentId(); if (f.length > 0) { writer.writeString( - 2, + 1, f ); } f = message.getChangePack(); if (f != null) { writer.writeMessage( - 3, + 2, f, yorkie_v1_resources_pb.ChangePack.serializeBinaryToWriter ); @@ -1320,53 +1147,11 @@ proto.yorkie.v1.AttachDocumentResponse.serializeBinaryToWriter = function(messag /** - * optional bytes client_id = 1; - * @return {string} - */ -proto.yorkie.v1.AttachDocumentResponse.prototype.getClientId = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); -}; - - -/** - * optional bytes client_id = 1; - * This is a type-conversion wrapper around `getClientId()` - * @return {string} - */ -proto.yorkie.v1.AttachDocumentResponse.prototype.getClientId_asB64 = function() { - return /** @type {string} */ (jspb.Message.bytesAsB64( - this.getClientId())); -}; - - -/** - * optional bytes client_id = 1; - * Note that Uint8Array is not supported on all browsers. - * @see http://caniuse.com/Uint8Array - * This is a type-conversion wrapper around `getClientId()` - * @return {!Uint8Array} - */ -proto.yorkie.v1.AttachDocumentResponse.prototype.getClientId_asU8 = function() { - return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( - this.getClientId())); -}; - - -/** - * @param {!(string|Uint8Array)} value - * @return {!proto.yorkie.v1.AttachDocumentResponse} returns this - */ -proto.yorkie.v1.AttachDocumentResponse.prototype.setClientId = function(value) { - return jspb.Message.setProto3BytesField(this, 1, value); -}; - - -/** - * optional string document_id = 2; + * optional string document_id = 1; * @return {string} */ proto.yorkie.v1.AttachDocumentResponse.prototype.getDocumentId = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); }; @@ -1375,17 +1160,17 @@ proto.yorkie.v1.AttachDocumentResponse.prototype.getDocumentId = function() { * @return {!proto.yorkie.v1.AttachDocumentResponse} returns this */ proto.yorkie.v1.AttachDocumentResponse.prototype.setDocumentId = function(value) { - return jspb.Message.setProto3StringField(this, 2, value); + return jspb.Message.setProto3StringField(this, 1, value); }; /** - * optional ChangePack change_pack = 3; + * optional ChangePack change_pack = 2; * @return {?proto.yorkie.v1.ChangePack} */ proto.yorkie.v1.AttachDocumentResponse.prototype.getChangePack = function() { return /** @type{?proto.yorkie.v1.ChangePack} */ ( - jspb.Message.getWrapperField(this, yorkie_v1_resources_pb.ChangePack, 3)); + jspb.Message.getWrapperField(this, yorkie_v1_resources_pb.ChangePack, 2)); }; @@ -1394,7 +1179,7 @@ proto.yorkie.v1.AttachDocumentResponse.prototype.getChangePack = function() { * @return {!proto.yorkie.v1.AttachDocumentResponse} returns this */ proto.yorkie.v1.AttachDocumentResponse.prototype.setChangePack = function(value) { - return jspb.Message.setWrapperField(this, 3, value); + return jspb.Message.setWrapperField(this, 2, value); }; @@ -1412,7 +1197,7 @@ proto.yorkie.v1.AttachDocumentResponse.prototype.clearChangePack = function() { * @return {boolean} */ proto.yorkie.v1.AttachDocumentResponse.prototype.hasChangePack = function() { - return jspb.Message.getField(this, 3) != null; + return jspb.Message.getField(this, 2) != null; }; @@ -1448,7 +1233,7 @@ proto.yorkie.v1.DetachDocumentRequest.prototype.toObject = function(opt_includeI */ proto.yorkie.v1.DetachDocumentRequest.toObject = function(includeInstance, msg) { var f, obj = { - clientId: msg.getClientId_asB64(), + clientId: jspb.Message.getFieldWithDefault(msg, 1, ""), documentId: jspb.Message.getFieldWithDefault(msg, 2, ""), changePack: (f = msg.getChangePack()) && yorkie_v1_resources_pb.ChangePack.toObject(includeInstance, f), removeIfNotAttached: jspb.Message.getBooleanFieldWithDefault(msg, 4, false) @@ -1489,7 +1274,7 @@ proto.yorkie.v1.DetachDocumentRequest.deserializeBinaryFromReader = function(msg var field = reader.getFieldNumber(); switch (field) { case 1: - var value = /** @type {!Uint8Array} */ (reader.readBytes()); + var value = /** @type {string} */ (reader.readString()); msg.setClientId(value); break; case 2: @@ -1534,9 +1319,9 @@ proto.yorkie.v1.DetachDocumentRequest.prototype.serializeBinary = function() { */ proto.yorkie.v1.DetachDocumentRequest.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getClientId_asU8(); + f = message.getClientId(); if (f.length > 0) { - writer.writeBytes( + writer.writeString( 1, f ); @@ -1567,7 +1352,7 @@ proto.yorkie.v1.DetachDocumentRequest.serializeBinaryToWriter = function(message /** - * optional bytes client_id = 1; + * optional string client_id = 1; * @return {string} */ proto.yorkie.v1.DetachDocumentRequest.prototype.getClientId = function() { @@ -1576,35 +1361,11 @@ proto.yorkie.v1.DetachDocumentRequest.prototype.getClientId = function() { /** - * optional bytes client_id = 1; - * This is a type-conversion wrapper around `getClientId()` - * @return {string} - */ -proto.yorkie.v1.DetachDocumentRequest.prototype.getClientId_asB64 = function() { - return /** @type {string} */ (jspb.Message.bytesAsB64( - this.getClientId())); -}; - - -/** - * optional bytes client_id = 1; - * Note that Uint8Array is not supported on all browsers. - * @see http://caniuse.com/Uint8Array - * This is a type-conversion wrapper around `getClientId()` - * @return {!Uint8Array} - */ -proto.yorkie.v1.DetachDocumentRequest.prototype.getClientId_asU8 = function() { - return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( - this.getClientId())); -}; - - -/** - * @param {!(string|Uint8Array)} value + * @param {string} value * @return {!proto.yorkie.v1.DetachDocumentRequest} returns this */ proto.yorkie.v1.DetachDocumentRequest.prototype.setClientId = function(value) { - return jspb.Message.setProto3BytesField(this, 1, value); + return jspb.Message.setProto3StringField(this, 1, value); }; @@ -1713,7 +1474,6 @@ proto.yorkie.v1.DetachDocumentResponse.prototype.toObject = function(opt_include */ proto.yorkie.v1.DetachDocumentResponse.toObject = function(includeInstance, msg) { var f, obj = { - clientKey: jspb.Message.getFieldWithDefault(msg, 1, ""), changePack: (f = msg.getChangePack()) && yorkie_v1_resources_pb.ChangePack.toObject(includeInstance, f) }; @@ -1751,10 +1511,6 @@ proto.yorkie.v1.DetachDocumentResponse.deserializeBinaryFromReader = function(ms } var field = reader.getFieldNumber(); switch (field) { - case 1: - var value = /** @type {string} */ (reader.readString()); - msg.setClientKey(value); - break; case 2: var value = new yorkie_v1_resources_pb.ChangePack; reader.readMessage(value,yorkie_v1_resources_pb.ChangePack.deserializeBinaryFromReader); @@ -1789,13 +1545,6 @@ proto.yorkie.v1.DetachDocumentResponse.prototype.serializeBinary = function() { */ proto.yorkie.v1.DetachDocumentResponse.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getClientKey(); - if (f.length > 0) { - writer.writeString( - 1, - f - ); - } f = message.getChangePack(); if (f != null) { writer.writeMessage( @@ -1807,24 +1556,6 @@ proto.yorkie.v1.DetachDocumentResponse.serializeBinaryToWriter = function(messag }; -/** - * optional string client_key = 1; - * @return {string} - */ -proto.yorkie.v1.DetachDocumentResponse.prototype.getClientKey = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); -}; - - -/** - * @param {string} value - * @return {!proto.yorkie.v1.DetachDocumentResponse} returns this - */ -proto.yorkie.v1.DetachDocumentResponse.prototype.setClientKey = function(value) { - return jspb.Message.setProto3StringField(this, 1, value); -}; - - /** * optional ChangePack change_pack = 2; * @return {?proto.yorkie.v1.ChangePack} @@ -1894,7 +1625,7 @@ proto.yorkie.v1.WatchDocumentRequest.prototype.toObject = function(opt_includeIn */ proto.yorkie.v1.WatchDocumentRequest.toObject = function(includeInstance, msg) { var f, obj = { - clientId: msg.getClientId_asB64(), + clientId: jspb.Message.getFieldWithDefault(msg, 1, ""), documentId: jspb.Message.getFieldWithDefault(msg, 2, "") }; @@ -1933,7 +1664,7 @@ proto.yorkie.v1.WatchDocumentRequest.deserializeBinaryFromReader = function(msg, var field = reader.getFieldNumber(); switch (field) { case 1: - var value = /** @type {!Uint8Array} */ (reader.readBytes()); + var value = /** @type {string} */ (reader.readString()); msg.setClientId(value); break; case 2: @@ -1969,9 +1700,9 @@ proto.yorkie.v1.WatchDocumentRequest.prototype.serializeBinary = function() { */ proto.yorkie.v1.WatchDocumentRequest.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getClientId_asU8(); + f = message.getClientId(); if (f.length > 0) { - writer.writeBytes( + writer.writeString( 1, f ); @@ -1987,7 +1718,7 @@ proto.yorkie.v1.WatchDocumentRequest.serializeBinaryToWriter = function(message, /** - * optional bytes client_id = 1; + * optional string client_id = 1; * @return {string} */ proto.yorkie.v1.WatchDocumentRequest.prototype.getClientId = function() { @@ -1996,35 +1727,11 @@ proto.yorkie.v1.WatchDocumentRequest.prototype.getClientId = function() { /** - * optional bytes client_id = 1; - * This is a type-conversion wrapper around `getClientId()` - * @return {string} - */ -proto.yorkie.v1.WatchDocumentRequest.prototype.getClientId_asB64 = function() { - return /** @type {string} */ (jspb.Message.bytesAsB64( - this.getClientId())); -}; - - -/** - * optional bytes client_id = 1; - * Note that Uint8Array is not supported on all browsers. - * @see http://caniuse.com/Uint8Array - * This is a type-conversion wrapper around `getClientId()` - * @return {!Uint8Array} - */ -proto.yorkie.v1.WatchDocumentRequest.prototype.getClientId_asU8 = function() { - return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( - this.getClientId())); -}; - - -/** - * @param {!(string|Uint8Array)} value + * @param {string} value * @return {!proto.yorkie.v1.WatchDocumentRequest} returns this */ proto.yorkie.v1.WatchDocumentRequest.prototype.setClientId = function(value) { - return jspb.Message.setProto3BytesField(this, 1, value); + return jspb.Message.setProto3StringField(this, 1, value); }; @@ -2239,7 +1946,7 @@ proto.yorkie.v1.WatchDocumentResponse.Initialization.prototype.toObject = functi */ proto.yorkie.v1.WatchDocumentResponse.Initialization.toObject = function(includeInstance, msg) { var f, obj = { - clientIdsList: msg.getClientIdsList_asB64() + clientIdsList: (f = jspb.Message.getRepeatedField(msg, 1)) == null ? undefined : f }; if (includeInstance) { @@ -2277,7 +1984,7 @@ proto.yorkie.v1.WatchDocumentResponse.Initialization.deserializeBinaryFromReader var field = reader.getFieldNumber(); switch (field) { case 1: - var value = /** @type {!Uint8Array} */ (reader.readBytes()); + var value = /** @type {string} */ (reader.readString()); msg.addClientIds(value); break; default: @@ -2309,9 +2016,9 @@ proto.yorkie.v1.WatchDocumentResponse.Initialization.prototype.serializeBinary = */ proto.yorkie.v1.WatchDocumentResponse.Initialization.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getClientIdsList_asU8(); + f = message.getClientIdsList(); if (f.length > 0) { - writer.writeRepeatedBytes( + writer.writeRepeatedString( 1, f ); @@ -2320,7 +2027,7 @@ proto.yorkie.v1.WatchDocumentResponse.Initialization.serializeBinaryToWriter = f /** - * repeated bytes client_ids = 1; + * repeated string client_ids = 1; * @return {!Array} */ proto.yorkie.v1.WatchDocumentResponse.Initialization.prototype.getClientIdsList = function() { @@ -2329,31 +2036,7 @@ proto.yorkie.v1.WatchDocumentResponse.Initialization.prototype.getClientIdsList /** - * repeated bytes client_ids = 1; - * This is a type-conversion wrapper around `getClientIdsList()` - * @return {!Array} - */ -proto.yorkie.v1.WatchDocumentResponse.Initialization.prototype.getClientIdsList_asB64 = function() { - return /** @type {!Array} */ (jspb.Message.bytesListAsB64( - this.getClientIdsList())); -}; - - -/** - * repeated bytes client_ids = 1; - * Note that Uint8Array is not supported on all browsers. - * @see http://caniuse.com/Uint8Array - * This is a type-conversion wrapper around `getClientIdsList()` - * @return {!Array} - */ -proto.yorkie.v1.WatchDocumentResponse.Initialization.prototype.getClientIdsList_asU8 = function() { - return /** @type {!Array} */ (jspb.Message.bytesListAsU8( - this.getClientIdsList())); -}; - - -/** - * @param {!(Array|Array)} value + * @param {!Array} value * @return {!proto.yorkie.v1.WatchDocumentResponse.Initialization} returns this */ proto.yorkie.v1.WatchDocumentResponse.Initialization.prototype.setClientIdsList = function(value) { @@ -2362,7 +2045,7 @@ proto.yorkie.v1.WatchDocumentResponse.Initialization.prototype.setClientIdsList /** - * @param {!(string|Uint8Array)} value + * @param {string} value * @param {number=} opt_index * @return {!proto.yorkie.v1.WatchDocumentResponse.Initialization} returns this */ @@ -2486,7 +2169,7 @@ proto.yorkie.v1.RemoveDocumentRequest.prototype.toObject = function(opt_includeI */ proto.yorkie.v1.RemoveDocumentRequest.toObject = function(includeInstance, msg) { var f, obj = { - clientId: msg.getClientId_asB64(), + clientId: jspb.Message.getFieldWithDefault(msg, 1, ""), documentId: jspb.Message.getFieldWithDefault(msg, 2, ""), changePack: (f = msg.getChangePack()) && yorkie_v1_resources_pb.ChangePack.toObject(includeInstance, f) }; @@ -2526,7 +2209,7 @@ proto.yorkie.v1.RemoveDocumentRequest.deserializeBinaryFromReader = function(msg var field = reader.getFieldNumber(); switch (field) { case 1: - var value = /** @type {!Uint8Array} */ (reader.readBytes()); + var value = /** @type {string} */ (reader.readString()); msg.setClientId(value); break; case 2: @@ -2567,9 +2250,9 @@ proto.yorkie.v1.RemoveDocumentRequest.prototype.serializeBinary = function() { */ proto.yorkie.v1.RemoveDocumentRequest.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getClientId_asU8(); + f = message.getClientId(); if (f.length > 0) { - writer.writeBytes( + writer.writeString( 1, f ); @@ -2593,7 +2276,7 @@ proto.yorkie.v1.RemoveDocumentRequest.serializeBinaryToWriter = function(message /** - * optional bytes client_id = 1; + * optional string client_id = 1; * @return {string} */ proto.yorkie.v1.RemoveDocumentRequest.prototype.getClientId = function() { @@ -2602,35 +2285,11 @@ proto.yorkie.v1.RemoveDocumentRequest.prototype.getClientId = function() { /** - * optional bytes client_id = 1; - * This is a type-conversion wrapper around `getClientId()` - * @return {string} - */ -proto.yorkie.v1.RemoveDocumentRequest.prototype.getClientId_asB64 = function() { - return /** @type {string} */ (jspb.Message.bytesAsB64( - this.getClientId())); -}; - - -/** - * optional bytes client_id = 1; - * Note that Uint8Array is not supported on all browsers. - * @see http://caniuse.com/Uint8Array - * This is a type-conversion wrapper around `getClientId()` - * @return {!Uint8Array} - */ -proto.yorkie.v1.RemoveDocumentRequest.prototype.getClientId_asU8 = function() { - return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( - this.getClientId())); -}; - - -/** - * @param {!(string|Uint8Array)} value + * @param {string} value * @return {!proto.yorkie.v1.RemoveDocumentRequest} returns this */ proto.yorkie.v1.RemoveDocumentRequest.prototype.setClientId = function(value) { - return jspb.Message.setProto3BytesField(this, 1, value); + return jspb.Message.setProto3StringField(this, 1, value); }; @@ -2721,7 +2380,6 @@ proto.yorkie.v1.RemoveDocumentResponse.prototype.toObject = function(opt_include */ proto.yorkie.v1.RemoveDocumentResponse.toObject = function(includeInstance, msg) { var f, obj = { - clientKey: jspb.Message.getFieldWithDefault(msg, 1, ""), changePack: (f = msg.getChangePack()) && yorkie_v1_resources_pb.ChangePack.toObject(includeInstance, f) }; @@ -2760,10 +2418,6 @@ proto.yorkie.v1.RemoveDocumentResponse.deserializeBinaryFromReader = function(ms var field = reader.getFieldNumber(); switch (field) { case 1: - var value = /** @type {string} */ (reader.readString()); - msg.setClientKey(value); - break; - case 2: var value = new yorkie_v1_resources_pb.ChangePack; reader.readMessage(value,yorkie_v1_resources_pb.ChangePack.deserializeBinaryFromReader); msg.setChangePack(value); @@ -2797,17 +2451,10 @@ proto.yorkie.v1.RemoveDocumentResponse.prototype.serializeBinary = function() { */ proto.yorkie.v1.RemoveDocumentResponse.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getClientKey(); - if (f.length > 0) { - writer.writeString( - 1, - f - ); - } f = message.getChangePack(); if (f != null) { writer.writeMessage( - 2, + 1, f, yorkie_v1_resources_pb.ChangePack.serializeBinaryToWriter ); @@ -2816,30 +2463,12 @@ proto.yorkie.v1.RemoveDocumentResponse.serializeBinaryToWriter = function(messag /** - * optional string client_key = 1; - * @return {string} - */ -proto.yorkie.v1.RemoveDocumentResponse.prototype.getClientKey = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); -}; - - -/** - * @param {string} value - * @return {!proto.yorkie.v1.RemoveDocumentResponse} returns this - */ -proto.yorkie.v1.RemoveDocumentResponse.prototype.setClientKey = function(value) { - return jspb.Message.setProto3StringField(this, 1, value); -}; - - -/** - * optional ChangePack change_pack = 2; + * optional ChangePack change_pack = 1; * @return {?proto.yorkie.v1.ChangePack} */ proto.yorkie.v1.RemoveDocumentResponse.prototype.getChangePack = function() { return /** @type{?proto.yorkie.v1.ChangePack} */ ( - jspb.Message.getWrapperField(this, yorkie_v1_resources_pb.ChangePack, 2)); + jspb.Message.getWrapperField(this, yorkie_v1_resources_pb.ChangePack, 1)); }; @@ -2848,7 +2477,7 @@ proto.yorkie.v1.RemoveDocumentResponse.prototype.getChangePack = function() { * @return {!proto.yorkie.v1.RemoveDocumentResponse} returns this */ proto.yorkie.v1.RemoveDocumentResponse.prototype.setChangePack = function(value) { - return jspb.Message.setWrapperField(this, 2, value); + return jspb.Message.setWrapperField(this, 1, value); }; @@ -2866,7 +2495,7 @@ proto.yorkie.v1.RemoveDocumentResponse.prototype.clearChangePack = function() { * @return {boolean} */ proto.yorkie.v1.RemoveDocumentResponse.prototype.hasChangePack = function() { - return jspb.Message.getField(this, 2) != null; + return jspb.Message.getField(this, 1) != null; }; @@ -2902,7 +2531,7 @@ proto.yorkie.v1.PushPullChangesRequest.prototype.toObject = function(opt_include */ proto.yorkie.v1.PushPullChangesRequest.toObject = function(includeInstance, msg) { var f, obj = { - clientId: msg.getClientId_asB64(), + clientId: jspb.Message.getFieldWithDefault(msg, 1, ""), documentId: jspb.Message.getFieldWithDefault(msg, 2, ""), changePack: (f = msg.getChangePack()) && yorkie_v1_resources_pb.ChangePack.toObject(includeInstance, f), pushOnly: jspb.Message.getBooleanFieldWithDefault(msg, 4, false) @@ -2943,7 +2572,7 @@ proto.yorkie.v1.PushPullChangesRequest.deserializeBinaryFromReader = function(ms var field = reader.getFieldNumber(); switch (field) { case 1: - var value = /** @type {!Uint8Array} */ (reader.readBytes()); + var value = /** @type {string} */ (reader.readString()); msg.setClientId(value); break; case 2: @@ -2988,9 +2617,9 @@ proto.yorkie.v1.PushPullChangesRequest.prototype.serializeBinary = function() { */ proto.yorkie.v1.PushPullChangesRequest.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getClientId_asU8(); + f = message.getClientId(); if (f.length > 0) { - writer.writeBytes( + writer.writeString( 1, f ); @@ -3021,7 +2650,7 @@ proto.yorkie.v1.PushPullChangesRequest.serializeBinaryToWriter = function(messag /** - * optional bytes client_id = 1; + * optional string client_id = 1; * @return {string} */ proto.yorkie.v1.PushPullChangesRequest.prototype.getClientId = function() { @@ -3030,35 +2659,11 @@ proto.yorkie.v1.PushPullChangesRequest.prototype.getClientId = function() { /** - * optional bytes client_id = 1; - * This is a type-conversion wrapper around `getClientId()` - * @return {string} - */ -proto.yorkie.v1.PushPullChangesRequest.prototype.getClientId_asB64 = function() { - return /** @type {string} */ (jspb.Message.bytesAsB64( - this.getClientId())); -}; - - -/** - * optional bytes client_id = 1; - * Note that Uint8Array is not supported on all browsers. - * @see http://caniuse.com/Uint8Array - * This is a type-conversion wrapper around `getClientId()` - * @return {!Uint8Array} - */ -proto.yorkie.v1.PushPullChangesRequest.prototype.getClientId_asU8 = function() { - return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( - this.getClientId())); -}; - - -/** - * @param {!(string|Uint8Array)} value + * @param {string} value * @return {!proto.yorkie.v1.PushPullChangesRequest} returns this */ proto.yorkie.v1.PushPullChangesRequest.prototype.setClientId = function(value) { - return jspb.Message.setProto3BytesField(this, 1, value); + return jspb.Message.setProto3StringField(this, 1, value); }; @@ -3167,7 +2772,6 @@ proto.yorkie.v1.PushPullChangesResponse.prototype.toObject = function(opt_includ */ proto.yorkie.v1.PushPullChangesResponse.toObject = function(includeInstance, msg) { var f, obj = { - clientId: msg.getClientId_asB64(), changePack: (f = msg.getChangePack()) && yorkie_v1_resources_pb.ChangePack.toObject(includeInstance, f) }; @@ -3206,10 +2810,6 @@ proto.yorkie.v1.PushPullChangesResponse.deserializeBinaryFromReader = function(m var field = reader.getFieldNumber(); switch (field) { case 1: - var value = /** @type {!Uint8Array} */ (reader.readBytes()); - msg.setClientId(value); - break; - case 2: var value = new yorkie_v1_resources_pb.ChangePack; reader.readMessage(value,yorkie_v1_resources_pb.ChangePack.deserializeBinaryFromReader); msg.setChangePack(value); @@ -3243,17 +2843,10 @@ proto.yorkie.v1.PushPullChangesResponse.prototype.serializeBinary = function() { */ proto.yorkie.v1.PushPullChangesResponse.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getClientId_asU8(); - if (f.length > 0) { - writer.writeBytes( - 1, - f - ); - } f = message.getChangePack(); if (f != null) { writer.writeMessage( - 2, + 1, f, yorkie_v1_resources_pb.ChangePack.serializeBinaryToWriter ); @@ -3262,54 +2855,12 @@ proto.yorkie.v1.PushPullChangesResponse.serializeBinaryToWriter = function(messa /** - * optional bytes client_id = 1; - * @return {string} - */ -proto.yorkie.v1.PushPullChangesResponse.prototype.getClientId = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); -}; - - -/** - * optional bytes client_id = 1; - * This is a type-conversion wrapper around `getClientId()` - * @return {string} - */ -proto.yorkie.v1.PushPullChangesResponse.prototype.getClientId_asB64 = function() { - return /** @type {string} */ (jspb.Message.bytesAsB64( - this.getClientId())); -}; - - -/** - * optional bytes client_id = 1; - * Note that Uint8Array is not supported on all browsers. - * @see http://caniuse.com/Uint8Array - * This is a type-conversion wrapper around `getClientId()` - * @return {!Uint8Array} - */ -proto.yorkie.v1.PushPullChangesResponse.prototype.getClientId_asU8 = function() { - return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( - this.getClientId())); -}; - - -/** - * @param {!(string|Uint8Array)} value - * @return {!proto.yorkie.v1.PushPullChangesResponse} returns this - */ -proto.yorkie.v1.PushPullChangesResponse.prototype.setClientId = function(value) { - return jspb.Message.setProto3BytesField(this, 1, value); -}; - - -/** - * optional ChangePack change_pack = 2; + * optional ChangePack change_pack = 1; * @return {?proto.yorkie.v1.ChangePack} */ proto.yorkie.v1.PushPullChangesResponse.prototype.getChangePack = function() { return /** @type{?proto.yorkie.v1.ChangePack} */ ( - jspb.Message.getWrapperField(this, yorkie_v1_resources_pb.ChangePack, 2)); + jspb.Message.getWrapperField(this, yorkie_v1_resources_pb.ChangePack, 1)); }; @@ -3318,7 +2869,7 @@ proto.yorkie.v1.PushPullChangesResponse.prototype.getChangePack = function() { * @return {!proto.yorkie.v1.PushPullChangesResponse} returns this */ proto.yorkie.v1.PushPullChangesResponse.prototype.setChangePack = function(value) { - return jspb.Message.setWrapperField(this, 2, value); + return jspb.Message.setWrapperField(this, 1, value); }; @@ -3336,7 +2887,7 @@ proto.yorkie.v1.PushPullChangesResponse.prototype.clearChangePack = function() { * @return {boolean} */ proto.yorkie.v1.PushPullChangesResponse.prototype.hasChangePack = function() { - return jspb.Message.getField(this, 2) != null; + return jspb.Message.getField(this, 1) != null; }; diff --git a/src/client/client.ts b/src/client/client.ts index abb207df3..0e2b3b4ad 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -134,9 +134,9 @@ export enum ClientEventType { */ StatusChanged = 'status-changed', /** - * `DocumentsChanged` means that the documents of the client has changed. + * `DocumentChanged` means that the document has changed. */ - DocumentsChanged = 'documents-changed', + DocumentChanged = 'document-changed', /** * `StreamConnectionStatusChanged` means that the stream connection status of * the client has changed. @@ -156,7 +156,7 @@ export enum ClientEventType { */ export type ClientEvent = | StatusChangedEvent - | DocumentsChangedEvent + | DocumentChangedEvent | StreamConnectionStatusChangedEvent | DocumentSyncedEvent; @@ -178,24 +178,24 @@ export interface StatusChangedEvent extends BaseClientEvent { */ type: ClientEventType.StatusChanged; /** - * `DocumentsChangedEvent` value + * `StatusChangedEvent` value */ value: ClientStatus; } /** - * `DocumentsChangedEvent` is an event that occurs when documents attached to + * `DocumentChangedEvent` is an event that occurs when document attached to * the client changes. * * @public */ -export interface DocumentsChangedEvent extends BaseClientEvent { +export interface DocumentChangedEvent extends BaseClientEvent { /** - * enum {@link ClientEventType}.DocumentsChangedEvent + * enum {@link ClientEventType}.DocumentChangedEvent */ - type: ClientEventType.DocumentsChanged; + type: ClientEventType.DocumentChanged; /** - * `DocumentsChangedEvent` value + * `DocumentChangedEvent` value */ value: Array; } @@ -219,7 +219,7 @@ export interface StreamConnectionStatusChangedEvent extends BaseClientEvent { } /** - * `DocumentSyncedEvent` is an event that occurs when documents + * `DocumentSyncedEvent` is an event that occurs when document * attached to the client are synced. * * @public @@ -377,7 +377,7 @@ export class Client implements Observable { return; } - this.id = converter.toHexString(res.getClientId_asU8()); + this.id = res.getClientId(); this.status = ClientStatus.Activated; this.runSyncLoop(); @@ -405,7 +405,7 @@ export class Client implements Observable { }); return new Promise((resolve, reject) => { const req = new DeactivateClientRequest(); - req.setClientId(converter.toUint8Array(this.id!)); + req.setClientId(this.id!); this.rpcClient.deactivateClient( req, @@ -457,7 +457,7 @@ export class Client implements Observable { return new Promise((resolve, reject) => { const req = new AttachDocumentRequest(); - req.setClientId(converter.toUint8Array(this.id!)); + req.setClientId(this.id!); req.setChangePack(converter.toChangePack(doc.createChangePack())); this.rpcClient.attachDocument( @@ -521,7 +521,7 @@ export class Client implements Observable { return new Promise((resolve, reject) => { const req = new DetachDocumentRequest(); - req.setClientId(converter.toUint8Array(this.id!)); + req.setClientId(this.id!); req.setDocumentId(attachment.docID); req.setChangePack(converter.toChangePack(doc.createChangePack())); @@ -703,7 +703,7 @@ export class Client implements Observable { doc.setActor(this.id!); return new Promise((resolve, reject) => { const req = new RemoveDocumentRequest(); - req.setClientId(converter.toUint8Array(this.id!)); + req.setClientId(this.id!); req.setDocumentId(attachment.docID); const pbChangePack = converter.toChangePack(doc.createChangePack()); pbChangePack.setIsRemoved(true); @@ -826,7 +826,7 @@ export class Client implements Observable { } const req = new WatchDocumentRequest(); - req.setClientId(converter.toUint8Array(this.id!)); + req.setClientId(this.id!); req.setDocumentId(attachment.docID); const stream = this.rpcClient.watchDocument(req, { 'x-shard-key': `${this.apiKey}/${docKey}`, @@ -865,10 +865,9 @@ export class Client implements Observable { ) { const docKey = attachment.doc.getKey(); if (resp.hasInitialization()) { - const pbClientIDs = resp.getInitialization()!.getClientIdsList(); + const clientIDs = resp.getInitialization()!.getClientIdsList(); const onlineClients: Set = new Set(); - for (const pbClientID of pbClientIDs) { - const clientID = converter.toHexString(pbClientID as Uint8Array); + for (const clientID of clientIDs) { onlineClients.add(clientID); } attachment.doc.setOnlineClients(onlineClients); @@ -881,16 +880,16 @@ export class Client implements Observable { const pbWatchEvent = resp.getEvent()!; const eventType = pbWatchEvent.getType(); - const publisher = converter.toHexString(pbWatchEvent.getPublisher_asU8()); + const publisher = pbWatchEvent.getPublisher(); switch (eventType) { - case PbDocEventType.DOC_EVENT_TYPE_DOCUMENTS_CHANGED: + case PbDocEventType.DOC_EVENT_TYPE_DOCUMENT_CHANGED: attachment.remoteChangeEventReceived = true; this.eventStreamObserver.next({ - type: ClientEventType.DocumentsChanged, + type: ClientEventType.DocumentChanged, value: [docKey], }); break; - case PbDocEventType.DOC_EVENT_TYPE_DOCUMENTS_WATCHED: + case PbDocEventType.DOC_EVENT_TYPE_DOCUMENT_WATCHED: attachment.doc.addOnlineClient(publisher); // NOTE(chacha912): We added to onlineClients, but we won't trigger watched event // unless we also know their initial presence data at this point. @@ -904,7 +903,7 @@ export class Client implements Observable { }); } break; - case PbDocEventType.DOC_EVENT_TYPE_DOCUMENTS_UNWATCHED: { + case PbDocEventType.DOC_EVENT_TYPE_DOCUMENT_UNWATCHED: { const presence = attachment.doc.getPresence(publisher); attachment.doc.removeOnlineClient(publisher); // NOTE(chacha912): There is no presence, when PresenceChange(clear) is applied before unwatching. @@ -947,7 +946,7 @@ export class Client implements Observable { const { doc, docID } = attachment; return new Promise((resolve, reject) => { const req = new PushPullChangesRequest(); - req.setClientId(converter.toUint8Array(this.id!)); + req.setClientId(this.id!); req.setDocumentId(docID); const reqPack = doc.createChangePack(); const localSize = reqPack.getChangeSize(); diff --git a/src/document/crdt/tree.ts b/src/document/crdt/tree.ts index d344b58a4..69838ee17 100644 --- a/src/document/crdt/tree.ts +++ b/src/document/crdt/tree.ts @@ -18,6 +18,7 @@ import { TimeTicket, InitialTimeTicket, TimeTicketStruct, + MaxTimeTicket, } from '@yorkie-js-sdk/src/document/time/ticket'; import { CRDTGCElement } from '@yorkie-js-sdk/src/document/crdt/element'; import { @@ -25,19 +26,14 @@ import { TreePos, IndexTreeNode, TreeNodeType, - traverse, + traverseAll, } from '@yorkie-js-sdk/src/util/index_tree'; +import { RHT } from './rht'; import { ActorID } from './../time/actor_id'; import { LLRBTree } from '@yorkie-js-sdk/src/util/llrb_tree'; -import { RHT } from './rht'; +import { Comparator } from '@yorkie-js-sdk/src/util/comparator'; import { parseObjectValues } from '@yorkie-js-sdk/src/util/object'; -/** - * DummyHeadType is a type of dummy head. It is used to represent the head node - * of RGA. - */ -const DummyHeadType = 'dummy'; - /** * `TreeNode` represents the JSON representation of a node in the tree. * It is used to serialize and deserialize the tree. @@ -81,11 +77,100 @@ export interface TreeChange { } /** - * `CRDTTreePos` represent a position in the tree. It indicates the virtual - * location in the tree, so whether the node is splitted or not, we can find - * the adjacent node to pos by calling `map.floorEntry()`. + * `CRDTTreePos` represent a position in the tree. It is used to identify a + * position in the tree. It is composed of the parent ID and the left sibling + * ID. If there's no left sibling in parent's children, then left sibling is + * parent. */ export class CRDTTreePos { + private parentID: CRDTTreeNodeID; + private leftSiblingID: CRDTTreeNodeID; + + constructor(parentID: CRDTTreeNodeID, leftSiblingID: CRDTTreeNodeID) { + this.parentID = parentID; + this.leftSiblingID = leftSiblingID; + } + + /** + * `of` creates a new instance of CRDTTreePos. + */ + public static of(parentID: CRDTTreeNodeID, leftSiblingID: CRDTTreeNodeID) { + return new CRDTTreePos(parentID, leftSiblingID); + } + + /** + * `getParentID` returns the parent ID. + */ + public getParentID() { + return this.parentID; + } + + /** + * `fromStruct` creates a new instance of CRDTTreeNodeID from the given struct. + */ + public static fromStruct(struct: CRDTTreePosStruct): CRDTTreePos { + return CRDTTreePos.of( + CRDTTreeNodeID.of( + TimeTicket.fromStruct(struct.parentID.createdAt), + struct.parentID.offset, + ), + CRDTTreeNodeID.of( + TimeTicket.fromStruct(struct.leftSiblingID.createdAt), + struct.leftSiblingID.offset, + ), + ); + } + + /** + * `toStruct` returns the structure of this position. + */ + public toStruct(): CRDTTreePosStruct { + return { + parentID: { + createdAt: this.getParentID().getCreatedAt().toStruct(), + offset: this.getParentID().getOffset(), + }, + leftSiblingID: { + createdAt: this.getLeftSiblingID().getCreatedAt().toStruct(), + offset: this.getLeftSiblingID().getOffset(), + }, + }; + } + + /** + * `getLeftSiblingID` returns the left sibling ID. + */ + public getLeftSiblingID() { + return this.leftSiblingID; + } + + /** + * `equals` returns whether the given pos equals to this or not. + */ + public equals(other: CRDTTreePos): boolean { + return ( + this.getParentID() + .getCreatedAt() + .equals(other.getParentID().getCreatedAt()) && + this.getParentID().getOffset() === other.getParentID().getOffset() && + this.getLeftSiblingID() + .getCreatedAt() + .equals(other.getLeftSiblingID().getCreatedAt()) && + this.getLeftSiblingID().getOffset() === + other.getLeftSiblingID().getOffset() + ); + } +} + +/** + * `CRDTTreeNodeID` represent an ID of a node in the tree. It is used to + * identify a node in the tree. It is composed of the creation time of the node + * and the offset from the beginning of the node if the node is split. + * + * Some of replicas may have nodes that are not split yet. In this case, we can + * use `map.floorEntry()` to find the adjacent node. + */ +export class CRDTTreeNodeID { /** * `createdAt` is the creation time of the node. */ @@ -103,22 +188,40 @@ export class CRDTTreePos { } /** - * `of` creates a new instance of CRDTTreePos. + * `of` creates a new instance of CRDTTreeNodeID. */ - public static of(createdAt: TimeTicket, offset: number): CRDTTreePos { - return new CRDTTreePos(createdAt, offset); + public static of(createdAt: TimeTicket, offset: number): CRDTTreeNodeID { + return new CRDTTreeNodeID(createdAt, offset); } /** - * `fromStruct` creates a new instance of CRDTTreePos from the given struct. + * `fromStruct` creates a new instance of CRDTTreeNodeID from the given struct. */ - public static fromStruct(struct: CRDTTreePosStruct): CRDTTreePos { - return CRDTTreePos.of( + public static fromStruct(struct: CRDTTreeNodeIDStruct): CRDTTreeNodeID { + return CRDTTreeNodeID.of( TimeTicket.fromStruct(struct.createdAt), struct.offset, ); } + /** + * `createComparator` creates a comparator for CRDTTreeNodeID. + */ + public static createComparator(): Comparator { + return (idA: CRDTTreeNodeID, idB: CRDTTreeNodeID) => { + const compare = idA.getCreatedAt().compare(idB.getCreatedAt()); + if (compare !== 0) { + return compare; + } + if (idA.getOffset() > idB.getOffset()) { + return 1; + } else if (idA.getOffset() < idB.getOffset()) { + return -1; + } + return 0; + }; + } + /** * `getCreatedAt` returns the creation time of the node. */ @@ -133,10 +236,17 @@ export class CRDTTreePos { return this.offset; } + /** + * `setOffset` sets the offset of the node. + */ + public setOffset(offset: number): void { + this.offset = offset; + } + /** * `toStruct` returns the structure of this position. */ - public toStruct(): CRDTTreePosStruct { + public toStruct(): CRDTTreeNodeIDStruct { return { createdAt: this.createdAt.toStruct(), offset: this.offset, @@ -153,31 +263,20 @@ export class CRDTTreePos { /** * `CRDTTreePosStruct` represents the structure of CRDTTreePos. - * It is used to serialize and deserialize the CRDTTreePos. */ -export type CRDTTreePosStruct = { createdAt: TimeTicketStruct; offset: number }; - -/** - * `InitialCRDTTreePos` is the initial position of the tree. - */ -export const InitialCRDTTreePos = CRDTTreePos.of(InitialTimeTicket, 0); +export type CRDTTreePosStruct = { + parentID: CRDTTreeNodeIDStruct; + leftSiblingID: CRDTTreeNodeIDStruct; +}; /** - * `compareCRDTTreePos` compares the given two CRDTTreePos. + * `CRDTTreeNodeIDStruct` represents the structure of CRDTTreeNodeID. + * It is used to serialize and deserialize the CRDTTreeNodeID. */ -function compareCRDTTreePos(posA: CRDTTreePos, posB: CRDTTreePos): number { - const compare = posA.getCreatedAt().compare(posB.getCreatedAt()); - if (compare !== 0) { - return compare; - } - - if (posA.getOffset() > posB.getOffset()) { - return 1; - } else if (posA.getOffset() < posB.getOffset()) { - return -1; - } - return 0; -} +export type CRDTTreeNodeIDStruct = { + createdAt: TimeTicketStruct; + offset: number; +}; /** * `TreePosRange` represents a pair of CRDTTreePos. @@ -195,35 +294,32 @@ export type TreePosStructRange = [CRDTTreePosStruct, CRDTTreePosStruct]; * links to other nodes to resolve conflicts. */ export class CRDTTreeNode extends IndexTreeNode { - pos: CRDTTreePos; + id: CRDTTreeNodeID; removedAt?: TimeTicket; attrs?: RHT; /** - * `next` is the next node of this node in the list. - */ - next?: CRDTTreeNode; - - /** - * `prev` is the previous node of this node in the list. + * `insPrev` is the previous node of this node after the node is split. */ - prev?: CRDTTreeNode; + insPrev?: CRDTTreeNode; /** - * `insPrev` is the previous node of this node after the node is split. + * `insNext` is the previous node of this node after the node is split. */ - insPrev?: CRDTTreeNode; + insNext?: CRDTTreeNode; _value = ''; constructor( - pos: CRDTTreePos, + id: CRDTTreeNodeID, type: string, opts?: string | Array, attributes?: RHT, + removedAt?: TimeTicket, ) { super(type); - this.pos = pos; + this.id = id; + this.removedAt = removedAt; attributes && (this.attrs = attributes); if (typeof opts === 'string') { @@ -237,19 +333,19 @@ export class CRDTTreeNode extends IndexTreeNode { * `create` creates a new instance of CRDTTreeNode. */ static create( - pos: CRDTTreePos, + id: CRDTTreeNodeID, type: string, opts?: string | Array, attributes?: RHT, ) { - return new CRDTTreeNode(pos, type, opts, attributes); + return new CRDTTreeNode(id, type, opts, attributes); } /** * `deepcopy` copies itself deeply. */ deepcopy(): CRDTTreeNode { - const clone = new CRDTTreeNode(this.pos, this.type); + const clone = new CRDTTreeNode(this.id, this.type); clone.removedAt = this.removedAt; clone._value = this._value; clone.size = this.size; @@ -312,8 +408,11 @@ export class CRDTTreeNode extends IndexTreeNode { */ clone(offset: number): CRDTTreeNode { return new CRDTTreeNode( - CRDTTreePos.of(this.pos.getCreatedAt(), offset), + CRDTTreeNodeID.of(this.id.getCreatedAt(), offset), this.type, + undefined, + undefined, + this.removedAt, ); } @@ -321,7 +420,24 @@ export class CRDTTreeNode extends IndexTreeNode { * `getCreatedAt` returns the creation time of this element. */ public getCreatedAt(): TimeTicket { - return this.pos.getCreatedAt(); + return this.id.getCreatedAt(); + } + + /** + * `getOffset` returns the offset of a pos. + */ + public getOffset(): number { + return this.id.getOffset(); + } + + /** + * `canDelete` checks if node is able to delete. + */ + public canDelete(editedAt: TimeTicket, latestCreatedAt: TimeTicket): boolean { + return ( + !this.getCreatedAt().after(latestCreatedAt) && + (!this.removedAt || editedAt.after(this.removedAt)) + ); } } @@ -386,22 +502,18 @@ function toTestTreeNode(node: CRDTTreeNode): TreeNodeForTest { * `CRDTTree` is a CRDT implementation of a tree. */ export class CRDTTree extends CRDTGCElement { - private dummyHead: CRDTTreeNode; private indexTree: IndexTree; - private nodeMapByPos: LLRBTree; + private nodeMapByID: LLRBTree; private removedNodeMap: Map; constructor(root: CRDTTreeNode, createdAt: TimeTicket) { super(createdAt); - this.dummyHead = new CRDTTreeNode(InitialCRDTTreePos, DummyHeadType); this.indexTree = new IndexTree(root); - this.nodeMapByPos = new LLRBTree(compareCRDTTreePos); + this.nodeMapByID = new LLRBTree(CRDTTreeNodeID.createComparator()); this.removedNodeMap = new Map(); - let previous = this.dummyHead; this.indexTree.traverse((node) => { - this.insertAfter(previous, node); - previous = node; + this.nodeMapByID.put(node.id, node); }); } @@ -413,128 +525,63 @@ export class CRDTTree extends CRDTGCElement { } /** - * `nodesBetweenByTree` returns the nodes between the given range. - */ - public nodesBetweenByTree( - from: number, - to: number, - callback: (node: CRDTTreeNode) => void, - ): void { - this.indexTree.nodesBetween(from, to, callback); - } - - /** - * `nodesBetween` returns the nodes between the given range. - * This method includes the given left node but excludes the given right node. - */ - public nodesBetween( - left: CRDTTreeNode, - right: CRDTTreeNode, - callback: (node: CRDTTreeNode) => void, - ): void { - let current = left; - while (current !== right) { - if (!current) { - throw new Error('left and right are not in the same list'); - } - - callback(current); - current = current.next!; - } - } - - /** - * `findPostorderRight` finds the right node of the given index in postorder. - */ - public findPostorderRight(index: number): CRDTTreeNode | undefined { - const pos = this.indexTree.findTreePos(index, true); - return this.indexTree.findPostorderRight(pos); - } - - /** - * `findTreePos` finds `TreePos` of the given `CRDTTreePos` - */ - public findTreePos( - pos: CRDTTreePos, - editedAt: TimeTicket, - ): [TreePos, CRDTTreeNode] { - const treePos = this.toTreePos(pos); - if (!treePos) { - throw new Error(`cannot find node at ${pos}`); - } - - // Find the appropriate position. This logic is similar to the logical to - // handle the same position insertion of RGA. - let current = treePos; - while ( - current.node.next?.pos.getCreatedAt().after(editedAt) && - current.node.parent === current.node.next.parent - ) { - current = { - node: current.node.next, - offset: current.node.next.size, - }; - } - - const right = this.indexTree.findPostorderRight(treePos)!; - return [current, right]; - } - - /** - * `findTreePosWithSplitText` finds `TreePos` of the given `CRDTTreePos` and + * `findNodesAndSplitText` finds `TreePos` of the given `CRDTTreeNodeID` and * splits the text node if necessary. * - * `CRDTTreePos` is a position in the CRDT perspective. This is + * `CRDTTreeNodeID` is a position in the CRDT perspective. This is * different from `TreePos` which is a position of the tree in the local * perspective. */ - public findTreePosWithSplitText( + public findNodesAndSplitText( pos: CRDTTreePos, editedAt: TimeTicket, - ): [TreePos, CRDTTreeNode] { - const treePos = this.toTreePos(pos); - if (!treePos) { + ): [CRDTTreeNode, CRDTTreeNode] { + const treeNodes = this.toTreeNodes(pos); + + if (!treeNodes) { throw new Error(`cannot find node at ${pos}`); } + const [parentNode] = treeNodes; + let [, leftSiblingNode] = treeNodes; // Find the appropriate position. This logic is similar to the logical to // handle the same position insertion of RGA. - let current = treePos; - while ( - current.node.next?.pos.getCreatedAt().after(editedAt) && - current.node.parent === current.node.next.parent - ) { - current = { - node: current.node.next, - offset: current.node.next.size, - }; - } - if (current.node.isText) { - const split = current.node.split(current.offset); + if (leftSiblingNode.isText) { + const absOffset = leftSiblingNode.id.getOffset(); + const split = leftSiblingNode.split( + pos.getLeftSiblingID().getOffset() - absOffset, + absOffset, + ); + if (split) { - this.insertAfter(current.node, split); - split.insPrev = current.node; + split.insPrev = leftSiblingNode; + this.nodeMapByID.put(split.id, split); + + if (leftSiblingNode.insNext) { + leftSiblingNode.insNext.insPrev = split; + split.insNext = leftSiblingNode.insNext; + } + leftSiblingNode.insNext = split; } } - const right = this.indexTree.findPostorderRight(treePos)!; - return [current, right]; - } + const index = + parentNode === leftSiblingNode + ? 0 + : parentNode.allChildren.indexOf(leftSiblingNode) + 1; - /** - * `insertAfter` inserts the given node after the given previous node. - */ - public insertAfter(prevNode: CRDTTreeNode, newNode: CRDTTreeNode): void { - const next = prevNode.next; - prevNode.next = newNode; - newNode.prev = prevNode; - if (next) { - newNode.next = next; - next.prev = newNode; + for (let i = index; i < parentNode.allChildren.length; i++) { + const next = parentNode.allChildren[i]; + + if (next.id.getCreatedAt().after(editedAt)) { + leftSiblingNode = next; + } else { + break; + } } - this.nodeMapByPos.put(newNode.pos, newNode); + return [parentNode, leftSiblingNode]; } /** @@ -545,31 +592,51 @@ export class CRDTTree extends CRDTGCElement { attributes: { [key: string]: string } | undefined, editedAt: TimeTicket, ) { - const [, toRight] = this.findTreePos(range[1], editedAt); - const [, fromRight] = this.findTreePos(range[0], editedAt); + const [fromParent, fromLeft] = this.findNodesAndSplitText( + range[0], + editedAt, + ); + const [toParent, toLeft] = this.findNodesAndSplitText(range[1], editedAt); const changes: Array = []; changes.push({ type: TreeChangeType.Style, - from: this.toIndex(range[0]), - to: this.toIndex(range[1]), - fromPath: this.indexTree.indexToPath(this.posToStartIndex(range[0])), - toPath: this.indexTree.indexToPath(this.posToStartIndex(range[0])), + from: this.toIndex(fromParent, fromLeft), + to: this.toIndex(toParent, toLeft), + fromPath: this.toPath(fromParent, fromLeft), + toPath: this.toPath(fromParent, fromLeft), actor: editedAt.getActorID()!, value: attributes ? parseObjectValues(attributes) : undefined, }); - this.nodesBetween(fromRight, toRight, (node) => { - if (!node.isRemoved && attributes) { - if (!node.attrs) { - node.attrs = new RHT(); - } + if (fromLeft !== toLeft) { + let fromChildIndex; + let parent; + + if (fromLeft.parent === toLeft.parent) { + parent = fromLeft.parent!; + fromChildIndex = parent.allChildren.indexOf(fromLeft) + 1; + } else { + parent = fromLeft; + fromChildIndex = 0; + } + + const toChildIndex = parent.allChildren.indexOf(toLeft); - for (const [key, value] of Object.entries(attributes)) { - node.attrs.set(key, value, editedAt); + for (let i = fromChildIndex; i <= toChildIndex; i++) { + const node = parent.allChildren[i]; + + if (!node.isRemoved && attributes) { + if (!node.attrs) { + node.attrs = new RHT(); + } + + for (const [key, value] of Object.entries(attributes)) { + node.attrs.set(key, value, editedAt); + } } } - }); + } return changes; } @@ -582,23 +649,24 @@ export class CRDTTree extends CRDTGCElement { range: [CRDTTreePos, CRDTTreePos], contents: Array | undefined, editedAt: TimeTicket, - ): Array { + latestCreatedAtMapByActor?: Map, + ): [Array, Map] { // 01. split text nodes at the given range if needed. - const [toPos, toRight] = this.findTreePosWithSplitText(range[1], editedAt); - const [fromPos, fromRight] = this.findTreePosWithSplitText( + const [fromParent, fromLeft] = this.findNodesAndSplitText( range[0], editedAt, ); + const [toParent, toLeft] = this.findNodesAndSplitText(range[1], editedAt); // TODO(hackerwins): If concurrent deletion happens, we need to seperate the // range(from, to) into multiple ranges. const changes: Array = []; changes.push({ type: TreeChangeType.Content, - from: this.toIndex(range[0]), - to: this.toIndex(range[1]), - fromPath: this.indexTree.treePosToPath(fromPos), - toPath: this.indexTree.treePosToPath(toPos), + from: this.toIndex(fromParent, fromLeft), + to: this.toIndex(toParent, toLeft), + fromPath: this.toPath(fromParent, fromLeft), + toPath: this.toPath(toParent, toLeft), actor: editedAt.getActorID()!, value: contents?.length ? contents.map((content) => toJSON(content)) @@ -606,76 +674,95 @@ export class CRDTTree extends CRDTGCElement { }); const toBeRemoveds: Array = []; - // 02. remove the nodes and update linked list and index tree. - if (fromRight !== toRight) { - this.nodesBetween(fromRight!, toRight!, (node) => { - if (!node.isRemoved) { - toBeRemoveds.push(node); - } - }); + const latestCreatedAtMap = new Map(); - const isRangeOnSameBranch = toPos.node.isAncestorOf(fromPos.node); - for (const node of toBeRemoveds) { - node.remove(editedAt); + if (fromLeft !== toLeft) { + let fromChildIndex; + let parent; - if (node.isRemoved) { - this.removedNodeMap.set(node.pos.toIDString(), node); - } + if (fromLeft.parent === toLeft.parent) { + parent = fromLeft.parent!; + fromChildIndex = parent.allChildren.indexOf(fromLeft) + 1; + } else { + parent = fromLeft; + fromChildIndex = 0; } - // move the alive children of the removed element node - if (isRangeOnSameBranch) { - let removedElementNode: CRDTTreeNode | undefined; - if (fromPos.node.parent?.isRemoved) { - removedElementNode = fromPos.node.parent; - } else if (!fromPos.node.isText && fromPos.node.isRemoved) { - removedElementNode = fromPos.node; - } + const toChildIndex = parent.allChildren.indexOf(toLeft); + + for (let i = fromChildIndex; i <= toChildIndex; i++) { + const node = parent.allChildren[i]; + const actorID = node.getCreatedAt().getActorID()!; + const latestCreatedAt = latestCreatedAtMapByActor + ? latestCreatedAtMapByActor!.has(actorID!) + ? latestCreatedAtMapByActor!.get(actorID!)! + : InitialTimeTicket + : MaxTimeTicket; + + if (node.canDelete(editedAt, latestCreatedAt)) { + const latestCreatedAt = latestCreatedAtMap.get(actorID); + const createdAt = node.getCreatedAt(); - // If the nearest removed element node of the fromNode is found, - // insert the alive children of the removed element node to the toNode. - if (removedElementNode) { - const elementNode = toPos.node; - const offset = elementNode.findBranchOffset(removedElementNode); - for (const node of removedElementNode.children.reverse()) { - elementNode.insertAt(node, offset); + if (!latestCreatedAt || createdAt.after(latestCreatedAt)) { + latestCreatedAtMap.set(actorID, createdAt); } + + traverseAll(node, (node) => { + if (node.canDelete(editedAt, MaxTimeTicket)) { + const latestCreatedAt = latestCreatedAtMap.get(actorID); + const createdAt = node.getCreatedAt(); + + if (!latestCreatedAt || createdAt.after(latestCreatedAt)) { + latestCreatedAtMap.set(actorID, createdAt); + } + } + + if (!node.isRemoved) { + toBeRemoveds.push(node); + } + }); } - } else { - if (fromPos.node.parent?.isRemoved) { - toPos.node.parent?.prepend(...fromPos.node.parent.children); + } + + for (const node of toBeRemoveds) { + node.remove(editedAt); + + if (node.isRemoved) { + this.removedNodeMap.set(node.id.toIDString(), node); } } } // 03. insert the given node at the given position. if (contents?.length) { - // 03-1. insert the content nodes to the list. - let previous = fromRight!.prev!; - const node = fromPos.node; - let offset = fromPos.offset; + let leftInChildren = fromLeft; // tree for (const content of contents!) { - traverse(content, (node) => { - this.insertAfter(previous, node); - previous = node; - }); - - // 03-2. insert the content nodes to the tree. - if (node.isText) { - if (fromPos.offset === 0) { - node.parent!.insertBefore(content, node); - } else { - node.parent!.insertAfter(content, node); - } + // 03-1. insert the content nodes to the tree. + if (leftInChildren === fromParent) { + // 03-1-1. when there's no leftSibling, then insert content into very front of parent's children List + fromParent.insertAt(content, 0); } else { - node.insertAt(content, offset); - offset++; + // 03-1-2. insert after leftSibling + fromParent.insertAfter(content, leftInChildren); } + + leftInChildren = content; + traverseAll(content, (node) => { + // if insertion happens during concurrent editing and parent node has been removed, + // make new nodes as tombstone immediately + if (fromParent.isRemoved) { + node.remove(editedAt); + + this.removedNodeMap.set(node.id.toIDString(), node); + } + + this.nodeMapByID.put(node.id, node); + }); } } - return changes; + return [changes, latestCreatedAtMap]; } /** @@ -730,9 +817,9 @@ export class CRDTTree extends CRDTGCElement { [...nodesToBeRemoved].forEach((node) => { node.parent?.removeChild(node); - this.nodeMapByPos.remove(node.pos); + this.nodeMapByID.remove(node.id); this.purge(node); - this.removedNodeMap.delete(node.pos.toIDString()); + this.removedNodeMap.delete(node.id.toIDString()); }); return count; @@ -742,19 +829,19 @@ export class CRDTTree extends CRDTGCElement { * `purge` physically purges the given node from RGATreeSplit. */ public purge(node: CRDTTreeNode): void { - const prev = node.prev; - const next = node.next; + const insPrev = node.insPrev; + const insNext = node.insNext; - if (prev) { - prev.next = next; + if (insPrev) { + insPrev.insNext = insNext; } - if (next) { - next.prev = prev; + + if (insNext) { + insNext.insPrev = insPrev; } - node.prev = undefined; - node.next = undefined; node.insPrev = undefined; + node.insNext = undefined; } /** @@ -763,9 +850,32 @@ export class CRDTTree extends CRDTGCElement { public findPos(index: number, preferText = true): CRDTTreePos { const treePos = this.indexTree.findTreePos(index, preferText); + const { offset } = treePos; + let { node } = treePos; + let leftSibling; + + if (node.isText) { + if (node.parent!.children[0] === node && offset === 0) { + leftSibling = node.parent!; + } else { + leftSibling = node; + } + + node = node.parent!; + } else { + if (offset === 0) { + leftSibling = node; + } else { + leftSibling = node.children[offset - 1]; + } + } + return CRDTTreePos.of( - treePos.node.pos.getCreatedAt(), - treePos.node.pos.getOffset() + treePos.offset, + node.id, + CRDTTreeNodeID.of( + leftSibling.getCreatedAt(), + leftSibling.getOffset() + offset, + ), ); } @@ -776,37 +886,11 @@ export class CRDTTree extends CRDTGCElement { return this.removedNodeMap.size; } - /** - * `posToStartIndex` returns start index of pos - * 0 1 2 3 4 5 6 7 8 - *

t e x t

- * if tree is just like above, and the pos is pointing index of 7 - * this returns 0 (start index of tag) - */ - public posToStartIndex(pos: CRDTTreePos): number { - const treePos = this.toTreePos(pos); - const index = this.toIndex(pos); - let size = treePos?.node.size; - - if (treePos!.node.type === 'text') { - size = treePos!.node.parent?.size; - } - - return index - size! - 1; - } - /** * `pathToPosRange` converts the given path of the node to the range of the position. */ public pathToPosRange(path: Array): [CRDTTreePos, CRDTTreePos] { - const index = this.pathToIndex(path); - const { node: parentNode, offset } = this.pathToTreePos(path); - - if (parentNode.hasTextChild()) { - throw new Error('invalid Path'); - } - const node = parentNode.children[offset]; - const fromIdx = index + node.size + 1; + const fromIdx = this.pathToIndex(path); return [this.findPos(fromIdx), this.findPos(fromIdx + 1)]; } @@ -822,12 +906,9 @@ export class CRDTTree extends CRDTGCElement { * `pathToPos` finds the position of the given index in the tree by path. */ public pathToPos(path: Array): CRDTTreePos { - const treePos = this.indexTree.pathToTreePos(path); + const index = this.indexTree.pathToIndex(path); - return CRDTTreePos.of( - treePos.node.pos.getCreatedAt(), - treePos.node.pos.getOffset() + treePos.offset, - ); + return this.findPos(index); } /** @@ -889,24 +970,30 @@ export class CRDTTree extends CRDTGCElement { } /** - * `Symbol.iterator` returns the iterator of the tree. + * `toPath` converts the given CRDTTreeNodeID to the path of the tree. */ - public *[Symbol.iterator](): IterableIterator { - let node = this.dummyHead.next; - while (node) { - if (!node.isRemoved) { - yield node; - } + public toPath( + parentNode: CRDTTreeNode, + leftSiblingNode: CRDTTreeNode, + ): Array { + const treePos = this.toTreePos(parentNode, leftSiblingNode); - node = node.next; + if (!treePos) { + return []; } + + return this.indexTree.treePosToPath(treePos); } /** - * `toIndex` converts the given CRDTTreePos to the index of the tree. + * `toIndex` converts the given CRDTTreeNodeID to the index of the tree. */ - public toIndex(pos: CRDTTreePos): number { - const treePos = this.toTreePos(pos); + public toIndex( + parentNode: CRDTTreeNode, + leftSiblingNode: CRDTTreeNode, + ): number { + const treePos = this.toTreePos(parentNode, leftSiblingNode); + if (!treePos) { return -1; } @@ -914,29 +1001,88 @@ export class CRDTTree extends CRDTGCElement { return this.indexTree.indexOf(treePos); } + private toTreeNodes(pos: CRDTTreePos) { + const parentID = pos.getParentID(); + const leftSiblingID = pos.getLeftSiblingID(); + const parentEntry = this.nodeMapByID.floorEntry(parentID); + const leftSiblingEntry = this.nodeMapByID.floorEntry(leftSiblingID); + + if ( + !parentEntry || + !leftSiblingEntry || + !parentEntry.key.getCreatedAt().equals(parentID.getCreatedAt()) || + !leftSiblingEntry.key.getCreatedAt().equals(leftSiblingID.getCreatedAt()) + ) { + return []; + } + + let leftSiblingNode = leftSiblingEntry.value; + + if ( + leftSiblingID.getOffset() > 0 && + leftSiblingID.getOffset() === leftSiblingNode.id.getOffset() && + leftSiblingNode.insPrev + ) { + leftSiblingNode = leftSiblingNode.insPrev; + } + + return [parentEntry.value, leftSiblingNode]; + } + /** - * `toTreePos` converts the given CRDTTreePos to TreePos. + * `toTreePos` converts the given CRDTTreePos to local TreePos. */ - private toTreePos(pos: CRDTTreePos): TreePos | undefined { - const entry = this.nodeMapByPos.floorEntry(pos); - if (!entry || !entry.key.getCreatedAt().equals(pos.getCreatedAt())) { + private toTreePos( + parentNode: CRDTTreeNode, + leftSiblingNode: CRDTTreeNode, + ): TreePos | undefined { + if (!parentNode || !leftSiblingNode) { return; } - // Choose the left node if the position is on the boundary of the split nodes. - let node = entry.value; - if ( - pos.getOffset() > 0 && - pos.getOffset() === node.pos.getOffset() && - node.insPrev - ) { - node = node.insPrev; + let treePos; + + if (parentNode.isRemoved) { + let childNode: CRDTTreeNode; + while (parentNode.isRemoved) { + childNode = parentNode; + parentNode = childNode.parent!; + } + + const childOffset = parentNode.findOffset(childNode!); + + treePos = { + node: parentNode, + offset: childOffset, + }; + } else { + if (parentNode === leftSiblingNode) { + treePos = { + node: parentNode, + offset: 0, + }; + } else { + let offset = parentNode.findOffset(leftSiblingNode); + + if (!leftSiblingNode.isRemoved) { + if (leftSiblingNode.isText) { + return { + node: leftSiblingNode, + offset: leftSiblingNode.paddedSize, + }; + } else { + offset++; + } + } + + treePos = { + node: parentNode, + offset, + }; + } } - return { - node, - offset: pos.getOffset() - node.pos.getOffset(), - }; + return treePos; } /** @@ -971,12 +1117,12 @@ export class CRDTTree extends CRDTGCElement { range: [number, number], ): TreePosStructRange { const [fromIdx, toIdx] = range; - const fromPos = this.findPos(fromIdx).toStruct(); + const fromPos = this.findPos(fromIdx); if (fromIdx === toIdx) { - return [fromPos, fromPos]; + return [fromPos.toStruct(), fromPos.toStruct()]; } - return [fromPos, this.findPos(toIdx).toStruct()]; + return [fromPos.toStruct(), this.findPos(toIdx).toStruct()]; } /** @@ -984,10 +1130,30 @@ export class CRDTTree extends CRDTGCElement { */ public posRangeToPathRange( range: TreePosRange, + timeTicket: TimeTicket, ): [Array, Array] { - const fromPath = this.indexTree.indexToPath(this.toIndex(range[0])); - const toPath = this.indexTree.indexToPath(this.toIndex(range[1])); + const [fromParent, fromLeft] = this.findNodesAndSplitText( + range[0], + timeTicket, + ); + const [toParent, toLeft] = this.findNodesAndSplitText(range[1], timeTicket); + + return [this.toPath(fromParent, fromLeft), this.toPath(toParent, toLeft)]; + } + + /** + * `posRangeToIndexRange` converts the given position range to the path range. + */ + public posRangeToIndexRange( + range: TreePosRange, + timeTicket: TimeTicket, + ): [number, number] { + const [fromParent, fromLeft] = this.findNodesAndSplitText( + range[0], + timeTicket, + ); + const [toParent, toLeft] = this.findNodesAndSplitText(range[1], timeTicket); - return [fromPath, toPath]; + return [this.toIndex(fromParent, fromLeft), this.toIndex(toParent, toLeft)]; } } diff --git a/src/document/document.ts b/src/document/document.ts index d23276571..a2246dea4 100644 --- a/src/document/document.ts +++ b/src/document/document.ts @@ -971,7 +971,11 @@ export class Document { change.execute(this.clone!.root, this.clone!.presences); let changeInfo: ChangeInfo | undefined; - let docEvent: DocEvent

| undefined; + let presenceEvent: + | WatchedEvent

+ | UnwatchedEvent

+ | PresenceChangedEvent

+ | undefined; const actorID = change.getID().getActorID()!; if (change.hasPresenceChange() && this.onlineClients.has(actorID)) { const presenceChange = change.getPresenceChange()!; @@ -980,7 +984,7 @@ export class Document { // NOTE(chacha912): When the user exists in onlineClients, but // their presence was initially absent, we can consider that we have // received their initial presence, so trigger the 'watched' event. - docEvent = { + presenceEvent = { type: this.presences.has(actorID) ? DocEventType.PresenceChanged : DocEventType.Watched, @@ -996,7 +1000,7 @@ export class Document { // occurring before unwatching. // Detached user is no longer participating in the document, we remove // them from the online clients and trigger the 'unwatched' event. - docEvent = { + presenceEvent = { type: DocEventType.Unwatched, value: { clientID: actorID, @@ -1029,8 +1033,8 @@ export class Document { value: changeInfo, }); } - if (docEvent) { - this.publish(docEvent); + if (presenceEvent) { + this.publish(presenceEvent); } this.changeID = this.changeID.syncLamport(change.getID().getLamport()); diff --git a/src/document/json/tree.ts b/src/document/json/tree.ts index 2360e74fe..065540acf 100644 --- a/src/document/json/tree.ts +++ b/src/document/json/tree.ts @@ -1,12 +1,29 @@ +/* + * Copyright 2023 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. + */ + import { Indexable } from '@yorkie-js-sdk/src/document/document'; import { TimeTicket } from '@yorkie-js-sdk/src/document/time/ticket'; import { ChangeContext } from '@yorkie-js-sdk/src/document/change/context'; import { CRDTTree, - CRDTTreePos, + CRDTTreeNodeID, CRDTTreeNode, TreePosStructRange, TreeChange, + CRDTTreePos, } from '@yorkie-js-sdk/src/document/crdt/tree'; import { @@ -59,7 +76,7 @@ function buildDescendants( validateTextNode(treeNode as TextNode); const { value } = treeNode as TextNode; const textNode = CRDTTreeNode.create( - CRDTTreePos.of(ticket, 0), + CRDTTreeNodeID.of(ticket, 0), type, value, ); @@ -79,7 +96,7 @@ function buildDescendants( } } const elementNode = CRDTTreeNode.create( - CRDTTreePos.of(ticket, 0), + CRDTTreeNodeID.of(ticket, 0), type, undefined, attrs, @@ -103,7 +120,7 @@ function createCRDTTreeNode(context: ChangeContext, content: TreeNode) { let root; if (content.type === DefaultTextType) { const { value } = content as TextNode; - root = CRDTTreeNode.create(CRDTTreePos.of(ticket, 0), type, value); + root = CRDTTreeNode.create(CRDTTreeNodeID.of(ticket, 0), type, value); } else if (content) { const { children = [] } = content as ElementNode; let { attributes } = content as ElementNode; @@ -119,7 +136,7 @@ function createCRDTTreeNode(context: ChangeContext, content: TreeNode) { } root = CRDTTreeNode.create( - CRDTTreePos.of(context.issueTimeTicket(), 0), + CRDTTreeNodeID.of(context.issueTimeTicket(), 0), type, undefined, attrs, @@ -210,14 +227,14 @@ export class Tree { public buildRoot(context: ChangeContext): CRDTTreeNode { if (!this.initialRoot) { return CRDTTreeNode.create( - CRDTTreePos.of(context.issueTimeTicket(), 0), + CRDTTreeNodeID.of(context.issueTimeTicket(), 0), DefaultRootType, ); } // TODO(hackerwins): Need to use the ticket of operation of creating tree. const root = CRDTTreeNode.create( - CRDTTreePos.of(context.issueTimeTicket(), 0), + CRDTTreeNodeID.of(context.issueTimeTicket(), 0), this.initialRoot.type, ); @@ -261,7 +278,6 @@ export class Tree { if (!path.length) { throw new Error('path should not be empty'); } - const [fromPos, toPos] = this.tree.pathToPosRange(path); const ticket = this.context.issueTimeTicket(); const attrs = attributes ? stringifyObjectValues(attributes) : undefined; @@ -339,7 +355,7 @@ export class Tree { } crdtNodes.push( CRDTTreeNode.create( - CRDTTreePos.of(this.context!.issueTimeTicket(), 0), + CRDTTreeNodeID.of(this.context!.issueTimeTicket(), 0), DefaultTextType, compVal, ), @@ -350,7 +366,7 @@ export class Tree { .filter((a) => a) as Array; } - this.tree!.edit( + const [, maxCreatedAtMapByActor] = this.tree!.edit( [fromPos, toPos], crdtNodes.length ? crdtNodes.map((crdtNode) => crdtNode?.deepcopy()) @@ -363,15 +379,13 @@ export class Tree { this.tree!.getCreatedAt(), fromPos, toPos, + maxCreatedAtMapByActor, crdtNodes.length ? crdtNodes : undefined, ticket, ), ); - if ( - !fromPos.getCreatedAt().equals(toPos.getCreatedAt()) || - fromPos.getOffset() !== toPos.getOffset() - ) { + if (!fromPos.equals(toPos)) { this.context!.registerElementHasRemovedNodes(this.tree!); } @@ -479,33 +493,6 @@ export class Tree { return this.tree.pathToIndex(path); } - /** - * eslint-disable-next-line jsdoc/require-jsdoc - * @internal - */ - public *[Symbol.iterator](): IterableIterator { - if (!this.tree) { - return; - } - - // TODO(hackerwins): Fill children of element node later. - for (const node of this.tree) { - if (node.isText) { - const textNode = node as TextNode; - yield { - type: textNode.type, - value: textNode.value, - }; - } else { - const elementNode = node as ElementNode; - yield { - type: elementNode.type, - children: [], - }; - } - } - } - /** * `pathRangeToPosRange` converts the path range into the position range. */ @@ -554,7 +541,10 @@ export class Tree { CRDTTreePos.fromStruct(range[1]), ]; - return [this.tree.toIndex(posRange[0]), this.tree.toIndex(posRange[1])]; + return this.tree.posRangeToIndexRange( + posRange, + this.context.getLastTimeTicket(), + ); } /** @@ -574,6 +564,9 @@ export class Tree { CRDTTreePos.fromStruct(range[1]), ]; - return this.tree.posRangeToPathRange(posRange); + return this.tree.posRangeToPathRange( + posRange, + this.context.getLastTimeTicket(), + ); } } diff --git a/src/document/operation/tree_edit_operation.ts b/src/document/operation/tree_edit_operation.ts index 636aed6d4..a2a9dc216 100644 --- a/src/document/operation/tree_edit_operation.ts +++ b/src/document/operation/tree_edit_operation.ts @@ -34,11 +34,13 @@ export class TreeEditOperation extends Operation { private fromPos: CRDTTreePos; private toPos: CRDTTreePos; private contents: Array | undefined; + private maxCreatedAtMapByActor: Map; constructor( parentCreatedAt: TimeTicket, fromPos: CRDTTreePos, toPos: CRDTTreePos, + maxCreatedAtMapByActor: Map, contents: Array | undefined, executedAt: TimeTicket, ) { @@ -46,6 +48,7 @@ export class TreeEditOperation extends Operation { this.fromPos = fromPos; this.toPos = toPos; this.contents = contents; + this.maxCreatedAtMapByActor = maxCreatedAtMapByActor; } /** @@ -55,6 +58,7 @@ export class TreeEditOperation extends Operation { parentCreatedAt: TimeTicket, fromPos: CRDTTreePos, toPos: CRDTTreePos, + maxCreatedAtMapByActor: Map, contents: Array | undefined, executedAt: TimeTicket, ): TreeEditOperation { @@ -62,6 +66,7 @@ export class TreeEditOperation extends Operation { parentCreatedAt, fromPos, toPos, + maxCreatedAtMapByActor, contents, executedAt, ); @@ -79,16 +84,14 @@ export class TreeEditOperation extends Operation { logger.fatal(`fail to execute, only Tree can execute edit`); } const tree = parentObject as CRDTTree; - const changes = tree.edit( + const [changes] = tree.edit( [this.fromPos, this.toPos], this.contents?.map((content) => content.deepcopy()), this.getExecutedAt(), + this.maxCreatedAtMapByActor, ); - if ( - !this.fromPos.getCreatedAt().equals(this.toPos.getCreatedAt()) || - this.fromPos.getOffset() !== this.toPos.getOffset() - ) { + if (!this.fromPos.equals(this.toPos)) { root.registerElementHasRemovedNodes(tree); } return changes.map(({ from, to, value, fromPath, toPath }) => { @@ -117,11 +120,13 @@ export class TreeEditOperation extends Operation { public toTestString(): string { const parent = this.getParentCreatedAt().toTestString(); const fromPos = `${this.fromPos + .getLeftSiblingID() .getCreatedAt() - .toTestString()}:${this.fromPos.getOffset()}`; + .toTestString()}:${this.fromPos.getLeftSiblingID().getOffset()}`; const toPos = `${this.toPos + .getLeftSiblingID() .getCreatedAt() - .toTestString()}:${this.toPos.getOffset()}`; + .toTestString()}:${this.toPos.getLeftSiblingID().getOffset()}`; const contents = this.contents; return `${parent}.EDIT(${fromPos},${toPos},${contents?.join('')})`; } @@ -146,4 +151,12 @@ export class TreeEditOperation extends Operation { public getContents(): Array | undefined { return this.contents; } + + /** + * `getMaxCreatedAtMapByActor` returns the map that stores the latest creation time + * by actor for the nodes included in the editing range. + */ + public getMaxCreatedAtMapByActor(): Map { + return this.maxCreatedAtMapByActor; + } } diff --git a/src/document/operation/tree_style_operation.ts b/src/document/operation/tree_style_operation.ts index 8de19b5cc..15fb2f31e 100644 --- a/src/document/operation/tree_style_operation.ts +++ b/src/document/operation/tree_style_operation.ts @@ -111,11 +111,13 @@ export class TreeStyleOperation extends Operation { public toTestString(): string { const parent = this.getParentCreatedAt().toTestString(); const fromPos = `${this.fromPos + .getLeftSiblingID() .getCreatedAt() - .toTestString()}:${this.fromPos.getOffset()}`; + .toTestString()}:${this.fromPos.getLeftSiblingID().getOffset()}`; const toPos = `${this.toPos + .getLeftSiblingID() .getCreatedAt() - .toTestString()}:${this.toPos.getOffset()}`; + .toTestString()}:${this.toPos.getLeftSiblingID().getOffset()}`; return `${parent}.STYLE(${fromPos},${toPos},${Object.entries( this.attributes || {}, diff --git a/src/util/index_tree.ts b/src/util/index_tree.ts index c183732ec..83aed8a3a 100644 --- a/src/util/index_tree.ts +++ b/src/util/index_tree.ts @@ -91,7 +91,7 @@ export type TreeNodeType = string; /** * `addSizeOfLeftSiblings` returns the size of left siblings of the given offset. */ -function addSizeOfLeftSiblings>( +export function addSizeOfLeftSiblings>( parent: T, offset: number, ): number { @@ -99,6 +99,11 @@ function addSizeOfLeftSiblings>( for (let i = 0; i < offset; i++) { const leftSibling = parent.children[i]; + + if (!leftSibling || leftSibling.isRemoved) { + continue; + } + acc += leftSibling.paddedSize; } @@ -177,9 +182,9 @@ export abstract class IndexTreeNode> { /** * `split` splits the node at the given offset. */ - split(offset: number): T | undefined { + split(offset: number, absOffset: number): T | undefined { if (this.isText) { - return this.splitText(offset); + return this.splitText(offset, absOffset); } return this.splitElement(offset); @@ -208,7 +213,7 @@ export abstract class IndexTreeNode> { /** * `splitText` splits the given node at the given offset. */ - splitText(offset: number): T | undefined { + splitText(offset: number, absOffset: number): T | undefined { if (offset === 0 || offset === this.size) { return; } @@ -216,10 +221,15 @@ export abstract class IndexTreeNode> { const leftValue = this.value.slice(0, offset); const rightValue = this.value.slice(offset); + if (!rightValue.length) { + return; + } + this.value = leftValue; - const rightNode = this.clone(offset); + const rightNode = this.clone(offset + absOffset); rightNode.value = rightValue; + this.parent!.insertAfterInternal(rightNode, this as any); return rightNode; @@ -235,6 +245,14 @@ export abstract class IndexTreeNode> { return this._children.filter((child) => !child.isRemoved); } + /** + * `allChildren` returns all the children of the node including tombstone nodes. + * It returns the shallow copy of the children. + */ + get allChildren(): Array { + return [...this._children]; + } + /** * `hasTextChild` returns true if the node has an text child. */ @@ -268,7 +286,10 @@ export abstract class IndexTreeNode> { this._children.unshift(...newNode); for (const node of newNode) { node.parent = this as any; - node.updateAncestorsSize(); + + if (!node.isRemoved) { + node.updateAncestorsSize(); + } } } @@ -401,6 +422,18 @@ export abstract class IndexTreeNode> { throw new Error('Text node cannot have children'); } + if (node.isRemoved) { + const index = this._children.indexOf(node); + + // If nodes are removed, the offset of the removed node is the number of + // nodes before the node excluding the removed nodes. + const refined = + this.allChildren.splice(0, index).filter((node) => !node.isRemoved) + .length - 1; + + return refined < 0 ? 0 : refined; + } + return this.children.indexOf(node); } @@ -526,7 +559,7 @@ export function traverse>( /** * `traverseAll` traverses the whole tree (include tombstones) with postorder traversal. */ -function traverseAll>( +export function traverseAll>( node: T, callback: (node: T, depth: number) => void, depth = 0, @@ -705,7 +738,7 @@ export class IndexTree> { let node: T | undefined = treePos.node; let offset: number = treePos.offset; for (let i = 0; i < depth && node && node !== this.root; i++) { - node.split(offset); + node.split(offset, 0); const nextOffset = node.parent!.findOffset(node); offset = offset === 0 ? nextOffset : nextOffset + 1; diff --git a/src/yorkie.ts b/src/yorkie.ts index 87c3c4ec7..91d4383d6 100644 --- a/src/yorkie.ts +++ b/src/yorkie.ts @@ -30,7 +30,7 @@ export { DocumentSyncResultType, ClientEventType, StatusChangedEvent, - DocumentsChangedEvent, + DocumentChangedEvent, StreamConnectionStatusChangedEvent, DocumentSyncedEvent, ClientOptions, @@ -78,8 +78,8 @@ export type { export { TreeChange, TreeChangeType, - CRDTTreePosStruct, - TreePosStructRange, + CRDTTreeNodeIDStruct, + TreePosStructRange as TreeRangeStruct, } from '@yorkie-js-sdk/src/document/crdt/tree'; export { diff --git a/test/integration/tree_test.ts b/test/integration/tree_test.ts index 08494a2aa..ca7a5f837 100644 --- a/test/integration/tree_test.ts +++ b/test/integration/tree_test.ts @@ -15,99 +15,13 @@ */ import { assert } from 'chai'; -import yorkie, { - Document, - Tree, - TreeNode, - ElementNode, - Indexable, -} from '@yorkie-js-sdk/src/yorkie'; -import { ChangePack } from '@yorkie-js-sdk/src/document/change/change_pack'; -import { Checkpoint } from '@yorkie-js-sdk/src/document/change/checkpoint'; +import yorkie, { Tree } from '@yorkie-js-sdk/src/yorkie'; import { toDocKey, withTwoClientsAndDocuments, } from '@yorkie-js-sdk/test/integration/integration_helper'; import { TreeEditOpInfo } from '@yorkie-js-sdk/src/document/operation/operation'; -/** - * `listEqual` is a helper function that the given tree is equal to the - * expected list of nodes. - */ -function listEqual(tree: Tree, expected: Array) { - const nodes: Array = []; - for (const node of tree) { - nodes.push(node); - } - assert.deepEqual(nodes, expected); -} - -/** - * `createChangePack` is a helper function that creates a change pack from the - * given document. It is used to to emulate the behavior of the server. - */ -function createChangePack( - doc: Document, -): ChangePack { - // 01. Create a change pack from the given document and emulate the behavior - // of PushPullChanges API. - const reqPack = doc.createChangePack(); - const reqCP = reqPack.getCheckpoint(); - const resPack = ChangePack.create( - reqPack.getDocumentKey(), - Checkpoint.of( - reqCP.getServerSeq().add(reqPack.getChangeSize()), - reqCP.getClientSeq() + reqPack.getChangeSize(), - ), - false, - [], - ); - doc.applyChangePack(resPack); - - // 02. Create a pack to apply the changes to other replicas. - return ChangePack.create( - reqPack.getDocumentKey(), - Checkpoint.of(reqCP.getServerSeq().add(reqPack.getChangeSize()), 0), - false, - reqPack.getChanges(), - ); -} - -/** - * `createTwoDocuments` is a helper function that creates two documents with - * the given initial tree. - */ -function createTwoTreeDocs( - key: string, - initial: ElementNode, -): [Document, Document] { - const doc1 = new yorkie.Document(key); - const doc2 = new yorkie.Document(key); - doc1.setActor('A'); - doc2.setActor('B'); - - doc1.update((root) => (root.t = new Tree(initial))); - doc2.applyChangePack(createChangePack(doc1)); - - return [doc1, doc2]; -} - -/** - * `syncTwoTreeDocsAndAssertEqual` is a helper function that syncs two documents - * and asserts that the given expected tree is equal to the two documents. - */ -function syncTwoTreeDocsAndAssertEqual( - doc1: Document, - doc2: Document, - expected: string, -) { - doc2.applyChangePack(createChangePack(doc1)); - doc1.applyChangePack(createChangePack(doc2)); - - assert.equal(doc1.getRoot().t.toXML(), doc2.getRoot().t.toXML()); - assert.equal(doc1.getRoot().t.toXML(), expected); -} - describe('Tree', () => { it('Can be created', function () { const key = toDocKey(`${this.test!.title}-${new Date().getTime()}`); @@ -132,6 +46,7 @@ describe('Tree', () => { ); // 03. Insert a text into the paragraph. + root.t.edit(3, 3, { type: 'text', value: 'CD' }); assert.equal(root.t.toXML(), /*html*/ `

ABCD

`); assert.equal( @@ -182,18 +97,33 @@ describe('Tree', () => { /*html*/ `

ab

cdefgh
`, ); assert.equal(root.t.getSize(), 18); - listEqual(root.t, [ - { type: 'text', value: 'ab' }, - { type: 'p', children: [] }, - { type: 'text', value: 'cd' }, - { type: 'note', children: [] }, - { type: 'text', value: 'ef' }, - { type: 'note', children: [] }, - { type: 'ng', children: [] }, - { type: 'text', value: 'gh' }, - { type: 'bp', children: [] }, - { type: 'doc', children: [] }, - ]); + }); + }); + + it('Can be created from JSON with attrebutes', function () { + const key = toDocKey(`${this.test!.title}-${new Date().getTime()}`); + const doc = new yorkie.Document<{ t: Tree }>(key); + + doc.update((root) => { + root.t = new Tree({ + type: 'doc', + children: [ + { + type: 'p', + children: [ + { + type: 'span', + attributes: { bold: true }, + children: [{ type: 'text', value: 'hello' }], + }, + ], + }, + ], + }); + assert.equal( + root.t.toXML(), + /*html*/ `

hello

`, + ); }); }); @@ -222,6 +152,21 @@ describe('Tree', () => { }); assert.equal(doc.getRoot().t.toXML(), /*html*/ `

ab

`); + doc.update((root) => { + root.t = new Tree({ + type: 'doc', + children: [{ type: 'p', children: [{ type: 'text', value: 'ab' }] }], + }); + assert.equal(root.t.toXML(), /*html*/ `

ab

`); + + root.t.edit(2, 2, { type: 'text', value: 'X' }); + assert.equal(root.t.toXML(), /*html*/ `

aXb

`); + + root.t.edit(1, 4); + assert.equal(root.t.toXML(), /*html*/ `

`); + }); + assert.equal(doc.getRoot().t.toXML(), /*html*/ `

`); + doc.update((root) => { root.t = new Tree({ type: 'doc', @@ -554,7 +499,7 @@ describe('Tree', () => { }); }); - it('Can sync its content with other replicas', async function () { + it('Can sync its content with other clients', async function () { await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { d1.update((root) => { root.t = new Tree({ @@ -625,6 +570,44 @@ describe('Tree', () => { range = tree.indexRangeToPosRange([5, 7]); assert.deepEqual(tree.posRangeToIndexRange(range), [5, 7]); }); + + it('Get correct range from path', function () { + const key = toDocKey(`${this.test!.title}-${new Date().getTime()}`); + const doc = new yorkie.Document<{ t: Tree }>(key); + + doc.update((root) => { + root.t = new Tree({ + type: 'root', + children: [ + { + type: 'p', + children: [ + { + type: 'b', + children: [ + { type: 'i', children: [{ type: 'text', value: 'ab' }] }, + ], + }, + ], + }, + ], + }); + }); + + const tree = doc.getRoot().t; + // 0 1 2 3 4 5 6 7 8 + //

a b

+ assert.deepEqual( + tree.toXML(), + /*html*/ `

ab

`, + ); + + let range = tree.pathRangeToPosRange([[0], [0, 0, 0, 2]]); + assert.deepEqual(tree.posRangeToPathRange(range), [[0], [0, 0, 0, 2]]); + + range = tree.pathRangeToPosRange([[0], [1]]); + assert.deepEqual(tree.posRangeToPathRange(range), [[0], [1]]); + }); }); describe('Tree.edit', function () { @@ -687,6 +670,112 @@ describe('Tree.edit', function () { ); }); + it('Can edit its content with path when multi tree nodes passed', function () { + const key = toDocKey(`${this.test!.title}-${new Date().getTime()}`); + const doc = new yorkie.Document<{ t: Tree }>(key); + + doc.update((root) => { + root.t = new Tree({ + type: 'doc', + children: [ + { + type: 'tc', + children: [ + { + type: 'p', + children: [ + { type: 'tn', children: [{ type: 'text', value: 'ab' }] }, + ], + }, + ], + }, + ], + }); + assert.equal( + root.t.toXML(), + /*html*/ `

ab

`, + ); + + root.t.editByPath( + [0, 0, 0, 1], + [0, 0, 0, 1], + { + type: 'text', + value: 'X', + }, + { + type: 'text', + value: 'X', + }, + ); + assert.equal( + root.t.toXML(), + /*html*/ `

aXXb

`, + ); + + root.t.editByPath( + [0, 1], + [0, 1], + { + type: 'p', + children: [ + { + type: 'tn', + children: [ + { type: 'text', value: 'te' }, + { type: 'text', value: 'st' }, + ], + }, + ], + }, + { + type: 'p', + children: [ + { + type: 'tn', + children: [ + { type: 'text', value: 'te' }, + { type: 'text', value: 'xt' }, + ], + }, + ], + }, + ); + assert.equal( + root.t.toXML(), + /*html*/ `

aXXb

test

text

`, + ); + + root.t.editByPath( + [0, 3], + [0, 3], + { + type: 'p', + children: [ + { + type: 'tn', + children: [ + { type: 'text', value: 'te' }, + { type: 'text', value: 'st' }, + ], + }, + ], + }, + { + type: 'tn', + children: [ + { type: 'text', value: 'te' }, + { type: 'text', value: 'xt' }, + ], + }, + ); + assert.equal( + root.t.toXML(), + /*html*/ `

aXXb

test

text

test

text
`, + ); + }); + }); + it('Detecting error for empty text', function () { const key = toDocKey(`${this.test!.title}-${new Date().getTime()}`); const doc = new yorkie.Document<{ t: Tree }>(key); @@ -832,51 +921,6 @@ describe('Tree.edit', function () { }); }, 'element node and text node cannot be passed together'); }); - - it.skip('Can insert text to the same position(left) concurrently', function () { - const [docA, docB] = createTwoTreeDocs(toDocKey(this.test!.title), { - type: 'r', - children: [{ type: 'p', children: [{ type: 'text', value: '12' }] }], - }); - assert.equal(docA.getRoot().t.toXML(), /*html*/ `

12

`); - - docA.update((r) => r.t.edit(1, 1, { type: 'text', value: 'A' })); - docB.update((r) => r.t.edit(1, 1, { type: 'text', value: 'B' })); - assert.equal(docA.getRoot().t.toXML(), /*html*/ `

A12

`); - assert.equal(docB.getRoot().t.toXML(), /*html*/ `

B12

`); - - syncTwoTreeDocsAndAssertEqual(docA, docB, /*html*/ `

BA12

`); - }); - - it('Can insert text to the same position(middle) concurrently', function () { - const [docA, docB] = createTwoTreeDocs(toDocKey(this.test!.title), { - type: 'r', - children: [{ type: 'p', children: [{ type: 'text', value: '12' }] }], - }); - assert.equal(docA.getRoot().t.toXML(), /*html*/ `

12

`); - - docA.update((r) => r.t.edit(2, 2, { type: 'text', value: 'A' })); - docB.update((r) => r.t.edit(2, 2, { type: 'text', value: 'B' })); - assert.equal(docA.getRoot().t.toXML(), /*html*/ `

1A2

`); - assert.equal(docB.getRoot().t.toXML(), /*html*/ `

1B2

`); - - syncTwoTreeDocsAndAssertEqual(docA, docB, /*html*/ `

1BA2

`); - }); - - it('Can insert text content to the same position(right) concurrently', function () { - const [docA, docB] = createTwoTreeDocs(toDocKey(this.test!.title), { - type: 'r', - children: [{ type: 'p', children: [{ type: 'text', value: '12' }] }], - }); - assert.equal(docA.getRoot().t.toXML(), /*html*/ `

12

`); - - docA.update((r) => r.t.edit(3, 3, { type: 'text', value: 'A' })); - docB.update((r) => r.t.edit(3, 3, { type: 'text', value: 'B' })); - assert.equal(docA.getRoot().t.toXML(), /*html*/ `

12A

`); - assert.equal(docB.getRoot().t.toXML(), /*html*/ `

12B

`); - - syncTwoTreeDocsAndAssertEqual(docA, docB, /*html*/ `

12BA

`); - }); }); describe('Tree.style', function () { @@ -932,19 +976,19 @@ describe('Tree.style', function () { /*html*/ `

`, ); - root.t.style(4, 5, { c: 'd' }); + root.t.style(1, 2, { c: 'd' }); assert.equal( root.t.toXML(), /*html*/ `

`, ); - root.t.style(4, 5, { c: 'q' }); + root.t.style(1, 2, { c: 'q' }); assert.equal( root.t.toXML(), /*html*/ `

`, ); - root.t.style(3, 4, { z: 'm' }); + root.t.style(2, 3, { z: 'm' }); assert.equal( root.t.toXML(), /*html*/ `

`, @@ -1029,7 +1073,7 @@ describe('Tree.style', function () { ); d1.update((root) => { - root.t.style(6, 7, { bold: 'true' }); + root.t.style(0, 1, { bold: 'true' }); }); await c1.sync(); await c2.sync(); @@ -1046,3 +1090,1256 @@ describe('Tree.style', function () { }, this.test!.title); }); }); + +describe('Concurrent editing, overlapping range', () => { + it('Can concurrently delete overlapping elements', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { type: 'p', children: [] }, + { type: 'i', children: [] }, + { type: 'b', children: [] }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

`, + ); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

`, + ); + + d1.update((r) => r.t.edit(0, 4)); + d2.update((r) => r.t.edit(2, 6)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ ``); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ ``); + assert.equal(d2.getRoot().t.toXML(), /*html*/ ``); + }, this.test!.title); + }); + + it('Can concurrently delete overlapping text', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { type: 'p', children: [{ type: 'text', value: 'abcd' }] }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

abcd

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

abcd

`); + + d1.update((r) => r.t.edit(1, 4)); + d2.update((r) => r.t.edit(2, 5)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

d

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

a

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + }, this.test!.title); + }); +}); + +describe('Concurrent editing, contained range', () => { + it('Can concurrently insert and delete contained elements of the same depth', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { type: 'p', children: [{ type: 'text', value: '1234' }] }, + { type: 'p', children: [{ type: 'text', value: 'abcd' }] }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

1234

abcd

`, + ); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

1234

abcd

`, + ); + + d1.update((r) => r.t.edit(6, 6, { type: 'p', children: [] })); + d2.update((r) => r.t.edit(0, 12)); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

1234

abcd

`, + ); + assert.equal(d2.getRoot().t.toXML(), /*html*/ ``); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + }, this.test!.title); + }); + + it('Can concurrently multiple insert and delete contained elements of the same depth', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { type: 'p', children: [{ type: 'text', value: '1234' }] }, + { type: 'p', children: [{ type: 'text', value: 'abcd' }] }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

1234

abcd

`, + ); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

1234

abcd

`, + ); + + d1.update((r) => r.t.edit(6, 6, { type: 'p', children: [] })); + d1.update((r) => r.t.edit(8, 8, { type: 'p', children: [] })); + d1.update((r) => r.t.edit(10, 10, { type: 'p', children: [] })); + d1.update((r) => r.t.edit(12, 12, { type: 'p', children: [] })); + d2.update((r) => r.t.edit(0, 12)); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

1234

abcd

`, + ); + assert.equal(d2.getRoot().t.toXML(), /*html*/ ``); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

`, + ); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

`, + ); + }, this.test!.title); + }); + + it('Detecting error when inserting and deleting contained elements at different depths', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [{ type: 'p', children: [{ type: 'i', children: [] }] }], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + d1.update((r) => r.t.edit(2, 2, { type: 'i', children: [] })); + d2.update((r) => r.t.edit(1, 3)); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

`, + ); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + }, this.test!.title); + }); + + it('Can concurrently delete contained elements', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { + type: 'p', + children: [ + { type: 'i', children: [{ type: 'text', value: '1234' }] }, + ], + }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

1234

`, + ); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

1234

`, + ); + + d1.update((r) => r.t.edit(0, 8)); + d2.update((r) => r.t.edit(1, 7)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ ``); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ ``); + assert.equal(d2.getRoot().t.toXML(), /*html*/ ``); + }, this.test!.title); + }); + + it('Can concurrently insert and delete contained text', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { + type: 'p', + children: [{ type: 'text', value: '1234' }], + }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

1234

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

1234

`); + + d1.update((r) => r.t.edit(1, 5)); + d2.update((r) => r.t.edit(3, 3, { type: 'text', value: 'a' })); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12a34

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

a

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

a

`); + }, this.test!.title); + }); + + it('Can concurrently delete contained text', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { + type: 'p', + children: [{ type: 'text', value: '1234' }], + }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

1234

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

1234

`); + + d1.update((r) => r.t.edit(1, 5)); + d2.update((r) => r.t.edit(2, 4)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

14

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + }, this.test!.title); + }); + + it('Can concurrently insert and delete contained text and elements', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { + type: 'p', + children: [{ type: 'text', value: '1234' }], + }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

1234

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

1234

`); + + d1.update((r) => r.t.edit(0, 6)); + d2.update((r) => r.t.edit(3, 3, { type: 'text', value: 'a' })); + assert.equal(d1.getRoot().t.toXML(), /*html*/ ``); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12a34

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ ``); + assert.equal(d2.getRoot().t.toXML(), /*html*/ ``); + }, this.test!.title); + }); + + it('Can concurrently delete contained text and elements', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { + type: 'p', + children: [{ type: 'text', value: '1234' }], + }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

1234

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

1234

`); + + d1.update((r) => r.t.edit(0, 6)); + d2.update((r) => r.t.edit(1, 5)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ ``); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ ``); + assert.equal(d2.getRoot().t.toXML(), /*html*/ ``); + }, this.test!.title); + }); +}); + +describe('Concurrent editing, side by side range', () => { + it('Can concurrently insert side by side elements (left)', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { + type: 'p', + children: [], + }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + d1.update((r) => r.t.edit(0, 0, { type: 'b', children: [] })); + d2.update((r) => r.t.edit(0, 0, { type: 'i', children: [] })); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

`, + ); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

`, + ); + }, this.test!.title); + }); + + it('Can concurrently insert side by side elements (middle)', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { + type: 'p', + children: [], + }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + d1.update((r) => r.t.edit(1, 1, { type: 'b', children: [] })); + d2.update((r) => r.t.edit(1, 1, { type: 'i', children: [] })); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

`, + ); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

`, + ); + }, this.test!.title); + }); + + it('Can concurrently insert side by side elements (right)', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { + type: 'p', + children: [], + }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + d1.update((r) => r.t.edit(2, 2, { type: 'b', children: [] })); + d2.update((r) => r.t.edit(2, 2, { type: 'i', children: [] })); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

`, + ); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

`, + ); + }, this.test!.title); + }); + + it('Can concurrently insert and delete side by side elements', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { + type: 'p', + children: [{ type: 'b', children: [] }], + }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + d1.update((r) => r.t.edit(1, 3)); + d2.update((r) => r.t.edit(1, 1, { type: 'i', children: [] })); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

`, + ); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + }, this.test!.title); + }); + + it('Can concurrently delete and insert side by side elements', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { + type: 'p', + children: [{ type: 'b', children: [] }], + }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + d1.update((r) => r.t.edit(1, 3)); + d2.update((r) => r.t.edit(3, 3, { type: 'i', children: [] })); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

`, + ); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + }, this.test!.title); + }); + + it('Can concurrently delete side by side elements', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { + type: 'p', + children: [ + { type: 'b', children: [] }, + { type: 'i', children: [] }, + ], + }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

`, + ); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

`, + ); + + d1.update((r) => r.t.edit(1, 3)); + d2.update((r) => r.t.edit(3, 5)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + }, this.test!.title); + }); + + it('Can insert text to the same position(left) concurrently', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [{ type: 'p', children: [{ type: 'text', value: '12' }] }], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12

`); + + d1.update((r) => r.t.edit(1, 1, { type: 'text', value: 'A' })); + d2.update((r) => r.t.edit(1, 1, { type: 'text', value: 'B' })); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

A12

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

B12

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

BA12

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

BA12

`); + }, this.test!.title); + }); + + it('Can insert text to the same position(middle) concurrently', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [{ type: 'p', children: [{ type: 'text', value: '12' }] }], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12

`); + + d1.update((r) => r.t.edit(2, 2, { type: 'text', value: 'A' })); + d2.update((r) => r.t.edit(2, 2, { type: 'text', value: 'B' })); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

1A2

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

1B2

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

1BA2

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

1BA2

`); + }, this.test!.title); + }); + + it('Can insert text content to the same position(right) concurrently', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [{ type: 'p', children: [{ type: 'text', value: '12' }] }], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12

`); + + d1.update((r) => r.t.edit(3, 3, { type: 'text', value: 'A' })); + d2.update((r) => r.t.edit(3, 3, { type: 'text', value: 'B' })); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12A

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12B

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12BA

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12BA

`); + }, this.test!.title); + }); + + it('Can concurrently insert and delete side by side text', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { type: 'p', children: [{ type: 'text', value: '1234' }] }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

1234

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

1234

`); + + d1.update((r) => r.t.edit(3, 3, { type: 'text', value: 'a' })); + d2.update((r) => r.t.edit(3, 5)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12a34

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12a

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12a

`); + }, this.test!.title); + }); + + it('Can concurrently delete and insert side by side text', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { type: 'p', children: [{ type: 'text', value: '1234' }] }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

1234

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

1234

`); + + d1.update((r) => r.t.edit(3, 3, { type: 'text', value: 'a' })); + d2.update((r) => r.t.edit(1, 3)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12a34

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

34

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

a34

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

a34

`); + }, this.test!.title); + }); + + it('Can concurrently delete side by side text blocks', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { type: 'p', children: [{ type: 'text', value: '1234' }] }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

1234

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

1234

`); + + d1.update((r) => r.t.edit(3, 5)); + d2.update((r) => r.t.edit(1, 3)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

34

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + }, this.test!.title); + }); + + it('Can delete text content at the same position(left) concurrently', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [{ type: 'p', children: [{ type: 'text', value: '123' }] }], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

123

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

123

`); + + d1.update((r) => r.t.edit(1, 2)); + d2.update((r) => r.t.edit(1, 2)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

23

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

23

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

23

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

23

`); + }, this.test!.title); + }); + + it('Can delete text content at the same position(middle) concurrently', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [{ type: 'p', children: [{ type: 'text', value: '123' }] }], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

123

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

123

`); + + d1.update((r) => r.t.edit(2, 3)); + d2.update((r) => r.t.edit(2, 3)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

13

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

13

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

13

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

13

`); + }, this.test!.title); + }); + + it('Can delete text content at the same position(right) concurrently', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [{ type: 'p', children: [{ type: 'text', value: '123' }] }], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

123

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

123

`); + + d1.update((r) => r.t.edit(3, 4)); + d2.update((r) => r.t.edit(3, 4)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12

`); + }, this.test!.title); + }); +}); + +describe('Concurrent editing, complex cases', () => { + it('Can delete text content anchored to another concurrently', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [{ type: 'p', children: [{ type: 'text', value: '123' }] }], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

123

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

123

`); + + d1.update((r) => r.t.edit(1, 2)); + d2.update((r) => r.t.edit(2, 3)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

23

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

13

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

3

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

3

`); + }, this.test!.title); + }); + + it('Can produce complete deletion concurrently', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [{ type: 'p', children: [{ type: 'text', value: '123' }] }], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

123

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

123

`); + + d1.update((r) => r.t.edit(1, 2)); + d2.update((r) => r.t.edit(2, 4)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

23

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

1

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + }, this.test!.title); + }); + + it('Can handle block delete concurrently', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { type: 'p', children: [{ type: 'text', value: '12345' }] }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12345

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12345

`); + + d1.update((r) => r.t.edit(1, 3)); + d2.update((r) => r.t.edit(4, 6)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

345

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

123

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

3

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

3

`); + }, this.test!.title); + }); + + it('Can handle insert within block delete concurrently', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { type: 'p', children: [{ type: 'text', value: '12345' }] }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12345

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12345

`); + + d1.update((r) => r.t.edit(2, 5)); + d2.update((r) => r.t.edit(3, 3, { type: 'text', value: 'B' })); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

15

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12B345

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

1B5

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

1B5

`); + }, this.test!.title); + }); + + it('Can handle insert within block delete concurrently [2]', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { type: 'p', children: [{ type: 'text', value: '12345' }] }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12345

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12345

`); + + d1.update((r) => r.t.edit(2, 6)); + d2.update((r) => + r.t.edit( + 3, + 3, + { type: 'text', value: 'a' }, + { type: 'text', value: 'bc' }, + ), + ); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

1

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12abc345

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

1abc

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

1abc

`); + }, this.test!.title); + }); + + it('Can handle block element insertion within delete [2]', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { type: 'p', children: [{ type: 'text', value: '1234' }] }, + { type: 'p', children: [{ type: 'text', value: '5678' }] }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

1234

5678

`, + ); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

1234

5678

`, + ); + + d1.update((r) => r.t.edit(0, 12)); + d2.update((r) => + r.t.edit( + 6, + 6, + { type: 'p', children: [{ type: 'text', value: 'cd' }] }, + { type: 'i', children: [{ type: 'text', value: 'fg' }] }, + ), + ); + assert.equal(d1.getRoot().t.toXML(), /*html*/ ``); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

1234

cd

fg

5678

`, + ); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

cd

fg
`, + ); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

cd

fg
`, + ); + }, this.test!.title); + }); + + it('Can handle concurrent element insert/ deletion (left)', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { type: 'p', children: [{ type: 'text', value: '12345' }] }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12345

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12345

`); + + d1.update((r) => r.t.edit(0, 7)); + d2.update((r) => + r.t.edit( + 0, + 0, + { type: 'p', children: [{ type: 'text', value: 'cd' }] }, + { type: 'i', children: [{ type: 'text', value: 'fg' }] }, + ), + ); + assert.equal(d1.getRoot().t.toXML(), /*html*/ ``); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

cd

fg

12345

`, + ); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

cd

fg
`, + ); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

cd

fg
`, + ); + }, this.test!.title); + }); + + it('Can handle concurrent element insert/ deletion (right)', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [ + { type: 'p', children: [{ type: 'text', value: '12345' }] }, + ], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12345

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12345

`); + + d1.update((r) => r.t.edit(0, 7)); + d2.update((r) => + r.t.edit( + 7, + 7, + { type: 'p', children: [{ type: 'text', value: 'cd' }] }, + { type: 'i', children: [{ type: 'text', value: 'fg' }] }, + ), + ); + + assert.equal(d1.getRoot().t.toXML(), /*html*/ ``); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

12345

cd

fg
`, + ); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal( + d1.getRoot().t.toXML(), + /*html*/ `

cd

fg
`, + ); + assert.equal( + d2.getRoot().t.toXML(), + /*html*/ `

cd

fg
`, + ); + }, this.test!.title); + }); + + it('Can handle deletion of insertion anchor concurrently', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [{ type: 'p', children: [{ type: 'text', value: '12' }] }], + }); + }); + + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12

`); + + d1.update((r) => r.t.edit(2, 2, { type: 'text', value: 'A' })); + d2.update((r) => r.t.edit(1, 2)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

1A2

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

2

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

A2

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

A2

`); + }, this.test!.title); + }); + + it('Can handle deletion after insertion concurrently', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [{ type: 'p', children: [{ type: 'text', value: '12' }] }], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12

`); + + d1.update((r) => r.t.edit(1, 1, { type: 'text', value: 'A' })); + d2.update((r) => r.t.edit(1, 3)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

A12

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

A

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

A

`); + }, this.test!.title); + }); + + it('Can handle deletion before insertion concurrently', async function () { + await withTwoClientsAndDocuments<{ t: Tree }>(async (c1, d1, c2, d2) => { + d1.update((root) => { + root.t = new Tree({ + type: 'r', + children: [{ type: 'p', children: [{ type: 'text', value: '12' }] }], + }); + }); + await c1.sync(); + await c2.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

12

`); + + d1.update((r) => r.t.edit(3, 3, { type: 'text', value: 'A' })); + d2.update((r) => r.t.edit(1, 3)); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

12A

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

`); + + await c1.sync(); + await c2.sync(); + await c1.sync(); + assert.equal(d1.getRoot().t.toXML(), /*html*/ `

A

`); + assert.equal(d2.getRoot().t.toXML(), /*html*/ `

A

`); + }, this.test!.title); + }); +}); + +describe('testing edge cases', () => { + it('Can delete very first text when there is tombstone in front of target text', function () { + const key = toDocKey(`${this.test!.title}-${new Date().getTime()}`); + const doc = new yorkie.Document<{ t: Tree }>(key); + + doc.update((root) => { + // 01. Create a tree and insert a paragraph. + root.t = new Tree(); + root.t.edit(0, 0, { + type: 'p', + children: [{ type: 'text', value: 'abcdefghi' }], + }); + assert.equal(root.t.toXML(), /*html*/ `

abcdefghi

`); + + root.t.edit(1, 1, { type: 'text', value: '12345' }); + assert.equal(root.t.toXML(), `

12345abcdefghi

`); + + root.t.edit(2, 5); + assert.equal(root.t.toXML(), `

15abcdefghi

`); + + root.t.edit(3, 5); + assert.equal(root.t.toXML(), `

15cdefghi

`); + + root.t.edit(2, 4); + assert.equal(root.t.toXML(), `

1defghi

`); + + root.t.edit(1, 3); + assert.equal(root.t.toXML(), `

efghi

`); + + root.t.edit(1, 2); + assert.equal(root.t.toXML(), `

fghi

`); + + root.t.edit(2, 5); + assert.equal(root.t.toXML(), `

f

`); + + root.t.edit(1, 2); + assert.equal(root.t.toXML(), `

`); + }); + }); + + it('Can delete node when there is more than one text node in front which has size bigger than 1', function () { + const key = toDocKey(`${this.test!.title}-${new Date().getTime()}`); + const doc = new yorkie.Document<{ t: Tree }>(key); + + doc.update((root) => { + // 01. Create a tree and insert a paragraph. + root.t = new Tree(); + root.t.edit(0, 0, { + type: 'p', + children: [{ type: 'text', value: 'abcde' }], + }); + assert.equal(root.t.toXML(), /*html*/ `

abcde

`); + + root.t.edit(6, 6, { + type: 'text', + value: 'f', + }); + assert.equal(root.t.toXML(), /*html*/ `

abcdef

`); + + root.t.edit(7, 7, { + type: 'text', + value: 'g', + }); + assert.equal(root.t.toXML(), /*html*/ `

abcdefg

`); + + root.t.edit(7, 8); + assert.equal(root.t.toXML(), /*html*/ `

abcdef

`); + root.t.edit(6, 7); + assert.equal(root.t.toXML(), /*html*/ `

abcde

`); + root.t.edit(5, 6); + assert.equal(root.t.toXML(), /*html*/ `

abcd

`); + root.t.edit(4, 5); + assert.equal(root.t.toXML(), /*html*/ `

abc

`); + root.t.edit(3, 4); + assert.equal(root.t.toXML(), /*html*/ `

ab

`); + root.t.edit(2, 3); + assert.equal(root.t.toXML(), /*html*/ `

a

`); + root.t.edit(1, 2); + assert.equal(root.t.toXML(), /*html*/ `

`); + }); + }); +}); diff --git a/test/unit/document/crdt/tree_test.ts b/test/unit/document/crdt/tree_test.ts index 701a9bc0f..a43213ab5 100644 --- a/test/unit/document/crdt/tree_test.ts +++ b/test/unit/document/crdt/tree_test.ts @@ -27,55 +27,15 @@ import { ChangeContext } from '@yorkie-js-sdk/src/document/change/context'; import { CRDTTree, CRDTTreeNode, + CRDTTreeNodeID, CRDTTreePos, - InitialCRDTTreePos as ITP, toXML, } from '@yorkie-js-sdk/src/document/crdt/tree'; /** - * `betweenEqual` is a helper function that checks the nodes between the given - * indexes. + * `DTP` is a dummy CRDTTreeNodeID for testing. */ -function betweenEqual( - tree: CRDTTree, - from: number, - to: number, - expected: Array, -) { - const nodes: Array = []; - tree.nodesBetweenByTree(from, to, (node) => { - nodes.push(node); - return true; - }); - assert.deepEqual( - nodes.map((node) => { - if (node.isText) { - return `${node.type}.${node.value}`; - } - return node.type; - }), - expected, - ); -} - -/** - * `listEqual` is a helper function that checks the nodes in the RGA in Tree. - */ -function listEqual(tree: CRDTTree, expected: Array) { - const nodes: Array = []; - for (const node of tree) { - nodes.push(node); - } - assert.deepEqual( - nodes.map((node) => { - if (node.isText) { - return `${node.type}.${node.value}`; - } - return node.type; - }), - expected, - ); -} +const DTP = CRDTTreeNodeID.of(ITT, 0); /** * `dummyContext` is a helper context that is used for testing. @@ -86,10 +46,10 @@ const dummyContext = ChangeContext.create( ); /** - * `issuePos` is a helper function that issues a new CRDTTreePos. + * `issuePos` is a helper function that issues a new CRDTTreeNodeID. */ -function issuePos(offset = 0): CRDTTreePos { - return CRDTTreePos.of(dummyContext.issueTimeTicket(), offset); +function issuePos(offset = 0): CRDTTreeNodeID { + return CRDTTreeNodeID.of(dummyContext.issueTimeTicket(), offset); } /** @@ -101,8 +61,8 @@ function issueTime(): TimeTicket { describe('CRDTTreeNode', function () { it('Can be created', function () { - const node = new CRDTTreeNode(ITP, 'text', 'hello'); - assert.equal(node.pos, ITP); + const node = new CRDTTreeNode(DTP, 'text', 'hello'); + assert.equal(node.id, DTP); assert.equal(node.type, 'text'); assert.equal(node.value, 'hello'); assert.equal(node.size, 5); @@ -111,21 +71,21 @@ describe('CRDTTreeNode', function () { }); it('Can be split', function () { - const para = new CRDTTreeNode(ITP, 'p', []); - para.append(new CRDTTreeNode(ITP, 'text', 'helloyorkie')); + const para = new CRDTTreeNode(DTP, 'p', []); + para.append(new CRDTTreeNode(DTP, 'text', 'helloyorkie')); assert.equal(toXML(para), /*html*/ `

helloyorkie

`); assert.equal(para.size, 11); assert.equal(para.isText, false); const left = para.children[0]; - const right = left.split(5); + const right = left.split(5, 0); assert.equal(toXML(para), /*html*/ `

helloyorkie

`); assert.equal(para.size, 11); assert.equal(left.value, 'hello'); assert.equal(right!.value, 'yorkie'); - assert.deepEqual(left.pos, CRDTTreePos.of(ITT, 0)); - assert.deepEqual(right!.pos, CRDTTreePos.of(ITT, 5)); + assert.deepEqual(left.id, CRDTTreeNodeID.of(ITT, 0)); + assert.deepEqual(right!.id, CRDTTreeNodeID.of(ITT, 5)); }); }); @@ -137,13 +97,11 @@ describe('CRDTTree', function () { const tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'r'), issueTime()); assert.equal(tree.getRoot().size, 0); assert.equal(tree.toXML(), /*html*/ ``); - listEqual(tree, ['r']); // 1 //

tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); assert.equal(tree.toXML(), /*html*/ `

`); - listEqual(tree, ['p', 'r']); assert.equal(tree.getRoot().size, 2); // 1 @@ -154,7 +112,6 @@ describe('CRDTTree', function () { issueTime(), ); assert.equal(tree.toXML(), /*html*/ `

hello

`); - listEqual(tree, ['text.hello', 'p', 'r']); assert.equal(tree.getRoot().size, 7); // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @@ -163,7 +120,6 @@ describe('CRDTTree', function () { p.insertAt(new CRDTTreeNode(issuePos(), 'text', 'world'), 0); tree.editByIndex([7, 7], [p], issueTime()); assert.equal(tree.toXML(), /*html*/ `

hello

world

`); - listEqual(tree, ['text.hello', 'p', 'text.world', 'p', 'r']); assert.equal(tree.getRoot().size, 14); // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @@ -174,7 +130,6 @@ describe('CRDTTree', function () { issueTime(), ); assert.equal(tree.toXML(), /*html*/ `

hello!

world

`); - listEqual(tree, ['text.hello', 'text.!', 'p', 'text.world', 'p', 'r']); assert.deepEqual( JSON.stringify(tree.toTestTreeNode()), @@ -212,15 +167,6 @@ describe('CRDTTree', function () { issueTime(), ); assert.equal(tree.toXML(), /*html*/ `

hello~!

world

`); - listEqual(tree, [ - 'text.hello', - 'text.~', - 'text.!', - 'p', - 'text.world', - 'p', - 'r', - ]); }); it('Can delete text nodes with edit', function () { @@ -244,7 +190,6 @@ describe('CRDTTree', function () { issueTime(), ); assert.deepEqual(tree.toXML(), /*html*/ `

ab

cd

`); - listEqual(tree, ['text.ab', 'p', 'text.cd', 'p', 'root']); let treeNode = tree.toTestTreeNode(); assert.equal(treeNode.size, 8); @@ -256,7 +201,6 @@ describe('CRDTTree', function () { //

a

c d

tree.editByIndex([2, 3], undefined, issueTime()); assert.deepEqual(tree.toXML(), /*html*/ `

a

cd

`); - listEqual(tree, ['text.a', 'p', 'text.cd', 'p', 'root']); treeNode = tree.toTestTreeNode(); assert.equal(treeNode.size, 7); @@ -264,6 +208,215 @@ describe('CRDTTree', function () { assert.equal(treeNode.children![0].children![0].size, 1); }); + it('Can find the closest TreePos when parentNode or leftSiblingNode does not exist', function () { + const tree = new CRDTTree( + new CRDTTreeNode(issuePos(), 'root'), + issueTime(), + ); + + const pNode = new CRDTTreeNode(issuePos(), 'p'); + const textNode = new CRDTTreeNode(issuePos(), 'text', 'ab'); + + // 0 1 2 3 4 + //

a b

+ tree.editByIndex([0, 0], [pNode], issueTime()); + tree.editByIndex([1, 1], [textNode], issueTime()); + assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); + + // Find the closest index.TreePos when leftSiblingNode in crdt.TreePos is removed. + // 0 1 2 + //

+ tree.editByIndex([1, 3], undefined, issueTime()); + assert.deepEqual(tree.toXML(), /*html*/ `

`); + + let [parent, left] = tree.findNodesAndSplitText( + new CRDTTreePos(pNode.id, textNode.id), + issueTime(), + ); + assert.equal(tree.toIndex(parent, left), 1); + + // Find the closest index.TreePos when parentNode in crdt.TreePos is removed. + // 0 + // + tree.editByIndex([0, 2], undefined, issueTime()); + assert.deepEqual(tree.toXML(), /*html*/ ``); + + [parent, left] = tree.findNodesAndSplitText( + new CRDTTreePos(pNode.id, textNode.id), + issueTime(), + ); + assert.equal(tree.toIndex(parent, left), 0); + }); +}); + +describe.skip('Tree.split', function () { + it('Can split text nodes', function () { + // 00. Create a tree with 2 paragraphs. + // 0 1 6 11 + //

hello world

+ const tree = new CRDTTree( + new CRDTTreeNode(issuePos(), 'root'), + issueTime(), + ); + tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); + tree.editByIndex( + [1, 1], + [new CRDTTreeNode(issuePos(), 'text', 'helloworld')], + issueTime(), + ); + + // 01. Split left side of 'helloworld'. + tree.split(1, 1); + // TODO(JOOHOJANG): make new helper function when implement Tree.split + //betweenEqual(tree, 1, 11, ['text.helloworld']); + + // 02. Split right side of 'helloworld'. + tree.split(11, 1); + // TODO(JOOHOJANG): make new helper function when implement Tree.split + //betweenEqual(tree, 1, 11, ['text.helloworld']); + + // 03. Split 'helloworld' into 'hello' and 'world'. + tree.split(6, 1); + // TODO(JOOHOJANG): make new helper function when implement Tree.split + //betweenEqual(tree, 1, 11, ['text.hello', 'text.world']); + }); + + it('Can split element nodes', function () { + // 01. Split position 1. + let tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); + tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); + tree.editByIndex( + [1, 1], + [new CRDTTreeNode(issuePos(), 'text', 'ab')], + issueTime(), + ); + assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); + tree.split(1, 2); + assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); + assert.equal(tree.getSize(), 6); + + // 02. Split position 2. + // 0 1 2 3 4 + //

a b

+ tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); + tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); + tree.editByIndex( + [1, 1], + [new CRDTTreeNode(issuePos(), 'text', 'ab')], + issueTime(), + ); + assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); + tree.split(2, 2); + assert.deepEqual(tree.toXML(), /*html*/ `

a

b

`); + assert.equal(tree.getSize(), 6); + + // 03. Split position 3. + tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); + tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); + tree.editByIndex( + [1, 1], + [new CRDTTreeNode(issuePos(), 'text', 'ab')], + issueTime(), + ); + assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); + tree.split(3, 2); + assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); + assert.equal(tree.getSize(), 6); + + // 04. Split position 3. + tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); + tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); + tree.editByIndex( + [1, 1], + [new CRDTTreeNode(issuePos(), 'text', 'ab')], + issueTime(), + ); + tree.editByIndex( + [3, 3], + [new CRDTTreeNode(issuePos(), 'text', 'cd')], + issueTime(), + ); + assert.deepEqual(tree.toXML(), /*html*/ `

abcd

`); + tree.split(3, 2); + assert.deepEqual(tree.toXML(), /*html*/ `

ab

cd

`); + assert.equal(tree.getSize(), 8); + + // 05. Split multiple nodes level 1. + tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); + tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); + tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); + tree.editByIndex( + [2, 2], + [new CRDTTreeNode(issuePos(), 'text', 'ab')], + issueTime(), + ); + assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); + tree.split(3, 1); + assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); + assert.equal(tree.getSize(), 6); + + // Split multiple nodes level 2. + tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); + tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); + tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); + tree.editByIndex( + [2, 2], + [new CRDTTreeNode(issuePos(), 'text', 'ab')], + issueTime(), + ); + assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); + tree.split(3, 2); + assert.deepEqual( + tree.toXML(), + /*html*/ `

ab

`, + ); + assert.equal(tree.getSize(), 8); + + // Split multiple nodes level 3. + tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); + tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); + tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); + tree.editByIndex( + [2, 2], + [new CRDTTreeNode(issuePos(), 'text', 'ab')], + issueTime(), + ); + assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); + tree.split(3, 3); + assert.deepEqual( + tree.toXML(), + /*html*/ `

a

b

`, + ); + assert.equal(tree.getSize(), 10); + }); + + it('Can split and merge element nodes by edit', function () { + const tree = new CRDTTree( + new CRDTTreeNode(issuePos(), 'root'), + issueTime(), + ); + tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); + tree.editByIndex( + [1, 1], + [new CRDTTreeNode(issuePos(), 'text', 'abcd')], + issueTime(), + ); + assert.deepEqual(tree.toXML(), /*html*/ `

abcd

`); + assert.equal(tree.getSize(), 6); + + // 0 1 2 3 4 5 6 7 8 + //

a b

c d

+ tree.split(3, 2); + assert.deepEqual(tree.toXML(), /*html*/ `

ab

cd

`); + assert.equal(tree.getSize(), 8); + + tree.editByIndex([3, 5], undefined, issueTime()); + assert.deepEqual(tree.toXML(), /*html*/ `

abcd

`); + assert.equal(tree.getSize(), 6); + }); +}); + +describe.skip('Tree.move', function () { it('Can delete nodes between element nodes with edit', function () { // 01. Create a tree with 2 paragraphs. // 0 1 2 3 4 5 6 7 8 @@ -285,7 +438,6 @@ describe('CRDTTree', function () { issueTime(), ); assert.deepEqual(tree.toXML(), /*html*/ `

ab

cd

`); - listEqual(tree, ['text.ab', 'p', 'text.cd', 'p', 'root']); // 02. delete b, c and first paragraph. // 0 1 2 3 4 @@ -293,8 +445,6 @@ describe('CRDTTree', function () { tree.editByIndex([2, 6], undefined, issueTime()); assert.deepEqual(tree.toXML(), /*html*/ `

ad

`); - // TODO(hackerwins): Uncomment the below line. - // listEqual(tree, ['text.a', 'text.d', 'p', 'root']); const treeNode = tree.toTestTreeNode(); assert.equal(treeNode.size, 4); // root assert.equal(treeNode.children![0].size, 2); // p @@ -450,207 +600,4 @@ describe('CRDTTree', function () { /*html*/ `

ab

cd

ef

`, ); }); - - it('Get correct range from index', function () { - const tree = new CRDTTree( - new CRDTTreeNode(issuePos(), 'root'), - issueTime(), - ); - - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex([2, 2], [new CRDTTreeNode(issuePos(), 'i')], issueTime()); - tree.editByIndex( - [3, 3], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - - // 0 1 2 3 4 5 6 7 8 - //

a b

- assert.deepEqual( - tree.toXML(), - /*html*/ `

ab

`, - ); - - let [from, to] = tree.pathToPosRange([0]); - let fromIdx = tree.toIndex(from); - let toIdx = tree.toIndex(to); - assert.deepEqual([fromIdx, toIdx], [7, 8]); - - [from, to] = tree.pathToPosRange([0, 0]); - fromIdx = tree.toIndex(from); - toIdx = tree.toIndex(to); - assert.deepEqual([fromIdx, toIdx], [6, 7]); - - [from, to] = tree.pathToPosRange([0, 0, 0]); - fromIdx = tree.toIndex(from); - toIdx = tree.toIndex(to); - assert.deepEqual([fromIdx, toIdx], [5, 6]); - assert.equal(tree.getSize(), 8); - }); -}); - -describe.skip('Tree.split', function () { - it('Can split text nodes', function () { - // 00. Create a tree with 2 paragraphs. - // 0 1 6 11 - //

hello world

- const tree = new CRDTTree( - new CRDTTreeNode(issuePos(), 'root'), - issueTime(), - ); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex( - [1, 1], - [new CRDTTreeNode(issuePos(), 'text', 'helloworld')], - issueTime(), - ); - - // 01. Split left side of 'helloworld'. - tree.split(1, 1); - betweenEqual(tree, 1, 11, ['text.helloworld']); - - // 02. Split right side of 'helloworld'. - tree.split(11, 1); - betweenEqual(tree, 1, 11, ['text.helloworld']); - - // 03. Split 'helloworld' into 'hello' and 'world'. - tree.split(6, 1); - betweenEqual(tree, 1, 11, ['text.hello', 'text.world']); - }); - - it('Can split element nodes', function () { - // 01. Split position 1. - let tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex( - [1, 1], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); - tree.split(1, 2); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); - assert.equal(tree.getSize(), 6); - - // 02. Split position 2. - // 0 1 2 3 4 - //

a b

- tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex( - [1, 1], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); - tree.split(2, 2); - assert.deepEqual(tree.toXML(), /*html*/ `

a

b

`); - assert.equal(tree.getSize(), 6); - - // 03. Split position 3. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex( - [1, 1], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); - tree.split(3, 2); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); - assert.equal(tree.getSize(), 6); - - // 04. Split position 3. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex( - [1, 1], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - tree.editByIndex( - [3, 3], - [new CRDTTreeNode(issuePos(), 'text', 'cd')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

abcd

`); - tree.split(3, 2); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

cd

`); - assert.equal(tree.getSize(), 8); - - // 05. Split multiple nodes level 1. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex( - [2, 2], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); - tree.split(3, 1); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); - assert.equal(tree.getSize(), 6); - - // Split multiple nodes level 2. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex( - [2, 2], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); - tree.split(3, 2); - assert.deepEqual( - tree.toXML(), - /*html*/ `

ab

`, - ); - assert.equal(tree.getSize(), 8); - - // Split multiple nodes level 3. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex( - [2, 2], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); - tree.split(3, 3); - assert.deepEqual( - tree.toXML(), - /*html*/ `

a

b

`, - ); - assert.equal(tree.getSize(), 10); - }); - - it('Can split and merge element nodes by edit', function () { - const tree = new CRDTTree( - new CRDTTreeNode(issuePos(), 'root'), - issueTime(), - ); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex( - [1, 1], - [new CRDTTreeNode(issuePos(), 'text', 'abcd')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

abcd

`); - assert.equal(tree.getSize(), 6); - - // 0 1 2 3 4 5 6 7 8 - //

a b

c d

- tree.split(3, 2); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

cd

`); - assert.equal(tree.getSize(), 8); - - tree.editByIndex([3, 5], undefined, issueTime()); - assert.deepEqual(tree.toXML(), /*html*/ `

abcd

`); - assert.equal(tree.getSize(), 6); - }); });