Skip to content

Commit

Permalink
Fix errors when editing Tree due to missing insPrevID in CRDTTree (#756)
Browse files Browse the repository at this point in the history
When performing `CRDTTree.edit()`, the edits are reflected by deepcopying the CRDTTreeNodes for the given range.
This commit adds information about `insPrevID` and `insNextID` during the `deepcopy()` process to ensure that the correct location is returned from the correct path.

---------

Co-authored-by: Yourim Cha <[email protected]>
  • Loading branch information
raararaara and chacha912 authored Feb 26, 2024
1 parent 0bc7ce1 commit 8b6ae97
Show file tree
Hide file tree
Showing 2 changed files with 259 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/document/crdt/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,8 @@ export class CRDTTreeNode extends IndexTreeNode<CRDTTreeNode> {
childClone.parent = clone;
return childClone;
});
clone.insPrevID = this.insPrevID;
clone.insNextID = this.insNextID;
return clone;
}

Expand Down
257 changes: 257 additions & 0 deletions test/integration/tree_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { describe, it, assert } from 'vitest';
import yorkie, { Tree } from '@yorkie-js-sdk/src/yorkie';
import {
testRPCAddr,
toDocKey,
withTwoClientsAndDocuments,
} from '@yorkie-js-sdk/test/integration/integration_helper';
Expand Down Expand Up @@ -1481,6 +1482,262 @@ describe('Tree.style', function () {
unsub();
}, task.name);
});

it('Can handle client reload case', async function ({ task }) {
type TestDoc = { t: Tree; num: number };
const docKey = toDocKey(`${task.name}-${new Date().getTime()}`);

const d1 = new yorkie.Document<TestDoc>(docKey);
const d2 = new yorkie.Document<TestDoc>(docKey);

const c1 = new yorkie.Client(testRPCAddr);
const c2 = new yorkie.Client(testRPCAddr);

await c1.activate();
await c2.activate();

await c1.attach(d1, { isRealtimeSync: false });
await c2.attach(d2, { isRealtimeSync: false });

// Perform a dummy update to apply changes up to the snapshot threshold.
const snapshotThreshold = 500;
for (let idx = 0; idx < snapshotThreshold; idx++) {
d1.update((root) => {
root.num = 0;
});
}

// Start scenario.
d1.update((root) => {
root.t = new Tree({
type: 'r',
children: [
{
type: 'c',
children: [
{
type: 'u',
children: [
{
type: 'p',
children: [
{
type: 'n',
children: [],
},
],
},
],
},
],
},
{
type: 'c',
children: [
{
type: 'p',
children: [
{
type: 'n',
children: [],
},
],
},
],
},
],
});
});
await c1.sync();
await c2.sync();
assert.equal(
d1.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n></n></p></c></r>`,
);
assert.equal(
d2.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n></n></p></c></r>`,
);

d1.update((r) => {
r.t.editByPath([1, 0, 0, 0], [1, 0, 0, 0], {
type: 'text',
value: '1',
});
r.t.editByPath([1, 0, 0, 1], [1, 0, 0, 1], {
type: 'text',
value: '2',
});
r.t.editByPath([1, 0, 0, 2], [1, 0, 0, 2], {
type: 'text',
value: '3',
});
r.t.editByPath([1, 0, 0, 2], [1, 0, 0, 2], {
type: 'text',
value: ' ',
});
r.t.editByPath([1, 0, 0, 3], [1, 0, 0, 3], {
type: 'text',
value: '네이버랑 ',
});
});
await c1.sync();
await c2.sync();
assert.equal(
d1.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>12 네이버랑 3</n></p></c></r>`,
);
assert.equal(
d2.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>12 네이버랑 3</n></p></c></r>`,
);

d2.update((r) => {
r.t.editByPath([1, 0, 0, 1], [1, 0, 0, 8], {
type: 'text',
value: ' 2 네이버랑 ',
});
r.t.editByPath([1, 0, 0, 2], [1, 0, 0, 2], {
type: 'text',
value: 'ㅋ',
});
r.t.editByPath([1, 0, 0, 2], [1, 0, 0, 3], {
type: 'text',
value: '카',
});
r.t.editByPath([1, 0, 0, 2], [1, 0, 0, 3], {
type: 'text',
value: '캌',
});
r.t.editByPath([1, 0, 0, 2], [1, 0, 0, 3], {
type: 'text',
value: '카카',
});
r.t.editByPath([1, 0, 0, 3], [1, 0, 0, 4], {
type: 'text',
value: '캉',
});
r.t.editByPath([1, 0, 0, 3], [1, 0, 0, 4], {
type: 'text',
value: '카오',
});
r.t.editByPath([1, 0, 0, 4], [1, 0, 0, 5], {
type: 'text',
value: '올',
});
r.t.editByPath([1, 0, 0, 4], [1, 0, 0, 5], {
type: 'text',
value: '오라',
});
r.t.editByPath([1, 0, 0, 5], [1, 0, 0, 6], {
type: 'text',
value: '랑',
});
r.t.editByPath([1, 0, 0, 6], [1, 0, 0, 6], {
type: 'text',
value: ' ',
});
});
await c2.sync();
await c1.sync();
assert.equal(
d1.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>1 카카오랑 2 네이버랑 3</n></p></c></r>`,
);
assert.equal(
d2.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>1 카카오랑 2 네이버랑 3</n></p></c></r>`,
);

d1.update((r) => {
r.t.editByPath([1, 0, 0, 13], [1, 0, 0, 14]);
r.t.editByPath([1, 0, 0, 12], [1, 0, 0, 13]);
});
await c1.sync();
await c2.sync();
assert.equal(
d1.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>1 카카오랑 2 네이버3</n></p></c></r>`,
);
assert.equal(
d2.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>1 카카오랑 2 네이버3</n></p></c></r>`,
);

d2.update((r) => {
r.t.editByPath([1, 0, 0, 6], [1, 0, 0, 7]);
r.t.editByPath([1, 0, 0, 5], [1, 0, 0, 6]);
});
await c2.sync();
await c1.sync();
assert.equal(
d1.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>1 카카오2 네이버3</n></p></c></r>`,
);
assert.equal(
d2.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>1 카카오2 네이버3</n></p></c></r>`,
);

d1.update((r) => {
r.t.editByPath([1, 0, 0, 9], [1, 0, 0, 10]);
});
await c1.sync();
await c2.sync();
assert.equal(
d1.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>1 카카오2 네이3</n></p></c></r>`,
);
assert.equal(
d2.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>1 카카오2 네이3</n></p></c></r>`,
);

// A new client has been added.
const d3 = new yorkie.Document<TestDoc>(docKey);
const c3 = new yorkie.Client(testRPCAddr);
await c3.activate();
await c3.attach(d3, { isRealtimeSync: false });
assert.equal(
d3.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>1 카카오2 네이3</n></p></c></r>`,
);
await c2.sync();

d3.update((r) => {
r.t.editByPath([1, 0, 0, 4], [1, 0, 0, 5]);
r.t.editByPath([1, 0, 0, 3], [1, 0, 0, 4]);
});
await c3.sync();
await c2.sync();
assert.equal(
d3.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>1 카2 네이3</n></p></c></r>`,
);
assert.equal(
d2.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>1 카2 네이3</n></p></c></r>`,
);

d3.update((r) => {
r.t.editByPath([1, 0, 0, 2], [1, 0, 0, 3]);
});

await c3.sync();
await c2.sync();
assert.equal(
d3.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>1 2 네이3</n></p></c></r>`,
);
assert.equal(
d2.getRoot().t.toXML(),
/*html*/ `<r><c><u><p><n></n></p></u></c><c><p><n>1 2 네이3</n></p></c></r>`,
);

await c1.deactivate();
await c2.deactivate();
await c3.deactivate();
});
});

describe('Tree.edit(concurrent overlapping range)', () => {
Expand Down

0 comments on commit 8b6ae97

Please sign in to comment.