diff --git a/src/client/client.ts b/src/client/client.ts index 14d458f4d..0335b463c 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -695,7 +695,8 @@ export class Client { // PushPull, ignore the response when the syncMode is PushOnly. if ( respPack.hasChanges() && - attachment.syncMode === SyncMode.RealtimePushOnly + (attachment.syncMode === SyncMode.RealtimePushOnly || + attachment.syncMode === SyncMode.RealtimeSyncOff) ) { return doc; } diff --git a/test/integration/client_test.ts b/test/integration/client_test.ts index 84ae66096..76e5a5676 100644 --- a/test/integration/client_test.ts +++ b/test/integration/client_test.ts @@ -610,6 +610,89 @@ describe.sequential('Client', function () { await c2.deactivate(); }); + it('Should prevent remote changes in sync-off mode', 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 d1 = new yorkie.Document<{ tree: Tree }>(docKey); + const d2 = new yorkie.Document<{ tree: Tree }>(docKey); + await c1.attach(d1); + await c2.attach(d2); + + const eventCollectorD1 = new EventCollector(); + const eventCollectorD2 = new EventCollector(); + const unsub1 = d1.subscribe((event) => { + eventCollectorD1.add(event.type); + }); + const unsub2 = d2.subscribe((event) => { + eventCollectorD2.add(event.type); + }); + + d1.update((root) => { + root.tree = new Tree({ + type: 'doc', + children: [ + { + type: 'p', + children: [{ type: 'text', value: '12' }], + }, + { + type: 'p', + children: [{ type: 'text', value: '34' }], + }, + ], + }); + }); + await eventCollectorD2.waitAndVerifyNthEvent(1, DocEventType.RemoteChange); + + assert.equal(d1.getRoot().tree.toXML(), '

12

34

'); + assert.equal(d2.getRoot().tree.toXML(), '

12

34

'); + + d1.update((root) => { + root.tree.edit(2, 2, { type: 'text', value: 'a' }); + }); + await c1.sync(); + + // Simulate the situation in the runSyncLoop where a pushpull request has been sent + // but a response has not yet been received. + c2.sync(); + + // In sync-off mode, remote-change events should not occur. + c2.changeSyncMode(d2, SyncMode.RealtimeSyncOff); + let remoteChangeOccured = false; + const unsub3 = d2.subscribe((event) => { + if (event.type === DocEventType.RemoteChange) { + remoteChangeOccured = true; + } + }); + await new Promise((res) => { + // TODO(chacha912): We need to clean up this later because it is non-deterministic. + setTimeout(res, 0); // Keep the sync-off state. + }); + unsub3(); + assert.isFalse(remoteChangeOccured); + + c2.changeSyncMode(d2, SyncMode.Realtime); + + d2.update((root) => { + root.tree.edit(2, 2, { type: 'text', value: 'b' }); + }); + await eventCollectorD1.waitAndVerifyNthEvent(3, DocEventType.RemoteChange); + + assert.equal(d1.getRoot().tree.toXML(), '

1ba2

34

'); + assert.equal(d2.getRoot().tree.toXML(), '

1ba2

34

'); + + unsub1(); + unsub2(); + await c1.deactivate(); + await c2.deactivate(); + }); + it('Should avoid unnecessary syncs in push-only mode', async function ({ task, }) {