From 95a2feefa5234f9cf6a926ecedb73cad5435f868 Mon Sep 17 00:00:00 2001 From: Jeehyun Kim Date: Mon, 15 Apr 2024 17:19:23 +0900 Subject: [PATCH] Add RealtimeSyncOff and refactor interface of SyncMode (#170) * fix TC failures * update codecov version * Add RealtimeSyncOff and refactor interface of SyncMode * fix TC failures * downgrade codecov * fix handling Unwatched event --- .../com/example/texteditor/EditorViewModel.kt | 6 +- .../kotlin/dev/yorkie/core/ClientTest.kt | 350 ++++++++---------- .../kotlin/dev/yorkie/core/DocumentTest.kt | 5 +- .../kotlin/dev/yorkie/core/GCTest.kt | 19 +- .../kotlin/dev/yorkie/core/PresenceTest.kt | 103 +++--- .../kotlin/dev/yorkie/core/TestUtils.kt | 6 +- .../dev/yorkie/document/json/JsonTextTest.kt | 3 +- .../document/json/JsonTreeSplitMergeTest.kt | 19 +- .../dev/yorkie/document/json/JsonTreeTest.kt | 95 ++--- .../main/kotlin/dev/yorkie/core/Attachment.kt | 3 +- .../src/main/kotlin/dev/yorkie/core/Client.kt | 82 +--- .../kotlin/dev/yorkie/document/Document.kt | 5 +- .../test/kotlin/dev/yorkie/core/ClientTest.kt | 3 +- 13 files changed, 323 insertions(+), 376 deletions(-) diff --git a/examples/texteditor/src/main/java/com/example/texteditor/EditorViewModel.kt b/examples/texteditor/src/main/java/com/example/texteditor/EditorViewModel.kt index 848522c47..22ce78c1d 100644 --- a/examples/texteditor/src/main/java/com/example/texteditor/EditorViewModel.kt +++ b/examples/texteditor/src/main/java/com/example/texteditor/EditorViewModel.kt @@ -6,6 +6,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.gson.Gson import dev.yorkie.core.Client +import dev.yorkie.core.Client.SyncMode.Realtime +import dev.yorkie.core.Client.SyncMode.RealtimePushOnly import dev.yorkie.core.PresenceInfo import dev.yorkie.document.Document import dev.yorkie.document.Document.Event.PresenceChange @@ -137,11 +139,11 @@ class EditorViewModel(private val client: Client) : ViewModel(), YorkieEditText. } override fun handleHangulCompositionStart() { - client.pause(document) + client.changeSyncMode(document, RealtimePushOnly) } override fun handleHangulCompositionEnd() { - client.resume(document) + client.changeSyncMode(document, Realtime) } override fun onCleared() { diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt index 6e9b2b388..e0868bdba 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt @@ -6,10 +6,13 @@ import dev.yorkie.core.Client.DocumentSyncResult import dev.yorkie.core.Client.Event.DocumentChanged import dev.yorkie.core.Client.Event.DocumentSynced import dev.yorkie.core.Client.StreamConnectionStatus +import dev.yorkie.core.Client.SyncMode.Manual +import dev.yorkie.core.Client.SyncMode.Realtime +import dev.yorkie.core.Client.SyncMode.RealtimePushOnly +import dev.yorkie.core.Client.SyncMode.RealtimeSyncOff import dev.yorkie.document.Document import dev.yorkie.document.Document.Event.LocalChange import dev.yorkie.document.Document.Event.RemoteChange -import dev.yorkie.document.change.CheckPoint import dev.yorkie.document.json.JsonCounter import dev.yorkie.document.json.JsonPrimitive import dev.yorkie.document.json.JsonTreeTest.Companion.assertTreesXmlEquals @@ -169,7 +172,7 @@ class ClientTest { } @Test - fun test_change_realtime_sync() { + fun test_change_sync_mode_between_realtime_and_manual() { runBlocking { val client1 = createClient() val client2 = createClient() @@ -182,8 +185,8 @@ class ClientTest { // 01. c1 and c2 attach the doc with manual sync mode. // c1 updates the doc, but c2 doesn't get until call sync manually. - client1.attachAsync(document1, isRealTimeSync = false).await() - client2.attachAsync(document2, isRealTimeSync = false).await() + client1.attachAsync(document1, syncMode = Manual).await() + client2.attachAsync(document2, syncMode = Manual).await() document1.updateAsync { root, _ -> root["version"] = "v1" @@ -201,7 +204,7 @@ class ClientTest { client2.events.collect(client2Events::add) } - client2.resume(document2) + client2.changeSyncMode(document2, Realtime) withTimeout(GENERAL_TIMEOUT) { while (client2Events.isEmpty()) { delay(50) @@ -224,7 +227,7 @@ class ClientTest { collectJob.cancel() // 03. c2 changes the sync mode to manual sync mode again. - client2.pause(document2) + client2.changeSyncMode(document2, Manual) document1.updateAsync { root, _ -> root["version"] = "v3" }.await() @@ -246,7 +249,7 @@ class ClientTest { } @Test - fun test_applying_previous_changes_after_resume() = runBlocking { + fun test_applying_previous_changes_after_switching_to_realtime() = runBlocking { val client1 = createClient() val client2 = createClient() val documentKey = UUID.randomUUID().toString().toDocKey() @@ -272,7 +275,7 @@ class ClientTest { } // 01. c2 attach the doc with realtime sync mode at first. - client1.attachAsync(document1, isRealTimeSync = false).await() + client1.attachAsync(document1, syncMode = Manual).await() client2.attachAsync(document2).await() document1.updateAsync { root, _ -> root["version"] = "v1" @@ -282,7 +285,7 @@ class ClientTest { assertDocument2IsSynced("""{"version":"v1"}""") // 02. c2 pauses realtime sync mode. So, c2 doesn't get the changes of c1. - client2.pause(document2) + client2.changeSyncMode(document2, Manual) document1.updateAsync { root, _ -> root["version"] = "v2" }.await() @@ -293,7 +296,7 @@ class ClientTest { // 03. c2 resumes realtime sync mode. // c2 should be able to apply changes made to the document while c2 is not in realtime sync. client2Events.clear() - client2.resume(document2) + client2.changeSyncMode(document2, Realtime) assertDocument2IsSynced("""{"version":"v2"}""") // 04. c2 should automatically synchronize changes. @@ -315,222 +318,116 @@ class ClientTest { } @Test - fun test_change_sync_mode_in_manual_sync() { - runBlocking { - val client1 = createClient() - val client2 = createClient() - val client3 = createClient() - - val documentKey = UUID.randomUUID().toString().toDocKey() - val document1 = Document(documentKey) - val document2 = Document(documentKey) - val document3 = Document(documentKey) - - client1.activateAsync().await() - client2.activateAsync().await() - client3.activateAsync().await() - - // 01. client2, client2, client3 attach to the same document - client1.attachAsync(document1, isRealTimeSync = false).await() - client2.attachAsync(document2, isRealTimeSync = false).await() - client3.attachAsync(document3, isRealTimeSync = false).await() - - // 02. client1 and client2 sync with push-pull mode. - document1.updateAsync { root, _ -> - root["c1"] = 0 - }.await() - document2.updateAsync { root, _ -> - root["c2"] = 0 - }.await() - - client1.syncAsync().await() - client2.syncAsync().await() - client1.syncAsync().await() - assertJsonContentEquals("""{"c1":0,"c2":0}""", document1.toJson()) - assertJsonContentEquals("""{"c1":0,"c2":0}""", document2.toJson()) - - // 03. client1 and client2 sync with push-only mode. - // So, the changes of client1 and client2 are not reflected to each other. - // But, client3 can get the changes of client1 and client2, - // because client3 sync with push-pull mode. - document1.updateAsync { root, _ -> - root["c1"] = 1 - }.await() - document2.updateAsync { root, _ -> - root["c2"] = 1 - }.await() - - client1.syncAsync(document1, Client.SyncMode.PushOnly).await() - client2.syncAsync(document2, Client.SyncMode.PushOnly).await() - client3.syncAsync().await() - assertJsonContentEquals("""{"c1":1,"c2":0}""", document1.toJson()) - assertJsonContentEquals("""{"c1":0,"c2":1}""", document2.toJson()) - assertJsonContentEquals("""{"c1":1,"c2":1}""", document3.toJson()) - - // 04. client1 and client2 sync with push-pull mode. - client1.syncAsync().await() - client2.syncAsync().await() - assertJsonContentEquals("""{"c1":1,"c2":1}""", document1.toJson()) - assertJsonContentEquals("""{"c1":1,"c2":1}""", document2.toJson()) - - client1.detachAsync(document1).await() - client2.detachAsync(document2).await() - client3.detachAsync(document3).await() - client1.deactivateAsync().await() - client2.deactivateAsync().await() - client3.deactivateAsync().await() - document1.close() - document2.close() - client1.close() - client2.close() - } - } - - @Test - fun test_change_sync_mode_in_realtime_sync() { - withTwoClientsAndDocuments { client1, client2, document1, document2, key -> - val client3 = createClient() - client3.activateAsync().await() + fun test_change_sync_mode_in_realtime() { + withTwoClientsAndDocuments { c1, c2, d1, d2, key -> + val c3 = createClient() + c3.activateAsync().await() // 01. c1, c2, c3 attach to the same document in realtime sync. - val document3 = Document(key) - client3.attachAsync(document3).await() + val d3 = Document(key) + c3.attachAsync(d3).await() - val document1Events = mutableListOf() - val document2Events = mutableListOf() - val document3Ops = mutableListOf() + val d1Events = mutableListOf() + val d2Events = mutableListOf() + val d3Events = mutableListOf() val collectJobs = listOf( launch(start = CoroutineStart.UNDISPATCHED) { - document1.events.filterNot { it is Document.Event.PresenceChange } - .collect(document1Events::add) + d1.events.filterNot { it is Document.Event.PresenceChange } + .collect(d1Events::add) }, launch(start = CoroutineStart.UNDISPATCHED) { - document2.events.filterNot { it is Document.Event.PresenceChange } - .collect(document2Events::add) + d2.events.filterNot { it is Document.Event.PresenceChange } + .collect(d2Events::add) }, launch(start = CoroutineStart.UNDISPATCHED) { - document3.events.filterIsInstance().collect { event -> - document3Ops.addAll(event.changeInfo.operations) - } + d3.events.filterNot { it is Document.Event.PresenceChange } + .collect(d3Events::add) }, ) - // 02. c1, c2 sync in realtime. - document1.updateAsync { root, _ -> + // 02. [Step1] c1, c2, c3 sync in realtime. + d1.updateAsync { root, _ -> root["c1"] = 0 }.await() - document2.updateAsync { root, _ -> + d2.updateAsync { root, _ -> root["c2"] = 0 }.await() + d3.updateAsync { root, _ -> + root["c3"] = 0 + }.await() withTimeout(GENERAL_TIMEOUT) { - // size should be 2 since it has local-change and remote-change - while (document1Events.size < 2 || - document2Events.size < 2 || - document3Ops.size < 2 - ) { + // 1 LocalChange, 2 RemoteChanges + while (d1Events.size < 3 || d2Events.size < 3 || d3Events.size < 3) { delay(50) } } - assertJsonContentEquals("""{"c1":0,"c2":0}""", document1.toJson()) - assertJsonContentEquals("""{"c1":0,"c2":0}""", document2.toJson()) - - // 03. c1 and c2 sync with push-only mode. So, the changes of c1 and c2 - // are not reflected to each other. - // But, c3 can get the changes of c1 and c2, because c3 sync with pull-pull mode. - client1.pauseRemoteChanges(document1) - client2.pauseRemoteChanges(document2) - document1.updateAsync { root, _ -> + assertJsonContentEquals("""{"c1":0,"c2":0,"c3":0}""", d1.toJson()) + assertJsonContentEquals("""{"c1":0,"c2":0,"c3":0}""", d2.toJson()) + assertJsonContentEquals("""{"c1":0,"c2":0,"c3":0}""", d3.toJson()) + + // 03. [Step2] c1 sync with push-only mode, c2 sync with sync-off mode. + // c3 can get the changes of c1 and c2, because c3 sync with push-pull mode. + c1.changeSyncMode(d1, RealtimePushOnly) + c2.changeSyncMode(d2, RealtimeSyncOff) + d1.updateAsync { root, _ -> root["c1"] = 1 }.await() - document2.updateAsync { root, _ -> + d2.updateAsync { root, _ -> root["c2"] = 1 }.await() + d3.updateAsync { root, _ -> + root["c3"] = 1 + }.await() withTimeout(GENERAL_TIMEOUT) { - while (document1Events.size < 3 || - document2Events.size < 3 || - document3Ops.size < 4 - ) { + while (d1Events.size < 4 || d2Events.size < 4 || d3Events.size < 5) { delay(50) } } - assertJsonContentEquals("""{"c1":1,"c2":0}""", document1.toJson()) - assertJsonContentEquals("""{"c1":0,"c2":1}""", document2.toJson()) - assertJsonContentEquals("""{"c1":1,"c2":1}""", document3.toJson()) + assertJsonContentEquals("""{"c1":1,"c2":0,"c3":0}""", d1.toJson()) + assertJsonContentEquals("""{"c1":0,"c2":1,"c3":0}""", d2.toJson()) + assertJsonContentEquals("""{"c1":1,"c2":0,"c3":1}""", d3.toJson()) - // 04. c1 and c2 sync with push-pull mode. - client1.resumeRemoteChanges(document1) - client2.resumeRemoteChanges(document2) + // 04. [Step3] c1 sync with sync-off mode, c2 sync with push-only mode. + c1.changeSyncMode(d1, RealtimeSyncOff) + c2.changeSyncMode(d2, RealtimePushOnly) + d1.updateAsync { root, _ -> + root["c1"] = 2 + }.await() + d2.updateAsync { root, _ -> + root["c2"] = 2 + }.await() + d3.updateAsync { root, _ -> + root["c3"] = 2 + }.await() withTimeout(GENERAL_TIMEOUT) { - while (document1Events.size < 4 || document2Events.size < 4) { + while (d1Events.size < 5 || d2Events.size < 5 || d3Events.size < 8) { delay(50) } } - assertJsonContentEquals("""{"c1":1,"c2":1}""", document1.toJson()) - assertJsonContentEquals("""{"c1":1,"c2":1}""", document2.toJson()) + assertJsonContentEquals("""{"c1":2,"c2":0,"c3":0}""", d1.toJson()) + assertJsonContentEquals("""{"c1":0,"c2":2,"c3":0}""", d2.toJson()) + assertJsonContentEquals("""{"c1":1,"c2":2,"c3":2}""", d3.toJson()) - client3.detachAsync(document3).await() - client3.deactivateAsync().await() + // 05. [Step4] c1 and c2 sync with push-pull mode. + c1.changeSyncMode(d1, Realtime) + c2.changeSyncMode(d2, Realtime) + withTimeout(GENERAL_TIMEOUT) { + while (d1Events.size < 9 || d2Events.size < 9 || d3Events.size < 9) { + delay(50) + } + } + assertJsonContentEquals("""{"c1":2,"c2":2,"c3":2}""", d1.toJson()) + assertJsonContentEquals("""{"c1":2,"c2":2,"c3":2}""", d2.toJson()) + assertJsonContentEquals("""{"c1":2,"c2":2,"c3":2}""", d3.toJson()) + + c3.detachAsync(d3).await() + c3.deactivateAsync().await() + c3.close() + d3.close() collectJobs.forEach(Job::cancel) } } - @Test - fun test_sync_option_with_mixed_mode() { - runBlocking { - val client = createClient() - val documentKey = UUID.randomUUID().toString().toDocKey() - val document = Document(documentKey) - - // 01. cli attach to the document having counter. - client.activateAsync().await() - client.attachAsync(document, isRealTimeSync = false).await() - - // 02. cli update the document with creating a counter - // and sync with push-pull mode: CP(0, 0) -> CP(1, 1) - document.updateAsync { root, _ -> - root.setNewCounter("counter", 0) - }.await() - - assertEquals(CheckPoint(1, 1u), document.checkPoint) - client.syncAsync().await() - assertEquals(CheckPoint(2, 2u), document.checkPoint) - - // 03. cli update the document with increasing the counter(0 -> 1) - // and sync with push-only mode: CP(1, 1) -> CP(2, 1) - document.updateAsync { root, _ -> - root.getAs("counter").increase(1) - }.await() - - var changePack = document.createChangePack() - assertEquals(1, changePack.changes.size) - - client.syncAsync(document, Client.SyncMode.PushOnly).await() - assertEquals(CheckPoint(2, 3u), document.checkPoint) - - // 04. cli update the document with increasing the counter(1 -> 2) - // and sync with push-pull mode. CP(2, 1) -> CP(3, 3) - document.updateAsync { root, _ -> - root.getAs("counter").increase(1) - }.await() - - // The previous increase(0->1) is already pushed to the server, - // so the ChangePack of the request only has the increase(1->2). - changePack = document.createChangePack() - assertEquals(1, changePack.changes.size) - - client.syncAsync().await() - - assertEquals(CheckPoint(4, 4u), document.checkPoint) - assertEquals(2, document.getRoot().getAs("counter").value) - - client.detachAsync(document).await() - client.deactivateAsync().await() - - document.close() - client.close() - } - } - @Test fun test_prevent_remote_change_in_push_only_mode() { withTwoClientsAndDocuments { c1, c2, d1, d2, _ -> @@ -576,12 +473,12 @@ class ClientTest { // In push-only mode, remote-change events should not occur. d2Events.clear() - c2.pauseRemoteChanges(d2) + c2.changeSyncMode(d2, RealtimePushOnly) delay(100) // Keep the push-only state. assertTrue(d2Events.none { it is RemoteChange }) - c2.resumeRemoteChanges(d2) + c2.changeSyncMode(d2, Realtime) d2.updateAsync { root, _ -> root.rootTree().edit(2, 2, text { "b" }) @@ -598,4 +495,77 @@ class ClientTest { collectJobs.forEach(Job::cancel) } } + + @Test + fun test_not_include_changes_from_push_only_after_switching_to_realtime() { + runBlocking { + val c1 = createClient() + c1.activateAsync().await() + + // 01. cli attach to the document having counter. + val docKey = UUID.randomUUID().toString().toDocKey() + val d1 = Document(docKey) + c1.attachAsync(d1, syncMode = Manual).await() + + // 02. cli update the document with creating a counter + // and sync with push-pull mode: CP(1, 1) -> CP(2, 2) + d1.updateAsync { root, _ -> + root.setNewCounter("counter", 0) + }.await() + + var checkPoint = d1.checkPoint + assertEquals(1u, checkPoint.clientSeq) + assertEquals(1, checkPoint.serverSeq) + + c1.syncAsync().await() + checkPoint = d1.checkPoint + assertEquals(2u, checkPoint.clientSeq) + assertEquals(2, checkPoint.serverSeq) + + // 03. cli update the document with increasing the counter(0 -> 1) + // and sync with push-only mode: CP(2, 2) -> CP(3, 2) + val c1Events = mutableListOf() + val collectJob = launch(start = CoroutineStart.UNDISPATCHED) { + c1.events.filterIsInstance().collect(c1Events::add) + } + d1.updateAsync { root, _ -> + root.getAs("counter").increase(1) + }.await() + var changePack = d1.createChangePack() + assertEquals(1, changePack.changes.size) + c1.changeSyncMode(d1, RealtimePushOnly) + withTimeout(GENERAL_TIMEOUT) { + while (c1Events.firstOrNull() !is DocumentSynced) { + delay(50) + } + } + checkPoint = d1.checkPoint + assertEquals(3u, checkPoint.clientSeq) + assertEquals(2, checkPoint.serverSeq) + c1.changeSyncMode(d1, Manual) + + // 04. cli update the document with increasing the counter(1 -> 2) + // and sync with push-pull mode. CP(3, 2) -> CP(4, 4) + d1.updateAsync { root, _ -> + root.getAs("counter").increase(1) + }.await() + + // The previous increase(0 -> 1) is already pushed to the server, + // so the ChangePack of the request only has the increase(1 -> 2). + changePack = d1.createChangePack() + assertEquals(1, changePack.changes.size) + + c1.syncAsync().await() + checkPoint = d1.checkPoint + assertEquals(4u, checkPoint.clientSeq) + assertEquals(4, checkPoint.serverSeq) + assertEquals(2, d1.getRoot().getAs("counter").value) + + collectJob.cancel() + c1.detachAsync(d1).await() + c1.deactivateAsync().await() + c1.close() + d1.close() + } + } } diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/DocumentTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/DocumentTest.kt index adb7e2b4a..ef841c683 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/DocumentTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/DocumentTest.kt @@ -2,6 +2,7 @@ package dev.yorkie.core import androidx.test.ext.junit.runners.AndroidJUnit4 import dev.yorkie.assertJsonContentEquals +import dev.yorkie.core.Client.SyncMode.Manual import dev.yorkie.document.Document import dev.yorkie.document.Document.DocumentStatus import dev.yorkie.document.Document.Event @@ -152,11 +153,11 @@ class DocumentTest { root["key"] = 1 }.await() c1.activateAsync().await() - c1.attachAsync(d1, isRealTimeSync = false).await() + c1.attachAsync(d1, syncMode = Manual).await() assertEquals("""{"key":1}""", d1.toJson()) c2.activateAsync().await() - c2.attachAsync(d2, isRealTimeSync = false).await() + c2.attachAsync(d2, syncMode = Manual).await() assertEquals("""{"key":1}""", d2.toJson()) c1.removeAsync(d1).await() diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/GCTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/GCTest.kt index 10d63f9cf..785131de9 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/GCTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/GCTest.kt @@ -2,6 +2,7 @@ package dev.yorkie.core import androidx.test.ext.junit.runners.AndroidJUnit4 import dev.yorkie.assertJsonContentEquals +import dev.yorkie.core.Client.SyncMode.Manual import dev.yorkie.document.Document import dev.yorkie.document.crdt.CrdtTreeNode import dev.yorkie.document.json.JsonObject @@ -208,7 +209,7 @@ class GCTest { @Test fun test_gc_with_tree_for_multi_client() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> d1.updateAsync { root, _ -> root.setNewTree( "t", @@ -273,7 +274,7 @@ class GCTest { @Test fun test_gc_with_container_type_for_multi_client() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> d1.updateAsync { root, _ -> root["1"] = 1 root.setNewArray("2").apply { @@ -327,7 +328,7 @@ class GCTest { @Test fun test_gc_with_text_for_multi_client() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> d1.updateAsync { root, _ -> root.setNewText("text").edit(0, 0, "Hello World") root.setNewText("textWithAttr").edit(0, 0, "Hello World") @@ -387,7 +388,7 @@ class GCTest { fun test_gc_with_detached_document() { withTwoClientsAndDocuments( detachDocuments = false, - realTimeSync = false, + syncMode = Manual, ) { c1, c2, d1, d2, _ -> d1.updateAsync { root, _ -> root["1"] = 1 @@ -476,7 +477,7 @@ class GCTest { val document = Document(documentKey) client.activateAsync().await() - client.attachAsync(document, isRealTimeSync = false).await() + client.attachAsync(document, syncMode = Manual).await() document.updateAsync { root, _ -> root["point"] = gson.toJson(Point(0, 0)) @@ -513,7 +514,7 @@ class GCTest { c1.activateAsync().await() c2.activateAsync().await() - c1.attachAsync(d1, isRealTimeSync = false).await() + c1.attachAsync(d1, syncMode = Manual).await() d1.updateAsync { root, _ -> root.setNewObject("point").apply { set("x", 0) @@ -524,7 +525,7 @@ class GCTest { assertEquals(1, d1.garbageLength) c1.syncAsync().await() - c2.attachAsync(d2, isRealTimeSync = false).await() + c2.attachAsync(d2, syncMode = Manual).await() assertEquals(1, d2.garbageLength) d2.updateAsync { root, _ -> root.getAs("point")["x"] = 2 @@ -576,14 +577,14 @@ class GCTest { c2.activateAsync().await() // 1. initial state - c1.attachAsync(d1, isRealTimeSync = false).await() + c1.attachAsync(d1, syncMode = Manual).await() d1.updateAsync { root, _ -> val point = root.setNewObject("point") point["x"] = 0 point["y"] = 0 }.await() c1.syncAsync().await() - c2.attachAsync(d2, isRealTimeSync = false).await() + c2.attachAsync(d2, syncMode = Manual).await() // 2. client1 updates doc d1.updateAsync { root, _ -> diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt index 879a9ddc1..961939a8f 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt @@ -1,7 +1,10 @@ package dev.yorkie.core import androidx.test.ext.junit.runners.AndroidJUnit4 +import dev.yorkie.core.Client.SyncMode.Manual +import dev.yorkie.core.Client.SyncMode.Realtime import dev.yorkie.document.Document +import dev.yorkie.document.Document.Event.PresenceChange import dev.yorkie.document.Document.Event.PresenceChange.MyPresence import dev.yorkie.document.Document.Event.PresenceChange.Others import dev.yorkie.gson @@ -13,7 +16,6 @@ import kotlin.test.assertTrue import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -26,7 +28,7 @@ class PresenceTest { @Test fun test_presence_from_snapshot() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> val snapshotThreshold = 500 repeat(snapshotThreshold) { d1.updateAsync { _, presence -> @@ -53,7 +55,7 @@ class PresenceTest { fun test_presence_with_attach_and_detach() { withTwoClientsAndDocuments( detachDocuments = false, - realTimeSync = false, + syncMode = Manual, presences = mapOf("key" to "key1") to mapOf("key" to "key2"), ) { c1, c2, d1, d2, _ -> assertEquals(mapOf("key" to "key1"), d1.allPresences.value[c1.requireClientId()]) @@ -74,7 +76,7 @@ class PresenceTest { @Test fun test_initial_presence_value_without_manual_initialization() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> val emptyMap = emptyMap() assertEquals(emptyMap, d1.allPresences.value[c1.requireClientId()]) assertNull(d1.allPresences.value[c2.requireClientId()]) @@ -97,19 +99,33 @@ class PresenceTest { c1.activateAsync().await() c2.activateAsync().await() - c1.attachAsync(d1, initialPresence = mapOf("name" to "a")).await() val d1Job = launch(start = CoroutineStart.UNDISPATCHED) { - d1.events.filterIsInstance() - .filterNot { it is MyPresence.Initialized } + d1.events.filterIsInstance() .collect(d1Events::add) } + c1.attachAsync(d1, initialPresence = mapOf("name" to "a")).await() + + withTimeout(GENERAL_TIMEOUT) { + // initialized, my-presence changed + while (d1Events.size < 2) { + delay(50) + } + } + d1Events.clear() - c2.attachAsync(d2, initialPresence = mapOf("name" to "b")).await() val d2Job = launch(start = CoroutineStart.UNDISPATCHED) { - d2.events.filterIsInstance() - .filterNot { it is MyPresence.Initialized } + d2.events.filterIsInstance() .collect(d2Events::add) } + c2.attachAsync(d2, initialPresence = mapOf("name" to "b")).await() + + withTimeout(GENERAL_TIMEOUT) { + // initialized, my-presence changed + while (d2Events.size < 2) { + delay(50) + } + } + d2Events.clear() withTimeout(GENERAL_TIMEOUT) { // watched from c2 @@ -189,7 +205,7 @@ class PresenceTest { val updatedCursor = gson.toJson(Cursor(1, 1)) withTwoClientsAndDocuments( - realTimeSync = false, + syncMode = Manual, presences = mapOf("key" to "key1", "cursor" to previousCursor) to mapOf( "key" to "key2", "cursor" to previousCursor, @@ -234,19 +250,23 @@ class PresenceTest { val c1ID = c1.requireClientId() val c2ID = c2.requireClientId() - c1.attachAsync(d1, mapOf("name" to "a", "cursor" to previousCursor)).await() val d1Job = launch(start = CoroutineStart.UNDISPATCHED) { - d1.events.filterIsInstance() - .filterNot { it is MyPresence.Initialized } - .collect(d1Events::add) + d1.events.filterIsInstance().collect(d1Events::add) } + c1.attachAsync(d1, mapOf("name" to "a", "cursor" to previousCursor)).await() + + withTimeout(GENERAL_TIMEOUT) { + // initialized, my-presence changed + while (d1Events.size < 2) { + delay(50) + } + } + d1Events.clear() - c2.attachAsync(d2, mapOf("name" to "b", "cursor" to previousCursor)).await() val d2Job = launch(start = CoroutineStart.UNDISPATCHED) { - d2.events.filterIsInstance() - .filterNot { it is MyPresence.Initialized } - .collect(d2Events::add) + d2.events.filterIsInstance().collect(d2Events::add) } + c2.attachAsync(d2, mapOf("name" to "b", "cursor" to previousCursor)).await() withTimeout(GENERAL_TIMEOUT) { while (d1Events.isEmpty()) { @@ -316,9 +336,7 @@ class PresenceTest { c1.attachAsync(d1, initialPresence = mapOf("name" to "a")).await() val d1CollectJob = launch(start = CoroutineStart.UNDISPATCHED) { - d1.events.filterIsInstance() - .filterNot { it is MyPresence.Initialized } - .collect(d1Events::add) + d1.events.filterIsInstance().collect(d1Events::add) } c2.attachAsync(d2, initialPresence = mapOf("name" to "b")).await() @@ -390,7 +408,7 @@ class PresenceTest { // 01. c2 attaches doc in realtime sync, and c3 attached doc in manual sync. c2.attachAsync(d2, initialPresence = mapOf("name" to "b1", "cursor" to cursor)).await() - c3.attachAsync(d3, mapOf("name" to "c1", "cursor" to cursor), false).await() + c3.attachAsync(d3, mapOf("name" to "c1", "cursor" to cursor), syncMode = Manual).await() withTimeout(GENERAL_TIMEOUT) { // c2 watched @@ -407,7 +425,7 @@ class PresenceTest { assertNull(d1.presences.value[c3ID]) // 02. c2 pauses the document (in manual sync), c3 resumes the document (in realtime sync). - c2.pause(d2) + c2.changeSyncMode(d2, Manual) withTimeout(GENERAL_TIMEOUT) { // c2 unwatched @@ -417,7 +435,7 @@ class PresenceTest { } assertIs(d1Events.last()) - c3.resume(d3) + c3.changeSyncMode(d3, Realtime) withTimeout(GENERAL_TIMEOUT) { // c3 watched @@ -472,19 +490,16 @@ class PresenceTest { val c2ID = c2.requireClientId() val c3ID = c3.requireClientId() - c1.attachAsync(d1, initialPresence = mapOf("name" to "a1", "cursor" to cursor)) - .await() + c1.attachAsync(d1, initialPresence = mapOf("name" to "a1", "cursor" to cursor)).await() val d1CollectJob = launch(start = CoroutineStart.UNDISPATCHED) { - d1.events.filterIsInstance() - .collect(d1Events::add) + d1.events.filterIsInstance().collect(d1Events::add) } // 01. c2 attaches doc in realtime sync, and c3 attached doc in manual sync. // c1 receives the watched event from c2. - c2.attachAsync(d2, initialPresence = mapOf("name" to "b1", "cursor" to cursor)) - .await() - c3.attachAsync(d3, mapOf("name" to "c1", "cursor" to cursor), false).await() + c2.attachAsync(d2, initialPresence = mapOf("name" to "b1", "cursor" to cursor)).await() + c3.attachAsync(d3, mapOf("name" to "c1", "cursor" to cursor), syncMode = Manual).await() withTimeout(GENERAL_TIMEOUT) { // c2 watched @@ -528,7 +543,7 @@ class PresenceTest { ) // 03-1. c2 pauses the document, c1 receives an unwatched event from c2. - c2.pause(d2) + c2.changeSyncMode(d2, Manual) withTimeout(GENERAL_TIMEOUT) { // c2 unwatched @@ -551,7 +566,7 @@ class PresenceTest { c3.syncAsync().await() c1.syncAsync().await() delay(50) - c3.resume(d3) + c3.changeSyncMode(d3, Realtime) withTimeout(GENERAL_TIMEOUT) { // c3 watched @@ -595,7 +610,7 @@ class PresenceTest { ) // 05-1. c3 pauses the document, c1 receives an unwatched event from c3. - c3.pause(d3) + c3.changeSyncMode(d3, Manual) withTimeout(GENERAL_TIMEOUT) { // c3 unwatched @@ -614,7 +629,7 @@ class PresenceTest { c2.syncAsync().await() c1.syncAsync().await() delay(50) - c2.resume(d2) + c2.changeSyncMode(d2, Realtime) withTimeout(GENERAL_TIMEOUT) { // c2 watched @@ -648,10 +663,10 @@ class PresenceTest { @Test fun test_receive_document_events_according_to_presence_changes() { withTwoClientsAndDocuments( - realTimeSync = false, + syncMode = Manual, detachDocuments = false, ) { c1, c2, d1, d2, _ -> - val d1Events = mutableListOf() + val d1Events = mutableListOf() val collectJob = launch(start = CoroutineStart.UNDISPATCHED) { d1.events.filterIsInstance() .collect { event -> @@ -677,13 +692,13 @@ class PresenceTest { } // c1 in realtime sync - c1.resume(d1) + c1.changeSyncMode(d1, Realtime) d2.updateAsync { _, presence -> presence.put(mapOf("k1" to "v1")) }.await() // c2 in realtime sync - c2.resume(d2) + c2.changeSyncMode(d2, Realtime) withTimeout(GENERAL_TIMEOUT) { // watched, presence changed @@ -713,14 +728,14 @@ class PresenceTest { @Test fun test_emit_the_same_presence_multiple_times() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> - val d1Events = mutableListOf() + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> + val d1Events = mutableListOf() val collectJob = launch(start = CoroutineStart.UNDISPATCHED) { d1.events.filterIsInstance().collect(d1Events::add) } - c1.resume(d1) - c2.resume(d2) + c1.changeSyncMode(d1, Realtime) + c2.changeSyncMode(d2, Realtime) d2.updateAsync { _, presence -> presence.put(mapOf("a" to "b")) diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt index 31c471b38..cc40a5d77 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/TestUtils.kt @@ -25,7 +25,7 @@ fun String.toDocKey(): Document.Key { fun withTwoClientsAndDocuments( detachDocuments: Boolean = true, - realTimeSync: Boolean = true, + syncMode: Client.SyncMode = Client.SyncMode.Realtime, presences: Pair, Map> = Pair(emptyMap(), emptyMap()), callback: suspend CoroutineScope.(Client, Client, Document, Document, Document.Key) -> Unit, ) { @@ -41,12 +41,12 @@ fun withTwoClientsAndDocuments( client1.attachAsync( document1, - isRealTimeSync = realTimeSync, + syncMode = syncMode, initialPresence = presences.first, ).await() client2.attachAsync( document2, - isRealTimeSync = realTimeSync, + syncMode = syncMode, initialPresence = presences.second, ).await() diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTextTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTextTest.kt index 78bbf7411..1a8ac78a7 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTextTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTextTest.kt @@ -2,6 +2,7 @@ package dev.yorkie.document.json import androidx.test.ext.junit.runners.AndroidJUnit4 import dev.yorkie.assertJsonContentEquals +import dev.yorkie.core.Client.SyncMode.Manual import dev.yorkie.core.withTwoClientsAndDocuments import org.junit.Test import org.junit.runner.RunWith @@ -11,7 +12,7 @@ class JsonTextTest { @Test fun test_concurrent_insertion_and_deletion() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> d1.updateAsync { root, _ -> root.setNewText("k1").apply { edit(0, 0, "AB") diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeSplitMergeTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeSplitMergeTest.kt index cbc20773d..9d3eaa496 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeSplitMergeTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeSplitMergeTest.kt @@ -3,6 +3,7 @@ package dev.yorkie.document.json import androidx.test.ext.junit.runners.AndroidJUnit4 import dev.yorkie.TreeSplitMergeTest import dev.yorkie.TreeTest +import dev.yorkie.core.Client.SyncMode.Manual import dev.yorkie.core.withTwoClientsAndDocuments import dev.yorkie.document.json.JsonTreeTest.Companion.rootTree import kotlin.test.assertEquals @@ -16,7 +17,7 @@ class JsonTreeSplitMergeTest { @Test fun test_contained_split_and_split_at_the_same_position() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> JsonTreeTest.updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.setNewTree( @@ -48,7 +49,7 @@ class JsonTreeSplitMergeTest { @Test fun test_contained_split_and_split_at_different_positions_on_the_same_node() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> JsonTreeTest.updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.setNewTree( @@ -81,7 +82,7 @@ class JsonTreeSplitMergeTest { @Test fun test_contained_split_and_insert_into_the_split_position() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> JsonTreeTest.updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.setNewTree( @@ -114,7 +115,7 @@ class JsonTreeSplitMergeTest { @Test fun test_contained_split_and_insert_into_original_node() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> JsonTreeTest.updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.setNewTree( @@ -147,7 +148,7 @@ class JsonTreeSplitMergeTest { @Test fun test_contained_split_and_insert_into_split_node() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> JsonTreeTest.updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.setNewTree( @@ -180,7 +181,7 @@ class JsonTreeSplitMergeTest { @Test fun test_contained_split_and_delete_contents_in_split_node() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> JsonTreeTest.updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.setNewTree( @@ -214,7 +215,7 @@ class JsonTreeSplitMergeTest { @Test fun test_split_and_merge_with_empty_paragraph_left() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> d1.updateAsync { root, _ -> root.setNewTree( "t", @@ -245,7 +246,7 @@ class JsonTreeSplitMergeTest { @Test fun test_split_and_merge_with_empty_paragraph_and_multiple_split_level_left() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> d1.updateAsync { root, _ -> root.setNewTree( "t", @@ -281,7 +282,7 @@ class JsonTreeSplitMergeTest { @Test fun test_split_same_offset_multiple_times() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> d1.updateAsync { root, _ -> root.setNewTree( "t", diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt index f1a27b619..0b95696d2 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt @@ -5,6 +5,7 @@ import com.google.gson.reflect.TypeToken import dev.yorkie.TreeBasicTest import dev.yorkie.TreeTest import dev.yorkie.core.Client +import dev.yorkie.core.Client.SyncMode.Manual import dev.yorkie.core.GENERAL_TIMEOUT import dev.yorkie.core.Presence import dev.yorkie.core.createClient @@ -48,7 +49,7 @@ class JsonTreeTest { @Test fun test_tree_sync_between_replicas() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -82,7 +83,7 @@ class JsonTreeTest { @Test fun test_inserting_text_to_same_position_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -128,7 +129,7 @@ class JsonTreeTest { @Test fun test_tree_with_attributes_between_replicas() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -161,7 +162,7 @@ class JsonTreeTest { @Test fun test_deleting_overlapping_elements_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -194,7 +195,7 @@ class JsonTreeTest { @Test fun test_deleting_overlapping_text_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -227,7 +228,7 @@ class JsonTreeTest { @Test fun test_insert_and_delete_contained_elements_of_the_same_depth_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -266,7 +267,7 @@ class JsonTreeTest { @Test fun test_multiple_insert_and_delete_contained_elements_of_the_same_depth_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -308,7 +309,7 @@ class JsonTreeTest { @Test fun test_detecting_error_when_inserting_and_deleting_contained_elements_at_different_depths() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -341,7 +342,7 @@ class JsonTreeTest { @Test fun test_deleting_contained_elements_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -376,7 +377,7 @@ class JsonTreeTest { @Test fun test_insert_and_delete_contained_text_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -409,7 +410,7 @@ class JsonTreeTest { @Test fun test_deleting_contained_text_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -442,7 +443,7 @@ class JsonTreeTest { @Test fun test_insert_and_delete_contained_text_and_elements_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -475,7 +476,7 @@ class JsonTreeTest { @Test fun test_delete_contained_text_and_elements_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -508,7 +509,7 @@ class JsonTreeTest { @Test fun test_insert_side_by_side_elements_into_right_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -539,7 +540,7 @@ class JsonTreeTest { @Test fun test_deleting_side_by_side_elements_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -573,7 +574,7 @@ class JsonTreeTest { @Test fun test_insert_text_to_the_same_left_position_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -606,7 +607,7 @@ class JsonTreeTest { @Test fun test_insert_text_to_the_same_middle_position_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -639,7 +640,7 @@ class JsonTreeTest { @Test fun test_insert_text_to_the_same_right_position_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -672,7 +673,7 @@ class JsonTreeTest { @Test fun test_insert_and_delete_side_by_side_text_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -705,7 +706,7 @@ class JsonTreeTest { @Test fun test_delete_and_insert_side_by_side_text_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -738,7 +739,7 @@ class JsonTreeTest { @Test fun test_delete_side_by_side_text_blocks_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -771,7 +772,7 @@ class JsonTreeTest { @Test fun test_delete_text_content_at_the_same_left_position_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -804,7 +805,7 @@ class JsonTreeTest { @Test fun test_delete_text_content_at_the_same_middle_position_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -837,7 +838,7 @@ class JsonTreeTest { @Test fun test_delete_text_content_at_the_same_right_position_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -870,7 +871,7 @@ class JsonTreeTest { @Test fun test_delete_text_content_anchored_to_another_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -903,7 +904,7 @@ class JsonTreeTest { @Test fun test_producing_complete_deletion_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -936,7 +937,7 @@ class JsonTreeTest { @Test fun test_handling_block_delete_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -969,7 +970,7 @@ class JsonTreeTest { @Test fun test_handling_insertion_within_block_delete_concurrently_case1() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1002,7 +1003,7 @@ class JsonTreeTest { @Test fun test_handling_insertion_within_block_delete_concurrently_case2() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1035,7 +1036,7 @@ class JsonTreeTest { @Test fun test_handling_block_element_insertion_within_deletion() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1079,7 +1080,7 @@ class JsonTreeTest { @Test fun test_handling_concurrent_element_insertion_and_deletion_to_left() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1120,7 +1121,7 @@ class JsonTreeTest { @Test fun test_handling_concurrent_element_insertion_and_deletion_to_right() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1161,7 +1162,7 @@ class JsonTreeTest { @Test fun test_handling_deletion_of_insertion_anchor_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1194,7 +1195,7 @@ class JsonTreeTest { @Test fun test_handling_deletion_after_insertion_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1227,7 +1228,7 @@ class JsonTreeTest { @Test fun test_handling_deletion_before_insertion_concurrently() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1260,7 +1261,7 @@ class JsonTreeTest { @Test fun test_whether_split_link_can_be_transmitted_through_rpc() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1299,7 +1300,7 @@ class JsonTreeTest { @Test fun test_calculating_size_of_index_tree() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1328,7 +1329,7 @@ class JsonTreeTest { @Test fun test_tree_change_concurrent_delete() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1373,7 +1374,7 @@ class JsonTreeTest { @Test fun test_tree_change_concurrent_delete_and_insert() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1428,7 +1429,7 @@ class JsonTreeTest { @Test fun test_tree_change_concurrent_delete_and_insert_when_parent_removed() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1473,7 +1474,7 @@ class JsonTreeTest { @Test fun test_tree_change_concurrent_delete_with_contents_and_insert() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1527,7 +1528,7 @@ class JsonTreeTest { @Test fun test_overlapping_merge_and_merge() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1567,7 +1568,7 @@ class JsonTreeTest { @Test fun test_concurrently_deleting_and_styling_on_same_path() { withTwoClientsAndDocuments( - realTimeSync = false, + syncMode = Manual, ) { client1, client2, document1, document2, _ -> val document1Ops = mutableListOf() val document2Ops = mutableListOf() @@ -1751,7 +1752,7 @@ class JsonTreeTest { @Test fun test_returning_range_from_index_correctly_within_document_events() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1807,7 +1808,7 @@ class JsonTreeTest { @Test fun test_returning_correct_range_path() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> updateAndSync( Updater(c1, d1) { root, _ -> root.setNewTree( @@ -1914,7 +1915,7 @@ class JsonTreeTest { @Test fun test_client_reload_cases() { - withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, key -> + withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, key -> // Perform a dummy update to apply changes up to the snapshot threshold. repeat(500) { d1.updateAsync { root, _ -> @@ -2035,7 +2036,7 @@ class JsonTreeTest { val d3 = Document(key) val c3 = createClient() c3.activateAsync().await() - c3.attachAsync(d3, isRealTimeSync = false).await() + c3.attachAsync(d3, syncMode = Manual).await() assertTreesXmlEquals(d2.getRoot().rootTree().toXml(), d3) updateAndSync( diff --git a/yorkie/src/main/kotlin/dev/yorkie/core/Attachment.kt b/yorkie/src/main/kotlin/dev/yorkie/core/Attachment.kt index 5930380d0..c9101906f 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/core/Attachment.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/core/Attachment.kt @@ -5,7 +5,6 @@ import dev.yorkie.document.Document internal data class Attachment( val document: Document, val documentID: String, - val isRealTimeSync: Boolean, - val syncMode: Client.SyncMode = Client.SyncMode.PushPull, + val syncMode: Client.SyncMode = Client.SyncMode.Realtime, val remoteChangeEventReceived: Boolean = false, ) diff --git a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt index 4a2aad4be..1163432d7 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt @@ -187,7 +187,7 @@ public class Client @VisibleForTesting internal constructor( } private fun filterRealTimeSyncNeeded() = attachments.value.filterValues { attachment -> - attachment.isRealTimeSync && + attachment.syncMode.needRealTimeSync && (attachment.document.hasLocalChanges || attachment.remoteChangeEventReceived) }.map { (key, attachment) -> attachments.value += key to attachment.copy(remoteChangeEventReceived = false) @@ -198,15 +198,12 @@ public class Client @VisibleForTesting internal constructor( * Pushes local changes of the attached documents to the server and * receives changes of the remote replica from the server then apply them to local documents. */ - public fun syncAsync( - document: Document? = null, - syncMode: SyncMode = SyncMode.PushPull, - ): Deferred { + public fun syncAsync(document: Document? = null): Deferred { return scope.async { var isAllSuccess = true val attachments = document?.let { listOf( - attachments.value[it.key]?.copy(syncMode = syncMode) + attachments.value[it.key]?.copy(syncMode = SyncMode.Realtime) ?: throw IllegalArgumentException("document is not attached"), ) } ?: attachments.value.values @@ -227,7 +224,7 @@ public class Client @VisibleForTesting internal constructor( private suspend fun Collection.asSyncFlow(): Flow { return asFlow() .map { attachment -> - val (document, documentID, _, syncMode) = attachment + val (document, documentID, syncMode) = attachment SyncResult( document, runCatching { @@ -235,7 +232,7 @@ public class Client @VisibleForTesting internal constructor( clientId = requireClientId().value changePack = document.createChangePack().toPBChangePack() documentId = documentID - pushOnly = syncMode == SyncMode.PushOnly + pushOnly = syncMode == SyncMode.RealtimePushOnly } val response = service.pushPullChanges( request, @@ -245,7 +242,7 @@ public class Client @VisibleForTesting internal constructor( // NOTE(7hong13, chacha912, hackerwins): If syncLoop already executed with // PushPull, ignore the response when the syncMode is PushOnly. if (responsePack.hasChanges && - attachments.value[document.key]?.syncMode == SyncMode.PushOnly + attachments.value[document.key]?.syncMode == SyncMode.RealtimePushOnly ) { return@runCatching } @@ -265,8 +262,8 @@ public class Client @VisibleForTesting internal constructor( private fun runWatchLoop() { scope.launch(activationJob) { - attachments.map { - it.filterValues(Attachment::isRealTimeSync) + attachments.map { attachment -> + attachment.filterValues { it.syncMode != SyncMode.Manual } }.fold(emptyMap()) { accumulator, attachments -> (accumulator.keys - attachments.keys).forEach { accumulator.getValue(it).job.cancel() @@ -454,7 +451,7 @@ public class Client @VisibleForTesting internal constructor( public fun attachAsync( document: Document, initialPresence: P = emptyMap(), - isRealTimeSync: Boolean = true, + syncMode: SyncMode = SyncMode.Realtime, ): Deferred { return scope.async { check(isActive) { @@ -490,7 +487,7 @@ public class Client @VisibleForTesting internal constructor( attachments.value += document.key to Attachment( document, response.documentId, - isRealTimeSync, + syncMode, ) waitForInitialization(document.key) true @@ -596,65 +593,23 @@ public class Client @VisibleForTesting internal constructor( private suspend fun waitForInitialization(documentKey: Document.Key) { val attachment = attachments.value[documentKey] ?: return - if (attachment.isRealTimeSync) { + if (attachment.syncMode == SyncMode.Realtime) { attachment.document.presences.first { it != UninitializedPresences } } } public fun requireClientId() = (status.value as Status.Activated).clientId - /** - * Pauses the realtime synchronization of the given [document]. - */ - public fun pause(document: Document) { - changeRealTimeSync(document, false) - } - - /** - * Resumes the realtime synchronization of the given [document]. - */ - public fun resume(document: Document) { - changeRealTimeSync(document, true) - } - - private fun changeRealTimeSync(document: Document, isRealTimeSync: Boolean) { - check(isActive) { - "client is not active" - } - val attachment = attachments.value[document.key] - ?: throw IllegalArgumentException("document is not attached") - attachments.value += document.key to attachment.copy( - isRealTimeSync = isRealTimeSync, - remoteChangeEventReceived = isRealTimeSync || attachment.remoteChangeEventReceived, - ) - } - - /** - * Pauses the synchronization of remote changes, - * allowing only local changes to be applied. - */ - public fun pauseRemoteChanges(document: Document) { - changeSyncMode(document, SyncMode.PushOnly) - } - - /** - * Resumes the synchronization of remote changes, - * allowing both local and remote changes to be applied. - */ - public fun resumeRemoteChanges(document: Document) { - changeSyncMode(document, SyncMode.PushPull) - } - /** * Changes the sync mode of the [document]. */ - private fun changeSyncMode(document: Document, syncMode: SyncMode) { + public fun changeSyncMode(document: Document, syncMode: SyncMode) { check(isActive) { "client is not active" } val attachment = attachments.value[document.key] ?: throw IllegalArgumentException("document is not attached") - attachments.value += document.key to if (syncMode == SyncMode.PushPull) { + attachments.value += document.key to if (syncMode == SyncMode.Realtime) { attachment.copy(syncMode = syncMode, remoteChangeEventReceived = true) } else { attachment.copy(syncMode = syncMode) @@ -699,12 +654,13 @@ public class Client @VisibleForTesting internal constructor( } /** - * [SyncMode] is the mode of synchronization. It is used to determine - * whether to push and pull changes in PushPullChanges API. + * [SyncMode] defines synchronization modes for the PushPullChanges API. */ - public enum class SyncMode { - PushPull, - PushOnly, + public enum class SyncMode(val needRealTimeSync: Boolean) { + Realtime(true), + RealtimePushOnly(true), + RealtimeSyncOff(false), + Manual(false), } /** diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt b/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt index cdf6f2fdb..a9a356a18 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt @@ -320,9 +320,8 @@ public class Document( // them from the online clients and trigger the 'unwatched' event. presences.value[actorID]?.let { presence -> Others.Unwatched(PresenceInfo(actorID, presence)) - }.also { - onlineClients.value -= actorID - } + }.takeIf { actorID in onlineClients.value } + ?.also { onlineClients.value -= actorID } } } } diff --git a/yorkie/src/test/kotlin/dev/yorkie/core/ClientTest.kt b/yorkie/src/test/kotlin/dev/yorkie/core/ClientTest.kt index cf16a6539..dcf4c50cc 100644 --- a/yorkie/src/test/kotlin/dev/yorkie/core/ClientTest.kt +++ b/yorkie/src/test/kotlin/dev/yorkie/core/ClientTest.kt @@ -12,6 +12,7 @@ import dev.yorkie.api.v1.YorkieServiceClientInterface import dev.yorkie.assertJsonContentEquals import dev.yorkie.core.Client.Event.DocumentChanged import dev.yorkie.core.Client.Event.DocumentSynced +import dev.yorkie.core.Client.SyncMode.Manual import dev.yorkie.core.MockYorkieService.Companion.ATTACH_ERROR_DOCUMENT_KEY import dev.yorkie.core.MockYorkieService.Companion.DETACH_ERROR_DOCUMENT_KEY import dev.yorkie.core.MockYorkieService.Companion.NORMAL_DOCUMENT_KEY @@ -101,7 +102,7 @@ class ClientTest { target.activateAsync().await() val attachRequestCaptor = argumentCaptor() - target.attachAsync(document, isRealTimeSync = false).await() + target.attachAsync(document, syncMode = Manual).await() verify(service).attachDocument(attachRequestCaptor.capture(), any()) assertIsTestActorID(attachRequestCaptor.firstValue.clientId) assertIsInitialChangePack(attachRequestCaptor.firstValue.changePack)