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,
}) {