diff --git a/src/client/client.ts b/src/client/client.ts index b29b64d20..25e5fdf91 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -954,9 +954,12 @@ export class Client implements Observable { .then((res) => { const respPack = converter.fromChangePack

(res.changePack!); - // (chacha912, hackerwins): If syncLoop already executed with + // NOTE(chacha912, hackerwins): If syncLoop already executed with // PushPull, ignore the response when the syncMode is PushOnly. - if (respPack.hasChanges() && syncMode === SyncMode.PushOnly) { + if ( + respPack.hasChanges() && + attachment.syncMode === SyncMode.PushOnly + ) { return doc; } @@ -965,7 +968,7 @@ export class Client implements Observable { type: ClientEventType.DocumentSynced, value: DocumentSyncResultType.Synced, }); - // (chacha912): If a document has been removed, watchStream should + // NOTE(chacha912): If a document has been removed, watchStream should // be disconnected to not receive an event for that document. if (doc.getStatus() === DocumentStatus.Removed) { this.detachInternal(doc.getKey()); diff --git a/test/integration/client_test.ts b/test/integration/client_test.ts index 2f754bfbb..19c3425ad 100644 --- a/test/integration/client_test.ts +++ b/test/integration/client_test.ts @@ -6,6 +6,7 @@ import yorkie, { DocumentSyncResultType, DocEventType, ClientEventType, + Tree, } from '@yorkie-js-sdk/src/yorkie'; import { EventCollector } from '@yorkie-js-sdk/test/helper/helper'; import { @@ -545,4 +546,87 @@ describe.sequential('Client', function () { await c1.deactivate(); }); + + it('Should prevent remote changes in push-only 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: any) => { + 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 push-only mode, remote-change events should not occur. + c2.pauseRemoteChanges(d2); + 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, 100); // Keep the push-only state. + }); + unsub3(); + assert.isFalse(remoteChangeOccured); + + c2.resumeRemoteChanges(d2); + + d2.update((root: any) => { + 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(); + }); });