Skip to content

Commit

Permalink
Modify set and add operations to allow setting initial values for obj…
Browse files Browse the repository at this point in the history
…ect and array

Previously, only the outer shells of object ({}) and array ([]) were added,
requiring separate operations to set their internal values. The modification
enables setting initial values directly within a single operation.
  • Loading branch information
chacha912 committed Nov 20, 2023
1 parent 859c019 commit 21f6b2f
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 73 deletions.
21 changes: 19 additions & 2 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,9 @@ 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())!);
return bytesToObject(pbElementSimple.getValue_asU8());
case PbValueType.VALUE_TYPE_JSON_ARRAY:
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 @@ -1357,6 +1359,21 @@ function objectToBytes(obj: CRDTObject): Uint8Array {
return toElement(obj).serializeBinary();
}

/**
* `bytesToArray` creates an CRDTArray from the given bytes.
*/
function bytesToArray(bytes: Uint8Array): CRDTArray {
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
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
51 changes: 40 additions & 11 deletions src/document/json/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import { AddOperation } from '@yorkie-js-sdk/src/document/operation/add_operatio
import { MoveOperation } from '@yorkie-js-sdk/src/document/operation/move_operation';
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 {
CRDTContainer,
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 {
Expand Down Expand Up @@ -329,6 +332,31 @@ export class ArrayProxy {
}
}

/**
* `buildArray` constructs an array of CRDTElements based on the user-provided array.
*/
public static buildArray(
context: ChangeContext,
value: Array<unknown>,
): Array<CRDTElement> {
const elementArray: Array<CRDTElement> = [];
for (const v of value) {
const ticket = context.issueTimeTicket();
let elem: CRDTElement;
if (Primitive.isSupport(v)) {
elem = Primitive.of(v as PrimitiveValue, ticket);
} else if (Array.isArray(v)) {
elem = CRDTArray.create(ticket, ArrayProxy.buildArray(context, v));
} else if (typeof v === 'object') {
elem = CRDTObject.create(ticket, ObjectProxy.buildObject(context, v!));
} else {
throw new TypeError(`Unsupported type of value: ${typeof value}`);
}
elementArray.push(elem);
}
return elementArray;
}

/**
* `pushInternal` pushes the value to the target array.
*/
Expand Down Expand Up @@ -455,7 +483,10 @@ export class ArrayProxy {
);
return primitive;
} else if (Array.isArray(value)) {
const array = CRDTArray.create(ticket);
const array = CRDTArray.create(
ticket,
ArrayProxy.buildArray(context, value),
);
const clone = array.deepcopy();
target.insertAfter(prevCreatedAt, clone);
context.registerElement(clone, target);
Expand All @@ -467,12 +498,12 @@ export class ArrayProxy {
ticket,
),
);
for (const element of value) {
ArrayProxy.pushInternal(context, clone, element);
}
return array;
} else if (typeof value === 'object') {
const obj = CRDTObject.create(ticket);
const obj = CRDTObject.create(
ticket,
ObjectProxy.buildObject(context, value!),
);
target.insertAfter(prevCreatedAt, obj);
context.registerElement(obj, target);
context.push(
Expand All @@ -483,10 +514,6 @@ export class ArrayProxy {
ticket,
),
);

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

Expand Down Expand Up @@ -579,7 +606,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
61 changes: 53 additions & 8 deletions src/document/json/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,14 @@ export class ObjectProxy {
SetOperation.create(key, primitive, target.getCreatedAt(), ticket),
);
} else if (Array.isArray(value)) {
const array = CRDTArray.create(ticket);
const array = CRDTArray.create(
ticket,
ArrayProxy.buildArray(context, value),
);
array.getDescendants((elem, parent) => {
context.registerElement(elem, parent);
return false;
});
setAndRegister(array);
context.push(
SetOperation.create(
Expand All @@ -182,9 +189,6 @@ export class ObjectProxy {
ticket,
),
);
for (const element of value) {
ArrayProxy.pushInternal(context, array, element);
}
} else if (typeof value === 'object') {
if (value instanceof Text) {
const text = CRDTText.create(RGATreeSplit.create(), ticket);
Expand Down Expand Up @@ -230,7 +234,14 @@ export class ObjectProxy {
);
value.initialize(context, tree);
} else {
const obj = CRDTObject.create(ticket);
const obj = CRDTObject.create(
ticket,
ObjectProxy.buildObject(context, value!),
);
obj.getDescendants((elem, parent) => {
context.registerElement(elem, parent);
return false;
});
setAndRegister(obj);
context.push(
SetOperation.create(
Expand All @@ -240,15 +251,49 @@ export class ObjectProxy {
ticket,
),
);
for (const [k, v] of Object.entries(value!)) {
ObjectProxy.setInternal(context, obj, k, v);
}
}
} else {
logger.fatal(`unsupported type of value: ${typeof value}`);
}
}

/**
* `buildObject` constructs an object where all values from the
* user-provided object are transformed into CRDTElements.
* This function takes an object and iterates through its values,
* converting each value into a corresponding CRDTElement.
*/
public static buildObject(
context: ChangeContext,
value: object,
): { [key: string]: CRDTElement } {
const elementObject: { [key: string]: CRDTElement } = {};
for (const [k, v] of Object.entries(value)) {
if (k.includes('.')) {
throw new YorkieError(
Code.InvalidObjectKey,
`key must not contain the '.'.`,
);
}

const ticket = context.issueTimeTicket();
let elem: CRDTElement;
if (Primitive.isSupport(v)) {
elem = Primitive.of(v as PrimitiveValue, ticket);
} else if (Array.isArray(v)) {
const array = ArrayProxy.buildArray(context, v);
elem = CRDTArray.create(ticket, array);
} else if (typeof v === 'object') {
const object = ObjectProxy.buildObject(context, v);
elem = CRDTObject.create(ticket, object);
} else {
throw new TypeError(`Unsupported type of value: ${typeof value}`);
}
elementObject[k] = elem;
}
return elementObject;
}

/**
* `deleteInternal` deletes the value of the given key.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/document/operation/add_operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class AddOperation extends Operation {
* `toTestString` returns a string containing the meta data.
*/
public toTestString(): string {
return `${this.getParentCreatedAt().toTestString()}.ADD`;
return `${this.getParentCreatedAt().toTestString()}.ADD.${this.value.toJSON()}`;
}

/**
Expand Down
14 changes: 10 additions & 4 deletions src/document/operation/set_operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@

import { logger } from '@yorkie-js-sdk/src/util/logger';
import { TimeTicket } from '@yorkie-js-sdk/src/document/time/ticket';
import { CRDTElement } from '@yorkie-js-sdk/src/document/crdt/element';
import {
CRDTContainer,
CRDTElement,
} from '@yorkie-js-sdk/src/document/crdt/element';
import { CRDTRoot } from '@yorkie-js-sdk/src/document/crdt/root';
import { CRDTObject } from '@yorkie-js-sdk/src/document/crdt/object';
import {
Expand Down Expand Up @@ -85,6 +88,12 @@ export class SetOperation extends Operation {
if (removed) {
root.registerRemovedElement(removed);
}
if (value instanceof CRDTContainer) {
value.getDescendants((elem, parent) => {
root.registerElement(elem, parent);
return false;
});
}

return {
opInfos: [
Expand All @@ -108,9 +117,6 @@ export class SetOperation extends Operation {
);

if (value !== undefined && !value.isRemoved()) {
// TODO(chacha912): When the value is an object,
// it always sets as an empty object from the remote.
// (Refer to https://github.com/yorkie-team/yorkie/issues/663)
reverseOp = SetOperation.create(
this.key,
value.deepcopy(),
Expand Down
Loading

0 comments on commit 21f6b2f

Please sign in to comment.