diff --git a/gradle.properties b/gradle.properties index 74ccb5be9..757b6b737 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarni kotlin.code.style=official kotlin.mpp.stability.nowarn=true GROUP=dev.yorkie -VERSION_NAME=0.4.19-rc2 +VERSION_NAME=0.4.20 POM_DESCRIPTION=Document store for building collaborative editing applications. POM_INCEPTION_YEAR=2022 POM_URL=https://github.com/yorkie-team/yorkie-android-sdk diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt index 6a8560968..e61a1e019 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt @@ -769,4 +769,75 @@ class ClientTest { collectJobs.forEach(Job::cancel) } } + + @Test + fun test_prevent_remote_changes_in_sync_off() { + withTwoClientsAndDocuments { c1, c2, d1, d2, _ -> + val d1Events = mutableListOf() + val d2Events = mutableListOf() + val collectJobs = listOf( + launch(start = CoroutineStart.UNDISPATCHED) { + d1.events.filter { it is RemoteChange || it is LocalChange } + .collect(d1Events::add) + }, + launch(start = CoroutineStart.UNDISPATCHED) { + d2.events.filter { it is RemoteChange || it is LocalChange } + .collect(d2Events::add) + }, + ) + + d1.updateAsync { root, _ -> + root.setNewTree( + "t", + element("doc") { + element("p") { text { "12" } } + element("p") { text { "34" } } + }, + ) + }.await() + + withTimeout(GENERAL_TIMEOUT) { + while (d2Events.isEmpty()) { + delay(50) + } + } + assertIs(d2Events.first()) + assertTreesXmlEquals("

12

34

", d1, d2) + + d1.updateAsync { root, _ -> + root.rootTree().edit(2, 2, text { "a" }) + }.await() + c1.syncAsync().await() + + // Simulate the situation in the runSyncLoop where a pushpull request has been sent + // but a response has not yet been received. + d2Events.clear() + val deferred = c2.syncAsync() + c2.changeSyncMode(d2, RealtimeSyncOff) + deferred.await() + + // In push-only mode, remote-change events should not occur. + d2Events.clear() + c2.changeSyncMode(d2, RealtimeSyncOff) + + delay(100) // Keep the sync-off state. + assertTrue(d2Events.none { it is RemoteChange }) + + c2.changeSyncMode(d2, Realtime) + + d2.updateAsync { root, _ -> + root.rootTree().edit(2, 2, text { "b" }) + }.await() + + withTimeout(GENERAL_TIMEOUT) { + while (d1Events.size < 3) { + delay(50) + } + } + assertIs(d1Events.last()) + assertTreesXmlEquals("

1ba2

34

", d1) + + collectJobs.forEach(Job::cancel) + } + } } diff --git a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt index ed27b455e..726e02530 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt @@ -237,8 +237,10 @@ public class Client @VisibleForTesting internal constructor( val responsePack = response.changePack.toChangePack() // NOTE(7hong13, chacha912, hackerwins): If syncLoop already executed with // PushPull, ignore the response when the syncMode is PushOnly. + val currentSyncMode = attachments.value[document.key]?.syncMode if (responsePack.hasChanges && - attachments.value[document.key]?.syncMode == SyncMode.RealtimePushOnly + currentSyncMode == SyncMode.RealtimePushOnly || + currentSyncMode == SyncMode.RealtimeSyncOff ) { return@runCatching } diff --git a/yorkie/src/test/kotlin/dev/yorkie/document/crdt/CrdtTreeTest.kt b/yorkie/src/test/kotlin/dev/yorkie/document/crdt/CrdtTreeTest.kt index 92eb44f57..df430b686 100644 --- a/yorkie/src/test/kotlin/dev/yorkie/document/crdt/CrdtTreeTest.kt +++ b/yorkie/src/test/kotlin/dev/yorkie/document/crdt/CrdtTreeTest.kt @@ -9,6 +9,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.Ignore import org.junit.Test class CrdtTreeTest { @@ -103,14 +104,8 @@ class CrdtTreeTest { // 02. delete b from first paragraph // 0 1 2 3 4 5 6 7 //

a

c d

- target.edit(2 to 6, null) - // TODO(7hong13): should be resolved after the JS SDK implementation - // assertEquals("

ad

", target.toXml()) - - // 03. insert a new text node at the start of the first paragraph. - target.edit(1 to 1, CrdtTreeText(issuePos(), "@").toList()) - // TODO(7hong13): should be resolved after the JS SDK implementation - // assertEquals("root>

@ad

", target.toXml()) + target.edit(2 to 3, null) + assertEquals("

a

cd

", target.toXml()) } @Test @@ -191,62 +186,62 @@ class CrdtTreeTest { } @Test + @Ignore("should be resolved after the JS SDK implementation") fun `should merge and edit different levels with edit`() { - // TODO(7hong13): should be resolved after the JS SDK implementation -// fun initializeTree() { -// setUp() -// target.edit(0 to 0, CrdtTreeElement(issuePos(), "p").toList(), issueTime()) -// target.edit(1 to 1, CrdtTreeElement(issuePos(), "b").toList(), issueTime()) -// target.edit(2 to 2, CrdtTreeElement(issuePos(), "i").toList(), issueTime()) -// target.edit(3 to 3, CrdtTreeText(issuePos(), "ab").toList(), issueTime()) -// assertEquals("

ab

", target.toXml()) -// } -// -// // 01. edit between two element nodes in the same hierarchy. -// // 0 1 2 3 4 5 6 7 8 -// //

a b

-// initializeTree() -// target.edit(5 to 6, null, issueTime()) -// assertEquals("

ab

", target.toXml()) -// -// // 02. edit between two element nodes in same hierarchy. -// initializeTree() -// target.edit(6 to 7, null, issueTime()) -// assertEquals("

ab

", target.toXml()) -// -// // 03. edit between text and element node in different hierarchy. -// initializeTree() -// target.edit(4 to 6, null, issueTime()) -// assertEquals("

a

", target.toXml()) -// -// // 04. edit between text and element node in different hierarchy. -// initializeTree() -// target.edit(5 to 7, null, issueTime()) -// assertEquals("

ab

", target.toXml()) -// -// // 05. edit between text and element node in different hierarchy. -// initializeTree() -// target.edit(4 to 7, null, issueTime()) -// assertEquals("

a

", target.toXml()) -// -// // 06. edit between text and element node in different hierarchy. -// initializeTree() -// target.edit(3 to 7, null, issueTime()) -// assertEquals("

", target.toXml()) -// -// // 07. edit between text and element node in same hierarchy. -// setUp() -// target.edit(0 to 0, CrdtTreeElement(issuePos(), "p").toList(), issueTime()) -// target.edit(1 to 1, CrdtTreeText(issuePos(), "ab").toList(), issueTime()) -// target.edit(4 to 4, CrdtTreeElement(issuePos(), "p").toList(), issueTime()) -// target.edit(5 to 5, CrdtTreeElement(issuePos(), "b").toList(), issueTime()) -// target.edit(6 to 6, CrdtTreeText(issuePos(), "cd").toList(), issueTime()) -// target.edit(10 to 10, CrdtTreeElement(issuePos(), "p").toList(), issueTime()) -// target.edit(11 to 11, CrdtTreeText(issuePos(), "ef").toList(), issueTime()) -// assertEquals("

ab

cd

ef

", target.toXml()) -// -// target.edit(9 to 10, null, issueTime()) -// assertEquals("

ab

cd

ef

", target.toXml()) + fun initializeTree() { + setUp() + target.edit(0 to 0, CrdtTreeElement(issuePos(), "p").toList()) + target.edit(1 to 1, CrdtTreeElement(issuePos(), "b").toList()) + target.edit(2 to 2, CrdtTreeElement(issuePos(), "i").toList()) + target.edit(3 to 3, CrdtTreeText(issuePos(), "ab").toList()) + assertEquals("

ab

", target.toXml()) + } + + // 01. edit between two element nodes in the same hierarchy. + // 0 1 2 3 4 5 6 7 8 + //

a b

+ initializeTree() + target.edit(5 to 6, null) + assertEquals("

ab

", target.toXml()) + + // 02. edit between two element nodes in same hierarchy. + initializeTree() + target.edit(6 to 7, null) + assertEquals("

ab

", target.toXml()) + + // 03. edit between text and element node in different hierarchy. + initializeTree() + target.edit(4 to 6, null) + assertEquals("

a

", target.toXml()) + + // 04. edit between text and element node in different hierarchy. + initializeTree() + target.edit(5 to 7, null) + assertEquals("

ab

", target.toXml()) + + // 05. edit between text and element node in different hierarchy. + initializeTree() + target.edit(4 to 7, null) + assertEquals("

a

", target.toXml()) + + // 06. edit between text and element node in different hierarchy. + initializeTree() + target.edit(3 to 7, null) + assertEquals("

", target.toXml()) + + // 07. edit between text and element node in same hierarchy. + setUp() + target.edit(0 to 0, CrdtTreeElement(issuePos(), "p").toList()) + target.edit(1 to 1, CrdtTreeText(issuePos(), "ab").toList()) + target.edit(4 to 4, CrdtTreeElement(issuePos(), "p").toList()) + target.edit(5 to 5, CrdtTreeElement(issuePos(), "b").toList()) + target.edit(6 to 6, CrdtTreeText(issuePos(), "cd").toList()) + target.edit(10 to 10, CrdtTreeElement(issuePos(), "p").toList()) + target.edit(11 to 11, CrdtTreeText(issuePos(), "ef").toList()) + assertEquals("

ab

cd

ef

", target.toXml()) + + target.edit(9 to 10, null) + assertEquals("

ab

cd

ef

", target.toXml()) } private fun issuePos(offset: Int = 0) = CrdtTreeNodeID(issueTime(), offset)