From 4d2db79706ca451f2d18f10bc1f562bc42c8379c Mon Sep 17 00:00:00 2001 From: raararaara Date: Mon, 21 Oct 2024 20:48:53 +0900 Subject: [PATCH 1/8] Add option to initialize Document with initialRoot --- packages/sdk/src/client/client.ts | 15 +- packages/sdk/src/document/json/element.ts | 5 + packages/sdk/src/document/json/object.ts | 24 + .../sdk/test/integration/document_test.ts | 489 +++++++++++++++++- 4 files changed, 531 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/client/client.ts b/packages/sdk/src/client/client.ts index 9a25cb7bc..88cd633f8 100644 --- a/packages/sdk/src/client/client.ts +++ b/packages/sdk/src/client/client.ts @@ -294,9 +294,10 @@ export class Client { * this client will synchronize the given document. */ public attach( - doc: Document, + doc: Document, options: { initialPresence?: P; + initialRoot?: P; syncMode?: SyncMode; } = {}, ): Promise> { @@ -365,6 +366,18 @@ export class Client { } logger.info(`[AD] c:"${this.getKey()}" attaches d:"${doc.getKey()}"`); + + const crdtObject = doc.getRootObject(); + if (options.initialRoot) { + doc.update((root) => { + for (const [k, v] of Object.entries(options.initialRoot || {})) { + if (crdtObject.get(k) === undefined) { + (root as Record)[k] = v; + } + } + }); + } + return doc; }) .catch((err) => { diff --git a/packages/sdk/src/document/json/element.ts b/packages/sdk/src/document/json/element.ts index 65e679414..2d70d83bc 100644 --- a/packages/sdk/src/document/json/element.ts +++ b/packages/sdk/src/document/json/element.ts @@ -166,6 +166,11 @@ export function buildCRDTElement( } else if (value instanceof Tree) { element = CRDTTree.create(value.buildRoot(context), createdAt); value.initialize(context, element as CRDTTree); + } else if (value instanceof Map) { + element = CRDTObject.create( + createdAt, + ObjectProxy.buildObjectMembersFromMap(context, value!), + ); } else { element = CRDTObject.create( createdAt, diff --git a/packages/sdk/src/document/json/object.ts b/packages/sdk/src/document/json/object.ts index 63ce8369d..2646959b6 100644 --- a/packages/sdk/src/document/json/object.ts +++ b/packages/sdk/src/document/json/object.ts @@ -169,6 +169,30 @@ export class ObjectProxy { ); } + /** + * `buildObjectMembersFromMap` 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 buildObjectMembersFromMap( + context: ChangeContext, + value: Map, + ): { [key: string]: CRDTElement } { + const members: { [key: string]: CRDTElement } = {}; + for (const [k, v] of value) { + if (k.includes('.')) { + throw new YorkieError( + Code.ErrInvalidObjectKey, + `key must not contain the '.'.`, + ); + } + const createdAt = context.issueTimeTicket(); + members[k] = buildCRDTElement(context, v, createdAt); + } + return members; + } + /** * `buildObjectMembers` constructs an object where all values from the * user-provided object are transformed into CRDTElements. diff --git a/packages/sdk/test/integration/document_test.ts b/packages/sdk/test/integration/document_test.ts index c14c1b8bd..b268955e9 100644 --- a/packages/sdk/test/integration/document_test.ts +++ b/packages/sdk/test/integration/document_test.ts @@ -3,7 +3,7 @@ import yorkie, { Counter, Text, JSONArray, - SyncMode, + SyncMode, Tree, } from '@yorkie-js-sdk/src/yorkie'; import { testRPCAddr, @@ -21,6 +21,7 @@ import { } from '@yorkie-js-sdk/src/document/document'; import { OperationInfo } from '@yorkie-js-sdk/src/document/operation/operation'; import { YorkieError } from '@yorkie-js-sdk/src/util/error'; +import { CounterType } from '@yorkie-js-sdk/src/document/crdt/counter'; describe('Document', function () { afterEach(() => { @@ -1097,4 +1098,490 @@ describe('Document', function () { assert.equal(doc.toSortedJSON(), '{"counter":100}'); }); }); + + describe('Document with InitialRoot', function () { + it('Can attach with InitialRoot', async function ({ task }) { + const c1 = new yorkie.Client(testRPCAddr); + const c2 = new yorkie.Client(testRPCAddr); + const c3 = new yorkie.Client(testRPCAddr); + await c1.activate(); + await c2.activate(); + await c3.activate(); + const docKey = toDocKey(`${task.name}-${new Date().getTime()}`); + const doc1 = new yorkie.Document(docKey); + + function toMap(obj: Record): Map { + return new Map(Object.entries(obj)); + } + // 01. attach and initialize document + await c1.attach(doc1, { + initialRoot: { + counter: new Counter(CounterType.IntegerCnt, 0), + content: new Map( + Object.entries({ + x: 1, + y: 1, + }), + ), + }, + }); + assert.equal(doc1.getStatus(), DocumentStatus.Attached); + assert.equal( + doc1.toSortedJSON(), + '{"content":{"x":1,"y":1},"counter":0}', + ); + await c1.sync(); + + // 02. attach and initialize document with new fields and if key already exists, it will be discarded + const doc2 = new yorkie.Document(docKey); + await c2.attach(doc2, { + initialRoot: { + counter: new Counter(CounterType.IntegerCnt, 1), + content: new Map( + Object.entries({ + x: 2, + y: 2, + }), + ), + new: new Map([['k', 'v']]), + }, + }); + assert.equal(doc2.getStatus(), DocumentStatus.Attached); + assert.equal( + doc2.toSortedJSON(), + '{"content":{"x":1,"y":1},"counter":0,"new":{"k":"v"}}', + ); + + await c1.deactivate(); + await c2.deactivate(); + await c3.deactivate(); + }); + + it('Can attach with InitialRoot after key deletion', async function ({ + task, + }) { + const c1 = new yorkie.Client(testRPCAddr); + const c2 = new yorkie.Client(testRPCAddr); + const c3 = new yorkie.Client(testRPCAddr); + await c1.activate(); + await c2.activate(); + await c3.activate(); + + const docKey = toDocKey(`${task.name}-${new Date().getTime()}`); + const doc1 = new yorkie.Document(docKey); + // 01. attach and initialize document + await c1.attach(doc1, { + initialRoot: { + counter: new Counter(CounterType.IntegerCnt, 0), + content: new Map( + Object.entries({ + x: 1, + y: 1, + }), + ), + }, + }); + assert.equal(doc1.getStatus(), DocumentStatus.Attached); + assert.equal( + doc1.toSortedJSON(), + '{"content":{"x":1,"y":1},"counter":0}', + ); + await c1.sync(); + + // 02. client2 attach with initialRoot and delete elements + const doc2 = new yorkie.Document(docKey); + await c2.attach(doc2); + assert.equal(doc2.getStatus(), DocumentStatus.Attached); + doc2.update((root) => { + delete root['counter']; + delete root['content']; + }); + assert.equal(doc2.toSortedJSON(), '{}'); + await c2.sync(); + + const doc3 = new yorkie.Document(docKey); + await c3.attach(doc3, { + initialRoot: { + counter: new Counter(CounterType.IntegerCnt, 3), + content: new Map( + Object.entries({ + x: 3, + y: 3, + }), + ), + }, + }); + assert.equal(doc3.getStatus(), DocumentStatus.Attached); + assert.equal( + doc3.toSortedJSON(), + '{"content":{"x":3,"y":3},"counter":3}', + ); + + await c1.deactivate(); + await c2.deactivate(); + await c3.deactivate(); + }); + + it('Can handle concurrent attach with InitialRoot', async function ({ + task, + }) { + const c1 = new yorkie.Client(testRPCAddr); + const c2 = new yorkie.Client(testRPCAddr); + await c1.activate(); + await c2.activate(); + + const docKey = toDocKey(`${task.name}-${new Date().getTime()}`); + const doc1 = new yorkie.Document(docKey); + + // 01. user1 attach with initialRoot and client doesn't sync + await c1.attach(doc1, { + initialRoot: { + first_writer: 'user1', + }, + }); + assert.equal(doc1.getStatus(), DocumentStatus.Attached); + assert.equal(doc1.toSortedJSON(), '{"first_writer":"user1"}'); + + // 02. user2 attach with initialRoot and client doesn't sync + const doc2 = new yorkie.Document(docKey); + await c2.attach(doc2, { + initialRoot: { + first_writer: 'user2', + }, + }); + assert.equal(doc2.getStatus(), DocumentStatus.Attached); + assert.equal(doc2.toSortedJSON(), '{"first_writer":"user2"}'); + + // 03. user1 sync first and user2 seconds + await c1.sync(); + await c2.sync(); + + // 04. user1's local document's first_writer was user1 + assert.equal(doc1.toSortedJSON(), '{"first_writer":"user1"}'); + assert.equal(doc2.toSortedJSON(), '{"first_writer":"user2"}'); + + // 05. user1's local document's first_writer is overwritten by user2 + await c1.sync(); + assert.equal(doc1.toSortedJSON(), '{"first_writer":"user2"}'); + + await c1.deactivate(); + await c2.deactivate(); + }); + + it('Can attach with InitialRoot by same key', async function ({ task }) { + const c1 = new yorkie.Client(testRPCAddr); + await c1.activate(); + + const docKey = toDocKey(`${task.name}-${new Date().getTime()}`); + const doc = new yorkie.Document(docKey); + + const k1 = 'key'; + const k2 = 'key'; + const k3 = 'key'; + const k4 = 'key'; + const k5 = 'key'; + + // Attach the document with initial root containing the same key multiple times + await c1.attach(doc, { + initialRoot: { + [k1]: 1, + [k2]: 2, + [k3]: 3, + [k4]: 4, + [k5]: 5, + }, + }); + + assert.equal(doc.getStatus(), DocumentStatus.Attached); + // The last value should be used when the same key is repeated + assert.equal(doc.toSortedJSON(), '{"key":5}'); + + await c1.deactivate(); + }); + + it('Can attach with InitialRoot conflict type', async function ({ task }) { + const c1 = new yorkie.Client(testRPCAddr); + const c2 = new yorkie.Client(testRPCAddr); + await c1.activate(); + await c2.activate(); + + const docKey1 = toDocKey(`${task.name}-doc1-${new Date().getTime()}`); + const docKey2 = toDocKey(`${task.name}-doc2-${new Date().getTime()}`); + + const doc1 = new yorkie.Document(docKey1); + + // 01. Attach with initialRoot and set counter + await c1.attach(doc1, { + initialRoot: { + k: new Counter(CounterType.LongCnt, 1), + }, + }); + assert.equal(doc1.getStatus(), DocumentStatus.Attached); + await c1.sync(); + + // 02. Attach with initialRoot and set text + const doc2 = new yorkie.Document(docKey2); + await c2.attach(doc2, { + initialRoot: { + k: new yorkie.Text(), + }, + }); + assert.equal(doc2.getStatus(), DocumentStatus.Attached); + await c2.sync(); + + // 03. Client2 tries to update the counter (this should fail as the type is a Text, not a Counter) + assert.throws(() => { + doc2.update((root) => { + root['k'].edit(0, 1, 'a'); + }); + }); + + await c1.deactivate(); + await c2.deactivate(); + }); + + it('Can attach with initialRoot support type', async function ({ task }) { + const c1 = new yorkie.Client(testRPCAddr); + await c1.activate(); + + type Myint = number; + + interface MyStruct { + M: Myint; + } + + interface t1 { + M: string; + } + + interface T1 { + M: string; + } + + interface T2 { + T1: T1; + t1: t1; + M: string; + } + + const nowTime = new Date(); + const tests = [ + // supported primitive types + { + caseName: 'nil', + input: null, + expectedJSON: `{"k":null}`, + expectPanic: false, + }, + { + caseName: 'int', + input: 1, + expectedJSON: `{"k":1}`, + expectPanic: false, + }, + { + caseName: 'int32', + input: 1, + expectedJSON: `{"k":1}`, + expectPanic: false, + }, + { + caseName: 'int64', + input: 1, + expectedJSON: `{"k":1}`, + expectPanic: false, + }, + { + caseName: 'float32', + input: 1.1, + expectedJSON: `{"k":1.1}`, + expectPanic: false, + }, + { + caseName: 'float64', + input: 1.1, + expectedJSON: `{"k":1.1}`, + expectPanic: false, + }, + { + caseName: 'string', + input: 'hello', + expectedJSON: `{"k":"hello"}`, + expectPanic: false, + }, + { + caseName: 'bool', + input: true, + expectedJSON: `{"k":true}`, + expectPanic: false, + }, + // { + // caseName: 'time', + // input: nowTime, + // expectedJSON: `{"k":"${nowTime.toISOString()}"}`, + // expectPanic: false, + // }, + { + caseName: 'Myint', + input: 1 as Myint, + expectedJSON: `{"k":1}`, + expectPanic: false, + }, + + // unsupported primitive types + // { caseName: 'int8', input: 1, expectedJSON: `{}`, expectPanic: true }, + + // supported slice, array types + { + caseName: 'int array', + input: [1, 2, 3], + expectedJSON: `{"k":[1,2,3]}`, + expectPanic: false, + }, + { + caseName: 'string array', + input: ['a', 'b', 'c'], + expectedJSON: `{"k":["a","b","c"]}`, + expectPanic: false, + }, + // { + // caseName: 'any array', + // input: [null, 1, 1.0, 'hello', true, nowTime, [1, 2, 3]], + // expectedJSON: `{"k":[null,1,1.000000,"hello",true,"${nowTime.toISOString()}",[1,2,3]]}`, + // expectPanic: false, + // }, + + // supported map types + // { + // caseName: 'string:any map', + // input: { + // a: null, + // b: 1, + // c: 1.0, + // d: 'hello', + // e: true, + // // f: nowTime, + // g: [1, 2, 3], + // }, + // // expectedJSON: `{"k":{"a":null,"b":1,"c":1.000000,"d":"hello","e":true,"f":"${nowTime.toISOString()}","g":[1,2,3]}}`, + // expectedJSON: `{"k":{"a":null,"b":1,"c":1.000000,"d":"hello","e":true,"g":[1,2,3]}}`, + // expectPanic: false, + // }, + + // unsupported map types + // { + // caseName: 'int map', + // input: new Map( + // Object.entries({ + // 1: 1, + // 2: 2, + // }), + // ), + // expectedJSON: `{}`, + // expectPanic: true, + // }, + + // supported JSON types + { + caseName: 'json.Text', + input: new Text(), + expectedJSON: `{"k":[]}`, + expectPanic: false, + }, + { + caseName: 'json.Tree', + input: new Tree({ + type: 'doc', + children: [ + { type: 'p', children: [{ type: 'text', value: 'ab' }] }, + ], + }), + expectedJSON: `{"k":{"type":"doc","children":[{"type":"p","children":[{"type":"text","value":"ab"}]}]}}`, + expectPanic: false, + }, + { + caseName: 'json.Counter', + input: new Counter(CounterType.IntegerCnt, 1), + expectedJSON: `{"k":1}`, + expectPanic: false, + }, + + // supported struct types + { + caseName: 'struct', + input: { M: 1 } as MyStruct, + expectedJSON: `{"k":{"M":1}}`, + expectPanic: false, + }, + { + caseName: 'struct with slice', + input: { M: [1, 2, 3] }, + expectedJSON: `{"k":{"M":[1,2,3]}}`, + expectPanic: false, + }, + { + caseName: 'struct array', + input: [{ M: 1 }, { M: 2 }] as Array, + expectedJSON: `{"k":[{"M":1},{"M":2}]}`, + expectPanic: false, + }, + { + caseName: 'anonymous struct', + input: { M: 'hello' }, + expectedJSON: `{"k":{"M":"hello"}}`, + expectPanic: false, + }, + // { + // caseName: 'struct with embedded struct', + // input: { T1: { M: 'a' } as T1, t1: { M: 'b' } as t1, M: 'c' } as T2, + // expectedJSON: `{"k":{"M":"c","T1":{"M":"a"}}}`, + // expectPanic: false, + // }, + + // // unsupported struct types + // { + // caseName: 'struct with unsupported map', + // input: new Map([ + // ['a', 1], + // ['b', 2], + // ]), + // expectedJSON: `{}`, + // expectPanic: true, + // }, + // { + // caseName: 'func', + // input: (a: number, b: number) => a + b, + // expectedJSON: `{}`, + // expectPanic: true, + // }, + ]; + + for (const test of tests) { + await c1.deactivate(); + await c1.activate(); + + const docKey = toDocKey( + `${task.name}-${test.caseName}-${new Date().getTime()}`, + ); + const doc = new yorkie.Document(docKey); + + const action = async () => { + await c1.attach(doc, { + initialRoot: { + k: test.input, + }, + }); + }; + + if (test.expectPanic) { + assert.throws(action); + } else { + await action(); + assert.equal(doc.toSortedJSON(), test.expectedJSON); + } + } + + await c1.deactivate(); + }); + }); }); From d36f2f4720615f66c1bf46b9e10509e370412bf3 Mon Sep 17 00:00:00 2001 From: Youngteac Hong Date: Tue, 22 Oct 2024 11:50:17 +0900 Subject: [PATCH 2/8] Revise the codes --- packages/sdk/src/client/client.ts | 9 +- .../sdk/test/integration/document_test.ts | 439 +----------------- 2 files changed, 20 insertions(+), 428 deletions(-) diff --git a/packages/sdk/src/client/client.ts b/packages/sdk/src/client/client.ts index 88cd633f8..b8f9fa147 100644 --- a/packages/sdk/src/client/client.ts +++ b/packages/sdk/src/client/client.ts @@ -294,10 +294,10 @@ export class Client { * this client will synchronize the given document. */ public attach( - doc: Document, + doc: Document, options: { initialPresence?: P; - initialRoot?: P; + initialRoot?: T; syncMode?: SyncMode; } = {}, ): Promise> { @@ -370,8 +370,9 @@ export class Client { const crdtObject = doc.getRootObject(); if (options.initialRoot) { doc.update((root) => { - for (const [k, v] of Object.entries(options.initialRoot || {})) { - if (crdtObject.get(k) === undefined) { + for (const [k, v] of Object.entries(options.initialRoot!)) { + if (!crdtObject.get(k)) { + // TODO(hackerwins): type cast (root as Record)[k] = v; } } diff --git a/packages/sdk/test/integration/document_test.ts b/packages/sdk/test/integration/document_test.ts index b268955e9..a5ca4afd3 100644 --- a/packages/sdk/test/integration/document_test.ts +++ b/packages/sdk/test/integration/document_test.ts @@ -3,7 +3,7 @@ import yorkie, { Counter, Text, JSONArray, - SyncMode, Tree, + SyncMode, } from '@yorkie-js-sdk/src/yorkie'; import { testRPCAddr, @@ -1103,29 +1103,18 @@ describe('Document', function () { it('Can attach with InitialRoot', async function ({ task }) { const c1 = new yorkie.Client(testRPCAddr); const c2 = new yorkie.Client(testRPCAddr); - const c3 = new yorkie.Client(testRPCAddr); await c1.activate(); await c2.activate(); - await c3.activate(); const docKey = toDocKey(`${task.name}-${new Date().getTime()}`); - const doc1 = new yorkie.Document(docKey); - function toMap(obj: Record): Map { - return new Map(Object.entries(obj)); - } // 01. attach and initialize document + const doc1 = new yorkie.Document(docKey); await c1.attach(doc1, { initialRoot: { counter: new Counter(CounterType.IntegerCnt, 0), - content: new Map( - Object.entries({ - x: 1, - y: 1, - }), - ), + content: { x: 1, y: 1 }, }, }); - assert.equal(doc1.getStatus(), DocumentStatus.Attached); assert.equal( doc1.toSortedJSON(), '{"content":{"x":1,"y":1},"counter":0}', @@ -1137,16 +1126,10 @@ describe('Document', function () { await c2.attach(doc2, { initialRoot: { counter: new Counter(CounterType.IntegerCnt, 1), - content: new Map( - Object.entries({ - x: 2, - y: 2, - }), - ), - new: new Map([['k', 'v']]), + content: { x: 1, y: 2 }, + new: { k: 'v' }, }, }); - assert.equal(doc2.getStatus(), DocumentStatus.Attached); assert.equal( doc2.toSortedJSON(), '{"content":{"x":1,"y":1},"counter":0,"new":{"k":"v"}}', @@ -1154,72 +1137,6 @@ describe('Document', function () { await c1.deactivate(); await c2.deactivate(); - await c3.deactivate(); - }); - - it('Can attach with InitialRoot after key deletion', async function ({ - task, - }) { - const c1 = new yorkie.Client(testRPCAddr); - const c2 = new yorkie.Client(testRPCAddr); - const c3 = new yorkie.Client(testRPCAddr); - await c1.activate(); - await c2.activate(); - await c3.activate(); - - const docKey = toDocKey(`${task.name}-${new Date().getTime()}`); - const doc1 = new yorkie.Document(docKey); - // 01. attach and initialize document - await c1.attach(doc1, { - initialRoot: { - counter: new Counter(CounterType.IntegerCnt, 0), - content: new Map( - Object.entries({ - x: 1, - y: 1, - }), - ), - }, - }); - assert.equal(doc1.getStatus(), DocumentStatus.Attached); - assert.equal( - doc1.toSortedJSON(), - '{"content":{"x":1,"y":1},"counter":0}', - ); - await c1.sync(); - - // 02. client2 attach with initialRoot and delete elements - const doc2 = new yorkie.Document(docKey); - await c2.attach(doc2); - assert.equal(doc2.getStatus(), DocumentStatus.Attached); - doc2.update((root) => { - delete root['counter']; - delete root['content']; - }); - assert.equal(doc2.toSortedJSON(), '{}'); - await c2.sync(); - - const doc3 = new yorkie.Document(docKey); - await c3.attach(doc3, { - initialRoot: { - counter: new Counter(CounterType.IntegerCnt, 3), - content: new Map( - Object.entries({ - x: 3, - y: 3, - }), - ), - }, - }); - assert.equal(doc3.getStatus(), DocumentStatus.Attached); - assert.equal( - doc3.toSortedJSON(), - '{"content":{"x":3,"y":3},"counter":3}', - ); - - await c1.deactivate(); - await c2.deactivate(); - await c3.deactivate(); }); it('Can handle concurrent attach with InitialRoot', async function ({ @@ -1231,357 +1148,31 @@ describe('Document', function () { await c2.activate(); const docKey = toDocKey(`${task.name}-${new Date().getTime()}`); - const doc1 = new yorkie.Document(docKey); // 01. user1 attach with initialRoot and client doesn't sync - await c1.attach(doc1, { - initialRoot: { - first_writer: 'user1', - }, - }); - assert.equal(doc1.getStatus(), DocumentStatus.Attached); - assert.equal(doc1.toSortedJSON(), '{"first_writer":"user1"}'); + const doc1 = new yorkie.Document(docKey); + await c1.attach(doc1, { initialRoot: { writer: 'user1' } }); + assert.equal(doc1.toSortedJSON(), '{"writer":"user1"}'); // 02. user2 attach with initialRoot and client doesn't sync const doc2 = new yorkie.Document(docKey); - await c2.attach(doc2, { - initialRoot: { - first_writer: 'user2', - }, - }); - assert.equal(doc2.getStatus(), DocumentStatus.Attached); - assert.equal(doc2.toSortedJSON(), '{"first_writer":"user2"}'); + await c2.attach(doc2, { initialRoot: { writer: 'user2' } }); + assert.equal(doc2.toSortedJSON(), '{"writer":"user2"}'); // 03. user1 sync first and user2 seconds await c1.sync(); await c2.sync(); - // 04. user1's local document's first_writer was user1 - assert.equal(doc1.toSortedJSON(), '{"first_writer":"user1"}'); - assert.equal(doc2.toSortedJSON(), '{"first_writer":"user2"}'); + // 04. user1's local document's writer was user1 + assert.equal(doc1.toSortedJSON(), '{"writer":"user1"}'); + assert.equal(doc2.toSortedJSON(), '{"writer":"user2"}'); - // 05. user1's local document's first_writer is overwritten by user2 + // 05. user1's local document's writer is overwritten by user2 await c1.sync(); - assert.equal(doc1.toSortedJSON(), '{"first_writer":"user2"}'); + assert.equal(doc1.toSortedJSON(), '{"writer":"user2"}'); await c1.deactivate(); await c2.deactivate(); }); - - it('Can attach with InitialRoot by same key', async function ({ task }) { - const c1 = new yorkie.Client(testRPCAddr); - await c1.activate(); - - const docKey = toDocKey(`${task.name}-${new Date().getTime()}`); - const doc = new yorkie.Document(docKey); - - const k1 = 'key'; - const k2 = 'key'; - const k3 = 'key'; - const k4 = 'key'; - const k5 = 'key'; - - // Attach the document with initial root containing the same key multiple times - await c1.attach(doc, { - initialRoot: { - [k1]: 1, - [k2]: 2, - [k3]: 3, - [k4]: 4, - [k5]: 5, - }, - }); - - assert.equal(doc.getStatus(), DocumentStatus.Attached); - // The last value should be used when the same key is repeated - assert.equal(doc.toSortedJSON(), '{"key":5}'); - - await c1.deactivate(); - }); - - it('Can attach with InitialRoot conflict type', async function ({ task }) { - const c1 = new yorkie.Client(testRPCAddr); - const c2 = new yorkie.Client(testRPCAddr); - await c1.activate(); - await c2.activate(); - - const docKey1 = toDocKey(`${task.name}-doc1-${new Date().getTime()}`); - const docKey2 = toDocKey(`${task.name}-doc2-${new Date().getTime()}`); - - const doc1 = new yorkie.Document(docKey1); - - // 01. Attach with initialRoot and set counter - await c1.attach(doc1, { - initialRoot: { - k: new Counter(CounterType.LongCnt, 1), - }, - }); - assert.equal(doc1.getStatus(), DocumentStatus.Attached); - await c1.sync(); - - // 02. Attach with initialRoot and set text - const doc2 = new yorkie.Document(docKey2); - await c2.attach(doc2, { - initialRoot: { - k: new yorkie.Text(), - }, - }); - assert.equal(doc2.getStatus(), DocumentStatus.Attached); - await c2.sync(); - - // 03. Client2 tries to update the counter (this should fail as the type is a Text, not a Counter) - assert.throws(() => { - doc2.update((root) => { - root['k'].edit(0, 1, 'a'); - }); - }); - - await c1.deactivate(); - await c2.deactivate(); - }); - - it('Can attach with initialRoot support type', async function ({ task }) { - const c1 = new yorkie.Client(testRPCAddr); - await c1.activate(); - - type Myint = number; - - interface MyStruct { - M: Myint; - } - - interface t1 { - M: string; - } - - interface T1 { - M: string; - } - - interface T2 { - T1: T1; - t1: t1; - M: string; - } - - const nowTime = new Date(); - const tests = [ - // supported primitive types - { - caseName: 'nil', - input: null, - expectedJSON: `{"k":null}`, - expectPanic: false, - }, - { - caseName: 'int', - input: 1, - expectedJSON: `{"k":1}`, - expectPanic: false, - }, - { - caseName: 'int32', - input: 1, - expectedJSON: `{"k":1}`, - expectPanic: false, - }, - { - caseName: 'int64', - input: 1, - expectedJSON: `{"k":1}`, - expectPanic: false, - }, - { - caseName: 'float32', - input: 1.1, - expectedJSON: `{"k":1.1}`, - expectPanic: false, - }, - { - caseName: 'float64', - input: 1.1, - expectedJSON: `{"k":1.1}`, - expectPanic: false, - }, - { - caseName: 'string', - input: 'hello', - expectedJSON: `{"k":"hello"}`, - expectPanic: false, - }, - { - caseName: 'bool', - input: true, - expectedJSON: `{"k":true}`, - expectPanic: false, - }, - // { - // caseName: 'time', - // input: nowTime, - // expectedJSON: `{"k":"${nowTime.toISOString()}"}`, - // expectPanic: false, - // }, - { - caseName: 'Myint', - input: 1 as Myint, - expectedJSON: `{"k":1}`, - expectPanic: false, - }, - - // unsupported primitive types - // { caseName: 'int8', input: 1, expectedJSON: `{}`, expectPanic: true }, - - // supported slice, array types - { - caseName: 'int array', - input: [1, 2, 3], - expectedJSON: `{"k":[1,2,3]}`, - expectPanic: false, - }, - { - caseName: 'string array', - input: ['a', 'b', 'c'], - expectedJSON: `{"k":["a","b","c"]}`, - expectPanic: false, - }, - // { - // caseName: 'any array', - // input: [null, 1, 1.0, 'hello', true, nowTime, [1, 2, 3]], - // expectedJSON: `{"k":[null,1,1.000000,"hello",true,"${nowTime.toISOString()}",[1,2,3]]}`, - // expectPanic: false, - // }, - - // supported map types - // { - // caseName: 'string:any map', - // input: { - // a: null, - // b: 1, - // c: 1.0, - // d: 'hello', - // e: true, - // // f: nowTime, - // g: [1, 2, 3], - // }, - // // expectedJSON: `{"k":{"a":null,"b":1,"c":1.000000,"d":"hello","e":true,"f":"${nowTime.toISOString()}","g":[1,2,3]}}`, - // expectedJSON: `{"k":{"a":null,"b":1,"c":1.000000,"d":"hello","e":true,"g":[1,2,3]}}`, - // expectPanic: false, - // }, - - // unsupported map types - // { - // caseName: 'int map', - // input: new Map( - // Object.entries({ - // 1: 1, - // 2: 2, - // }), - // ), - // expectedJSON: `{}`, - // expectPanic: true, - // }, - - // supported JSON types - { - caseName: 'json.Text', - input: new Text(), - expectedJSON: `{"k":[]}`, - expectPanic: false, - }, - { - caseName: 'json.Tree', - input: new Tree({ - type: 'doc', - children: [ - { type: 'p', children: [{ type: 'text', value: 'ab' }] }, - ], - }), - expectedJSON: `{"k":{"type":"doc","children":[{"type":"p","children":[{"type":"text","value":"ab"}]}]}}`, - expectPanic: false, - }, - { - caseName: 'json.Counter', - input: new Counter(CounterType.IntegerCnt, 1), - expectedJSON: `{"k":1}`, - expectPanic: false, - }, - - // supported struct types - { - caseName: 'struct', - input: { M: 1 } as MyStruct, - expectedJSON: `{"k":{"M":1}}`, - expectPanic: false, - }, - { - caseName: 'struct with slice', - input: { M: [1, 2, 3] }, - expectedJSON: `{"k":{"M":[1,2,3]}}`, - expectPanic: false, - }, - { - caseName: 'struct array', - input: [{ M: 1 }, { M: 2 }] as Array, - expectedJSON: `{"k":[{"M":1},{"M":2}]}`, - expectPanic: false, - }, - { - caseName: 'anonymous struct', - input: { M: 'hello' }, - expectedJSON: `{"k":{"M":"hello"}}`, - expectPanic: false, - }, - // { - // caseName: 'struct with embedded struct', - // input: { T1: { M: 'a' } as T1, t1: { M: 'b' } as t1, M: 'c' } as T2, - // expectedJSON: `{"k":{"M":"c","T1":{"M":"a"}}}`, - // expectPanic: false, - // }, - - // // unsupported struct types - // { - // caseName: 'struct with unsupported map', - // input: new Map([ - // ['a', 1], - // ['b', 2], - // ]), - // expectedJSON: `{}`, - // expectPanic: true, - // }, - // { - // caseName: 'func', - // input: (a: number, b: number) => a + b, - // expectedJSON: `{}`, - // expectPanic: true, - // }, - ]; - - for (const test of tests) { - await c1.deactivate(); - await c1.activate(); - - const docKey = toDocKey( - `${task.name}-${test.caseName}-${new Date().getTime()}`, - ); - const doc = new yorkie.Document(docKey); - - const action = async () => { - await c1.attach(doc, { - initialRoot: { - k: test.input, - }, - }); - }; - - if (test.expectPanic) { - assert.throws(action); - } else { - await action(); - assert.equal(doc.toSortedJSON(), test.expectedJSON); - } - } - - await c1.deactivate(); - }); }); }); From dc4a048459aca795edff9e536b8fa1eb3b40ba26 Mon Sep 17 00:00:00 2001 From: raararaara Date: Tue, 22 Oct 2024 16:09:59 +0900 Subject: [PATCH 3/8] Add test of Document with initialRoot --- .../sdk/test/integration/document_test.ts | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/packages/sdk/test/integration/document_test.ts b/packages/sdk/test/integration/document_test.ts index a5ca4afd3..d528b945a 100644 --- a/packages/sdk/test/integration/document_test.ts +++ b/packages/sdk/test/integration/document_test.ts @@ -4,6 +4,9 @@ import yorkie, { Text, JSONArray, SyncMode, + PrimitiveValue, + Tree, + JSONElement, } from '@yorkie-js-sdk/src/yorkie'; import { testRPCAddr, @@ -22,6 +25,8 @@ import { import { OperationInfo } from '@yorkie-js-sdk/src/document/operation/operation'; import { YorkieError } from '@yorkie-js-sdk/src/util/error'; import { CounterType } from '@yorkie-js-sdk/src/document/crdt/counter'; +import {LeafElement} from "@yorkie-js-sdk/src/document/json/element"; +import Long from "long"; describe('Document', function () { afterEach(() => { @@ -1174,5 +1179,79 @@ describe('Document', function () { await c1.deactivate(); await c2.deactivate(); }); + + describe('With various types', () => { + interface TestCase { + caseName: string; + input: JSONElement; + expectedJSON: string; + } + + // TODO(raararaara): Need test cases for Double, Bytes, Date, Object, Array + const testCases: Array = [ + // Custom CRDT Types + { + caseName: 'json.Tree', + input: new Tree({ + type: 'doc', + children: [ + { type: 'p', children: [{ type: 'text', value: 'ab' }] }, + ], + }), + expectedJSON: `{"k":{"type":"doc","children":[{"type":"p","children":[{"type":"text","value":"ab"}]}]}}`, + }, + { + caseName: 'json.Text', + input: new Text(), + expectedJSON: `{"k":[]}`, + }, + { + caseName: 'json.Counter', + input: new Counter(CounterType.IntegerCnt, 1), + expectedJSON: `{"k":1}`, + }, + // Primitives + { + caseName: 'null', + input: null, + expectedJSON: `{"k":null}`, + }, + { + caseName: 'boolean', + input: true, + expectedJSON: `{"k":true}`, + }, + { + caseName: 'number', + input: 1, + expectedJSON: `{"k":1}`, + }, + { + caseName: 'Long', + input: Long.MAX_VALUE, + expectedJSON: `{"k":9223372036854775807}`, + }, + ]; + + it('Can support various types', async function ({ task }) { + for (const { caseName, input, expectedJSON } of testCases) { + const c1 = new yorkie.Client(testRPCAddr); + await c1.activate(); + const docKey = toDocKey( + `${task.name}-${caseName}-${new Date().getTime()}`, + ); + const doc = new yorkie.Document(docKey); + + await c1.attach(doc, { + initialRoot: { + k: input, + }, + }); + assert.equal(doc.toSortedJSON(), expectedJSON); + + await c1.deactivate(); + } + }); + }); }); }); From aa9f92f377ecb4cc37d42275b01ef3aef48869b6 Mon Sep 17 00:00:00 2001 From: raararaara Date: Tue, 22 Oct 2024 16:14:29 +0900 Subject: [PATCH 4/8] Fix lint --- packages/sdk/test/integration/document_test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/sdk/test/integration/document_test.ts b/packages/sdk/test/integration/document_test.ts index d528b945a..595f007f3 100644 --- a/packages/sdk/test/integration/document_test.ts +++ b/packages/sdk/test/integration/document_test.ts @@ -4,7 +4,6 @@ import yorkie, { Text, JSONArray, SyncMode, - PrimitiveValue, Tree, JSONElement, } from '@yorkie-js-sdk/src/yorkie'; @@ -25,8 +24,7 @@ import { import { OperationInfo } from '@yorkie-js-sdk/src/document/operation/operation'; import { YorkieError } from '@yorkie-js-sdk/src/util/error'; import { CounterType } from '@yorkie-js-sdk/src/document/crdt/counter'; -import {LeafElement} from "@yorkie-js-sdk/src/document/json/element"; -import Long from "long"; +import Long from 'long'; describe('Document', function () { afterEach(() => { From e0ba8feb23c5b0c1b2e6a04d68c628fd4c43397f Mon Sep 17 00:00:00 2001 From: Youngteac Hong Date: Tue, 22 Oct 2024 17:50:45 +0900 Subject: [PATCH 5/8] Revise the codes --- packages/sdk/src/document/json/element.ts | 5 -- packages/sdk/src/document/json/object.ts | 24 ---------- .../sdk/test/integration/document_test.ts | 48 +++++++++++++------ 3 files changed, 33 insertions(+), 44 deletions(-) diff --git a/packages/sdk/src/document/json/element.ts b/packages/sdk/src/document/json/element.ts index 2d70d83bc..65e679414 100644 --- a/packages/sdk/src/document/json/element.ts +++ b/packages/sdk/src/document/json/element.ts @@ -166,11 +166,6 @@ export function buildCRDTElement( } else if (value instanceof Tree) { element = CRDTTree.create(value.buildRoot(context), createdAt); value.initialize(context, element as CRDTTree); - } else if (value instanceof Map) { - element = CRDTObject.create( - createdAt, - ObjectProxy.buildObjectMembersFromMap(context, value!), - ); } else { element = CRDTObject.create( createdAt, diff --git a/packages/sdk/src/document/json/object.ts b/packages/sdk/src/document/json/object.ts index 2646959b6..63ce8369d 100644 --- a/packages/sdk/src/document/json/object.ts +++ b/packages/sdk/src/document/json/object.ts @@ -169,30 +169,6 @@ export class ObjectProxy { ); } - /** - * `buildObjectMembersFromMap` 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 buildObjectMembersFromMap( - context: ChangeContext, - value: Map, - ): { [key: string]: CRDTElement } { - const members: { [key: string]: CRDTElement } = {}; - for (const [k, v] of value) { - if (k.includes('.')) { - throw new YorkieError( - Code.ErrInvalidObjectKey, - `key must not contain the '.'.`, - ); - } - const createdAt = context.issueTimeTicket(); - members[k] = buildCRDTElement(context, v, createdAt); - } - return members; - } - /** * `buildObjectMembers` constructs an object where all values from the * user-provided object are transformed into CRDTElements. diff --git a/packages/sdk/test/integration/document_test.ts b/packages/sdk/test/integration/document_test.ts index 595f007f3..08ce19d28 100644 --- a/packages/sdk/test/integration/document_test.ts +++ b/packages/sdk/test/integration/document_test.ts @@ -1180,16 +1180,14 @@ describe('Document', function () { describe('With various types', () => { interface TestCase { - caseName: string; + name: string; input: JSONElement; expectedJSON: string; } - // TODO(raararaara): Need test cases for Double, Bytes, Date, Object, Array const testCases: Array = [ - // Custom CRDT Types { - caseName: 'json.Tree', + name: 'json.Tree', input: new Tree({ type: 'doc', children: [ @@ -1199,40 +1197,60 @@ describe('Document', function () { expectedJSON: `{"k":{"type":"doc","children":[{"type":"p","children":[{"type":"text","value":"ab"}]}]}}`, }, { - caseName: 'json.Text', + name: 'json.Text', input: new Text(), expectedJSON: `{"k":[]}`, }, { - caseName: 'json.Counter', + name: 'json.Counter', input: new Counter(CounterType.IntegerCnt, 1), expectedJSON: `{"k":1}`, }, - // Primitives { - caseName: 'null', + name: 'null', input: null, expectedJSON: `{"k":null}`, }, { - caseName: 'boolean', + name: 'boolean', input: true, expectedJSON: `{"k":true}`, }, { - caseName: 'number', + name: 'number', input: 1, expectedJSON: `{"k":1}`, }, { - caseName: 'Long', + name: 'Long', input: Long.MAX_VALUE, expectedJSON: `{"k":9223372036854775807}`, }, + { + name: 'Object', + input: { k: 'v' }, + expectedJSON: `{"k":{"k":"v"}}`, + }, + { + name: 'Array', + input: [1, 2], + expectedJSON: `{"k":[1,2]}`, + }, + { + name: 'Bytes', + input: new Uint8Array([1, 2]), + expectedJSON: `{"k":1,2}`, + }, + // TODO(hackerwins): Encode Date type to JSON + // { + // name: 'Date', + // input: new Date(0), + // expectedJSON: `{"k":"1970-01-01T00:00:00.000Z"}`, + // }, ]; - it('Can support various types', async function ({ task }) { - for (const { caseName, input, expectedJSON } of testCases) { + for (const { name: caseName, input, expectedJSON } of testCases) { + it(`Can support various types: ${caseName}`, async function ({ task }) { const c1 = new yorkie.Client(testRPCAddr); await c1.activate(); const docKey = toDocKey( @@ -1248,8 +1266,8 @@ describe('Document', function () { assert.equal(doc.toSortedJSON(), expectedJSON); await c1.deactivate(); - } - }); + }); + } }); }); }); From 0ed6957bbab163c7f2b97cddfa2b4d9316b8e4ed Mon Sep 17 00:00:00 2001 From: raararaara Date: Tue, 22 Oct 2024 19:28:08 +0900 Subject: [PATCH 6/8] Add docType to test cases to clarify document type --- packages/sdk/src/client/client.ts | 4 +- .../sdk/test/integration/document_test.ts | 64 ++++++++++++------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/packages/sdk/src/client/client.ts b/packages/sdk/src/client/client.ts index b8f9fa147..f3fda8ee1 100644 --- a/packages/sdk/src/client/client.ts +++ b/packages/sdk/src/client/client.ts @@ -372,8 +372,8 @@ export class Client { doc.update((root) => { for (const [k, v] of Object.entries(options.initialRoot!)) { if (!crdtObject.get(k)) { - // TODO(hackerwins): type cast - (root as Record)[k] = v; + // TODO(raararaara): Need a way to accurately infer the type of `k` for indexing. + root[k as keyof T] = v; } } }); diff --git a/packages/sdk/test/integration/document_test.ts b/packages/sdk/test/integration/document_test.ts index 08ce19d28..eb2ea11cb 100644 --- a/packages/sdk/test/integration/document_test.ts +++ b/packages/sdk/test/integration/document_test.ts @@ -14,6 +14,7 @@ import { import { EventCollector, assertThrowsAsync, + Indexable, } from '@yorkie-js-sdk/test/helper/helper'; import type { CRDTElement } from '@yorkie-js-sdk/src/document/crdt/element'; import { @@ -1181,67 +1182,68 @@ describe('Document', function () { describe('With various types', () => { interface TestCase { name: string; - input: JSONElement; + input: JSONElement | Indexable; expectedJSON: string; } const testCases: Array = [ { - name: 'json.Tree', + name: 'tree', input: new Tree({ type: 'doc', children: [ { type: 'p', children: [{ type: 'text', value: 'ab' }] }, ], }), - expectedJSON: `{"k":{"type":"doc","children":[{"type":"p","children":[{"type":"text","value":"ab"}]}]}}`, + expectedJSON: `{"tree":{"type":"doc","children":[{"type":"p","children":[{"type":"text","value":"ab"}]}]}}`, }, { - name: 'json.Text', + name: 'text', input: new Text(), - expectedJSON: `{"k":[]}`, + expectedJSON: `{"text":[]}`, }, { - name: 'json.Counter', + name: 'counter', input: new Counter(CounterType.IntegerCnt, 1), - expectedJSON: `{"k":1}`, + expectedJSON: `{"counter":1}`, }, { name: 'null', input: null, - expectedJSON: `{"k":null}`, + expectedJSON: `{"null":null}`, }, { name: 'boolean', input: true, - expectedJSON: `{"k":true}`, + expectedJSON: `{"boolean":true}`, }, { name: 'number', input: 1, - expectedJSON: `{"k":1}`, + expectedJSON: `{"number":1}`, }, { - name: 'Long', + name: 'long', input: Long.MAX_VALUE, - expectedJSON: `{"k":9223372036854775807}`, + expectedJSON: `{"long":9223372036854775807}`, }, { - name: 'Object', + name: 'object', input: { k: 'v' }, - expectedJSON: `{"k":{"k":"v"}}`, + expectedJSON: `{"object":{"k":"v"}}`, }, { - name: 'Array', + name: 'array', input: [1, 2], - expectedJSON: `{"k":[1,2]}`, + expectedJSON: `{"array":[1,2]}`, }, + // TODO(hackerwins): We need to consider the case where the value is + // a byte array and a date. { - name: 'Bytes', + name: 'bytes', input: new Uint8Array([1, 2]), - expectedJSON: `{"k":1,2}`, + expectedJSON: `{"bytes":1,2}`, }, - // TODO(hackerwins): Encode Date type to JSON // { // name: 'Date', // input: new Date(0), @@ -1249,18 +1251,32 @@ describe('Document', function () { // }, ]; - for (const { name: caseName, input, expectedJSON } of testCases) { - it(`Can support various types: ${caseName}`, async function ({ task }) { + for (const { name: name, input, expectedJSON } of testCases) { + it(`Can support various types: ${name}`, async function ({ task }) { const c1 = new yorkie.Client(testRPCAddr); await c1.activate(); const docKey = toDocKey( - `${task.name}-${caseName}-${new Date().getTime()}`, + `${task.name}-${name}-${new Date().getTime()}`, ); - const doc = new yorkie.Document(docKey); + + type docType = { + tree?: Tree; + text?: Text; + counter?: Counter; + null?: null; + boolean?: boolean; + number?: number; + long?: Long; + object?: Object; + array?: Array; + bytes?: Uint8Array; + // date: Date; + }; + const doc = new yorkie.Document(docKey); await c1.attach(doc, { initialRoot: { - k: input, + [name]: input, }, }); assert.equal(doc.toSortedJSON(), expectedJSON); From 8a2ecebc5af6b83d265c18750483ada02c33ef07 Mon Sep 17 00:00:00 2001 From: raararaara Date: Tue, 22 Oct 2024 19:31:23 +0900 Subject: [PATCH 7/8] Fix build error --- packages/sdk/src/client/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/client/client.ts b/packages/sdk/src/client/client.ts index f3fda8ee1..c61ec8fae 100644 --- a/packages/sdk/src/client/client.ts +++ b/packages/sdk/src/client/client.ts @@ -373,7 +373,7 @@ export class Client { for (const [k, v] of Object.entries(options.initialRoot!)) { if (!crdtObject.get(k)) { // TODO(raararaara): Need a way to accurately infer the type of `k` for indexing. - root[k as keyof T] = v; + (root as Record)[k] = v; } } }); From d65e9049e729d4971d046de614e6564dd4d45fbe Mon Sep 17 00:00:00 2001 From: Youngteac Hong Date: Wed, 23 Oct 2024 15:00:49 +0900 Subject: [PATCH 8/8] Revise the codes --- packages/sdk/src/client/client.ts | 11 ++++++----- packages/sdk/test/integration/document_test.ts | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/sdk/src/client/client.ts b/packages/sdk/src/client/client.ts index c61ec8fae..1c6cabac8 100644 --- a/packages/sdk/src/client/client.ts +++ b/packages/sdk/src/client/client.ts @@ -296,8 +296,8 @@ export class Client { public attach( doc: Document, options: { - initialPresence?: P; initialRoot?: T; + initialPresence?: P; syncMode?: SyncMode; } = {}, ): Promise> { @@ -369,11 +369,12 @@ export class Client { const crdtObject = doc.getRootObject(); if (options.initialRoot) { + const initialRoot = options.initialRoot; doc.update((root) => { - for (const [k, v] of Object.entries(options.initialRoot!)) { - if (!crdtObject.get(k)) { - // TODO(raararaara): Need a way to accurately infer the type of `k` for indexing. - (root as Record)[k] = v; + for (const [k, v] of Object.entries(initialRoot)) { + if (!crdtObject.has(k)) { + const key = k as keyof T; + root[key] = v as any; } } }); diff --git a/packages/sdk/test/integration/document_test.ts b/packages/sdk/test/integration/document_test.ts index eb2ea11cb..b3df7b787 100644 --- a/packages/sdk/test/integration/document_test.ts +++ b/packages/sdk/test/integration/document_test.ts @@ -1259,7 +1259,7 @@ describe('Document', function () { `${task.name}-${name}-${new Date().getTime()}`, ); - type docType = { + type DocType = { tree?: Tree; text?: Text; counter?: Counter; @@ -1267,12 +1267,12 @@ describe('Document', function () { boolean?: boolean; number?: number; long?: Long; - object?: Object; + object?: { k: string }; array?: Array; bytes?: Uint8Array; // date: Date; }; - const doc = new yorkie.Document(docKey); + const doc = new yorkie.Document(docKey); await c1.attach(doc, { initialRoot: {