Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance Set and Add for representing nested elements #691

Merged
merged 11 commits into from
Nov 23, 2023
3 changes: 1 addition & 2 deletions public/whiteboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,7 @@ <h2>
const selectedShape = root.shapes.find(
(shape) => shape.id === selectedShapeID,
);
selectedShape.point.x = movingShapePoint.x;
selectedShape.point.y = movingShapePoint.y;
selectedShape.point = movingShapePoint;
presence.set({ movingShapePoint: null });
});
}
Expand Down
37 changes: 34 additions & 3 deletions src/api/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,11 @@ function toElementSimple(element: CRDTElement): PbJSONElementSimple {
if (element instanceof CRDTObject) {
pbElementSimple.setType(PbValueType.VALUE_TYPE_JSON_OBJECT);
pbElementSimple.setCreatedAt(toTimeTicket(element.getCreatedAt()));
pbElementSimple.setValue(objectToBytes(element));
} else if (element instanceof CRDTArray) {
pbElementSimple.setType(PbValueType.VALUE_TYPE_JSON_ARRAY);
pbElementSimple.setCreatedAt(toTimeTicket(element.getCreatedAt()));
pbElementSimple.setValue(arrayToBytes(element));
} else if (element instanceof CRDTText) {
pbElementSimple.setType(PbValueType.VALUE_TYPE_TEXT);
pbElementSimple.setCreatedAt(toTimeTicket(element.getCreatedAt()));
Expand Down Expand Up @@ -835,9 +837,19 @@ function fromCounterType(pbValueType: PbValueType): CounterType {
function fromElementSimple(pbElementSimple: PbJSONElementSimple): CRDTElement {
switch (pbElementSimple.getType()) {
case PbValueType.VALUE_TYPE_JSON_OBJECT:
return CRDTObject.create(fromTimeTicket(pbElementSimple.getCreatedAt())!);
if (!pbElementSimple.getValue()) {
return CRDTObject.create(
fromTimeTicket(pbElementSimple.getCreatedAt())!,
);
}
return bytesToObject(pbElementSimple.getValue_asU8());
case PbValueType.VALUE_TYPE_JSON_ARRAY:
return CRDTArray.create(fromTimeTicket(pbElementSimple.getCreatedAt())!);
if (!pbElementSimple.getValue()) {
return CRDTArray.create(
fromTimeTicket(pbElementSimple.getCreatedAt())!,
);
}
return bytesToArray(pbElementSimple.getValue_asU8());
case PbValueType.VALUE_TYPE_TEXT:
return CRDTText.create(
RGATreeSplit.create(),
Expand Down Expand Up @@ -1343,7 +1355,7 @@ function bytesToSnapshot<P extends Indexable>(
*/
function bytesToObject(bytes?: Uint8Array): CRDTObject {
if (!bytes) {
return CRDTObject.create(InitialTimeTicket);
throw new Error('bytes is empty');
}

const pbElement = PbJSONElement.deserializeBinary(bytes);
Expand All @@ -1357,6 +1369,25 @@ function objectToBytes(obj: CRDTObject): Uint8Array {
return toElement(obj).serializeBinary();
}

/**
* `bytesToArray` creates an CRDTArray from the given bytes.
*/
function bytesToArray(bytes?: Uint8Array): CRDTArray {
if (!bytes) {
throw new Error('bytes is empty');
}

const pbElement = PbJSONElement.deserializeBinary(bytes);
return fromArray(pbElement.getJsonArray()!);
}

/**
* `arrayToBytes` converts the given CRDTArray to bytes.
*/
function arrayToBytes(array: CRDTArray): Uint8Array {
return toArray(array).serializeBinary();
}

/**
* `bytesToTree` creates an CRDTTree from the given bytes.
*/
Expand Down
15 changes: 13 additions & 2 deletions src/document/crdt/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,19 @@ export class CRDTArray extends CRDTContainer {
/**
* `create` creates a new instance of Array.
*/
public static create(createdAt: TimeTicket): CRDTArray {
return new CRDTArray(createdAt, RGATreeList.create());
public static create(
createdAt: TimeTicket,
value?: Array<CRDTElement>,
): CRDTArray {
if (!value) {
return new CRDTArray(createdAt, RGATreeList.create());
}

const elements = RGATreeList.create();
for (const v of value) {
elements.insertAfter(elements.getLastCreatedAt(), v.deepcopy());
}
return new CRDTArray(createdAt, elements);
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/document/crdt/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ export abstract class CRDTElement {
removedAt.after(this.getPositionedAt()) &&
(!this.removedAt || removedAt.after(this.removedAt))
) {
// NOTE(chacha912): If it's a CRDTContainer, removedAt is marked only on
// the top-level element, without marking all descendant elements. This
// enhances the speed of deletion.
this.removedAt = removedAt;
return true;
}
Expand Down
15 changes: 13 additions & 2 deletions src/document/crdt/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,19 @@ export class CRDTObject extends CRDTContainer {
/**
* `create` creates a new instance of CRDTObject.
*/
public static create(createdAt: TimeTicket): CRDTObject {
return new CRDTObject(createdAt, ElementRHT.create());
public static create(
createdAt: TimeTicket,
value?: { [key: string]: CRDTElement },
): CRDTObject {
if (!value) {
return new CRDTObject(createdAt, ElementRHT.create());
}

const memberNodes = ElementRHT.create();
for (const [k, v] of Object.entries(value)) {
memberNodes.set(k, v.deepcopy(), v.getCreatedAt());
}
return new CRDTObject(createdAt, memberNodes);
}

/**
Expand Down
82 changes: 42 additions & 40 deletions src/document/crdt/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,7 @@ export class CRDTRoot {
this.removedElementSetByCreatedAt = new Set();
this.elementHasRemovedNodesSetByCreatedAt = new Set();
this.opsForTest = [];

this.elementPairMapByCreatedAt.set(
this.rootObject.getCreatedAt().toIDString(),
{ element: this.rootObject },
);

rootObject.getDescendants(
(elem: CRDTElement, parent: CRDTContainer): boolean => {
this.registerElement(elem, parent);
return false;
},
);
this.registerElement(rootObject, undefined);
}

/**
Expand All @@ -115,6 +104,16 @@ export class CRDTRoot {
return pair.element;
}

/**
* `findElementPairByCreatedAt` returns the element and parent pair
* of given creation time.
*/
public findElementPairByCreatedAt(
createdAt: TimeTicket,
): CRDTElementPair | undefined {
return this.elementPairMapByCreatedAt.get(createdAt.toIDString());
}

/**
* `createSubPaths` creates an array of the sub paths for the given element.
*/
Expand Down Expand Up @@ -150,23 +149,45 @@ export class CRDTRoot {
}

/**
* `registerElement` registers the given element to hash table.
* `registerElement` registers the given element and its descendants to hash table.
*/
public registerElement(element: CRDTElement, parent: CRDTContainer): void {
public registerElement(element: CRDTElement, parent?: CRDTContainer): void {
this.elementPairMapByCreatedAt.set(element.getCreatedAt().toIDString(), {
parent,
element,
});

if (element instanceof CRDTContainer) {
element.getDescendants((elem, parent) => {
this.registerElement(elem, parent);
return false;
});
}
}

/**
* `deregisterElement` deregister the given element from hash table.
* `deregisterElement` deregister the given element and its descendants from hash table.
*/
public deregisterElement(element: CRDTElement): void {
this.elementPairMapByCreatedAt.delete(element.getCreatedAt().toIDString());
this.removedElementSetByCreatedAt.delete(
element.getCreatedAt().toIDString(),
);
public deregisterElement(element: CRDTElement): number {
let count = 0;

const deregisterElementInternal = (elem: CRDTElement) => {
const createdAt = elem.getCreatedAt().toIDString();
this.elementPairMapByCreatedAt.delete(createdAt);
this.removedElementSetByCreatedAt.delete(createdAt);
count++;

if (elem instanceof CRDTContainer) {
elem.getDescendants((e) => {
deregisterElementInternal(e);
return false;
});
}
};

deregisterElementInternal(element);

return count;
}

/**
Expand Down Expand Up @@ -253,7 +274,7 @@ export class CRDTRoot {
ticket.compare(pair.element.getRemovedAt()!) >= 0
) {
pair.parent!.purge(pair.element);
count += this.garbageCollectInternal(pair.element);
count += this.deregisterElement(pair.element);
}
}

Expand All @@ -273,25 +294,6 @@ export class CRDTRoot {
return count;
}

private garbageCollectInternal(element: CRDTElement): number {
let count = 0;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const callback = (elem: CRDTElement, parent?: CRDTContainer): boolean => {
this.deregisterElement(elem);
count++;
return false;
};

callback(element);

if (element instanceof CRDTContainer) {
element.getDescendants(callback);
}

return count;
}

/**
* `toJSON` returns the JSON encoding of this root object.
*/
Expand Down
93 changes: 34 additions & 59 deletions src/document/json/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,14 @@ import { MoveOperation } from '@yorkie-js-sdk/src/document/operation/move_operat
import { RemoveOperation } from '@yorkie-js-sdk/src/document/operation/remove_operation';
import { ChangeContext } from '@yorkie-js-sdk/src/document/change/context';
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 {
Primitive,
PrimitiveValue,
} from '@yorkie-js-sdk/src/document/crdt/primitive';
import { ObjectProxy } from '@yorkie-js-sdk/src/document/json/object';
import { Primitive } from '@yorkie-js-sdk/src/document/crdt/primitive';
import {
JSONElement,
WrappedElement,
toWrappedElement,
toJSONElement,
buildCRDTElement,
} from '@yorkie-js-sdk/src/document/json/element';

/**
Expand Down Expand Up @@ -329,6 +325,22 @@ export class ArrayProxy {
}
}

/**
* `buildArrayElements` constructs array elements based on the user-provided array.
*/
public static buildArrayElements(
context: ChangeContext,
value: Array<unknown>,
): Array<CRDTElement> {
const elements: Array<CRDTElement> = [];
for (const v of value) {
const createdAt = context.issueTimeTicket();
const elem = buildCRDTElement(context, v, createdAt);
elements.push(elem);
}
return elements;
}

/**
* `pushInternal` pushes the value to the target array.
*/
Expand Down Expand Up @@ -439,58 +451,19 @@ export class ArrayProxy {
prevCreatedAt: TimeTicket,
value: unknown,
): CRDTElement {
const ticket = context.issueTimeTicket();
if (Primitive.isSupport(value)) {
const primitive = Primitive.of(value as PrimitiveValue, ticket);
const clone = primitive.deepcopy();
target.insertAfter(prevCreatedAt, clone);
context.registerElement(clone, target);
context.push(
AddOperation.create(
target.getCreatedAt(),
prevCreatedAt,
primitive.deepcopy(),
ticket,
),
);
return primitive;
} else if (Array.isArray(value)) {
const array = CRDTArray.create(ticket);
const clone = array.deepcopy();
target.insertAfter(prevCreatedAt, clone);
context.registerElement(clone, target);
context.push(
AddOperation.create(
target.getCreatedAt(),
prevCreatedAt,
array.deepcopy(),
ticket,
),
);
for (const element of value) {
ArrayProxy.pushInternal(context, clone, element);
}
return array;
} else if (typeof value === 'object') {
const obj = CRDTObject.create(ticket);
target.insertAfter(prevCreatedAt, obj);
context.registerElement(obj, target);
context.push(
AddOperation.create(
target.getCreatedAt(),
prevCreatedAt,
obj.deepcopy(),
ticket,
),
);

for (const [k, v] of Object.entries(value!)) {
ObjectProxy.setInternal(context, obj, k, v);
}
return obj;
}

throw new TypeError(`Unsupported type of value: ${typeof value}`);
const createdAt = context.issueTimeTicket();
const element = buildCRDTElement(context, value, createdAt);
target.insertAfter(prevCreatedAt, element);
context.registerElement(element, target);
context.push(
AddOperation.create(
target.getCreatedAt(),
prevCreatedAt,
element.deepcopy(),
createdAt,
),
);
return element;
}

/**
Expand Down Expand Up @@ -579,7 +552,9 @@ export class ArrayProxy {
for (let i = from; i < to; i++) {
const removed = ArrayProxy.deleteInternalByIndex(context, target, from);
if (removed) {
removeds.push(toJSONElement(context, removed)!);
const removedElem = removed.deepcopy();
removedElem.setRemovedAt();
removeds.push(toJSONElement(context, removedElem)!);
}
}
if (items) {
Expand Down
Loading
Loading