diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9123b65dc..ac446a0f1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -94,9 +94,7 @@ jobs:
ram-size: 4096M
emulator-boot-timeout: 12000
disable-animations: true
- script: |
- ./gradlew yorkie:connectedCheck -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest --no-build-cache --no-daemon --stacktrace
- ./gradlew yorkie:connectedCheck -Pandroid.testInstrumentationRunnerArguments.annotation=androidx.test.filters.LargeTest --no-build-cache --no-daemon --stacktrace
+ script: ./gradlew yorkie:connectedCheck --no-build-cache --no-daemon --stacktrace
- if: ${{ matrix.api-level == 30 }}
run: ./gradlew yorkie:jacocoDebugTestReport
- if: ${{ matrix.api-level == 30 }}
diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml
index 5313b0289..4b2366f0b 100644
--- a/docker/docker-compose-ci.yml
+++ b/docker/docker-compose-ci.yml
@@ -15,7 +15,7 @@ services:
depends_on:
- yorkie
yorkie:
- image: 'yorkieteam/yorkie:0.4.6'
+ image: 'yorkieteam/yorkie:0.4.5'
container_name: 'yorkie'
command: [
'server',
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 7f4b860fd..2f5c71af8 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -22,7 +22,7 @@ services:
extra_hosts:
- "host.docker.internal:host-gateway"
yorkie:
- image: 'yorkieteam/yorkie:0.4.6'
+ image: 'yorkieteam/yorkie:0.4.5'
container_name: 'yorkie'
command: [
'server',
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 665ed9a75..e7f8cbdbf 100644
--- a/examples/texteditor/src/main/java/com/example/texteditor/EditorViewModel.kt
+++ b/examples/texteditor/src/main/java/com/example/texteditor/EditorViewModel.kt
@@ -30,9 +30,8 @@ class EditorViewModel(private val client: Client) : ViewModel(), YorkieEditText.
private val _content = MutableSharedFlow()
val content = _content.asSharedFlow()
- private val _textOperationInfos =
- MutableSharedFlow>()
- val textOpInfos = _textOperationInfos.asSharedFlow()
+ private val _textOpInfos = MutableSharedFlow>()
+ val textOpInfos = _textOpInfos.asSharedFlow()
val removedPeers = document.events.filterIsInstance()
.map { it.unwatched.actorID }
@@ -74,7 +73,7 @@ class EditorViewModel(private val client: Client) : ViewModel(), YorkieEditText.
private suspend fun emitEditOpInfos(changeInfo: Document.Event.ChangeInfo) {
changeInfo.operations.filterIsInstance()
.forEach { opInfo ->
- _textOperationInfos.emit(changeInfo.actorID to opInfo)
+ _textOpInfos.emit(changeInfo.actorID to opInfo)
}
}
@@ -86,7 +85,7 @@ class EditorViewModel(private val client: Client) : ViewModel(), YorkieEditText.
gson.fromJson(jsonArray.getString(1), TextPosStructure::class.java) ?: return
val (from, to) = document.getRoot().getAs(TEXT_KEY)
.posRangeToIndexRange(fromPos to toPos)
- _textOperationInfos.emit(actorID to OperationInfo.SelectOpInfo(from, to))
+ _textOpInfos.emit(actorID to OperationInfo.SelectOpInfo(from, to))
}
fun syncText() {
diff --git a/yorkie/proto/src/main/proto/yorkie/v1/resources.proto b/yorkie/proto/src/main/proto/yorkie/v1/resources.proto
index 281b5906d..46d879aad 100644
--- a/yorkie/proto/src/main/proto/yorkie/v1/resources.proto
+++ b/yorkie/proto/src/main/proto/yorkie/v1/resources.proto
@@ -95,10 +95,6 @@ message Operation {
TimeTicket executed_at = 6;
map attributes = 7;
}
- // NOTE(hackerwins): Select Operation is not used in the current version.
- // In the previous version, it was used to represent selection of Text.
- // However, it has been replaced by Presence now. It is retained for backward
- // compatibility purposes.
message Select {
TimeTicket parent_created_at = 1;
TextNodePos from = 2;
@@ -121,9 +117,8 @@ message Operation {
TimeTicket parent_created_at = 1;
TreePos from = 2;
TreePos to = 3;
- map created_at_map_by_actor = 4;
- repeated TreeNodes contents = 5;
- TimeTicket executed_at = 6;
+ repeated TreeNodes contents = 4;
+ TimeTicket executed_at = 5;
}
message TreeStyle {
TimeTicket parent_created_at = 1;
@@ -238,30 +233,24 @@ message TextNodeID {
}
message TreeNode {
- TreeNodeID id = 1;
+ TreePos pos = 1;
string type = 2;
string value = 3;
TimeTicket removed_at = 4;
- TreeNodeID ins_prev_id = 5;
- TreeNodeID ins_next_id = 6;
- int32 depth = 7;
- map attributes = 8;
+ TreePos ins_prev_pos = 5;
+ int32 depth = 6;
+ map attributes = 7;
}
message TreeNodes {
repeated TreeNode content = 1;
}
-message TreeNodeID {
+message TreePos {
TimeTicket created_at = 1;
int32 offset = 2;
}
-message TreePos {
- TreeNodeID parent_id = 1;
- TreeNodeID left_sibling_id = 2;
-}
-
/////////////////////////////////////////
// Messages for Common //
/////////////////////////////////////////
@@ -354,12 +343,12 @@ enum ValueType {
}
enum DocEventType {
- DOC_EVENT_TYPE_DOCUMENT_CHANGED = 0;
- DOC_EVENT_TYPE_DOCUMENT_WATCHED = 1;
- DOC_EVENT_TYPE_DOCUMENT_UNWATCHED = 2;
+ DOC_EVENT_TYPE_DOCUMENTS_CHANGED = 0;
+ DOC_EVENT_TYPE_DOCUMENTS_WATCHED = 1;
+ DOC_EVENT_TYPE_DOCUMENTS_UNWATCHED = 2;
}
message DocEvent {
DocEventType type = 1;
- string publisher = 2;
+ bytes publisher = 2;
}
diff --git a/yorkie/proto/src/main/proto/yorkie/v1/yorkie.proto b/yorkie/proto/src/main/proto/yorkie/v1/yorkie.proto
index 639ddd37d..b4f0eda6a 100644
--- a/yorkie/proto/src/main/proto/yorkie/v1/yorkie.proto
+++ b/yorkie/proto/src/main/proto/yorkie/v1/yorkie.proto
@@ -42,45 +42,49 @@ message ActivateClientRequest {
}
message ActivateClientResponse {
- string client_id = 1;
+ string client_key = 1;
+ bytes client_id = 2;
}
message DeactivateClientRequest {
- string client_id = 1;
+ bytes client_id = 1;
}
message DeactivateClientResponse {
+ bytes client_id = 1;
}
message AttachDocumentRequest {
- string client_id = 1;
+ bytes client_id = 1;
ChangePack change_pack = 2;
}
message AttachDocumentResponse {
- string document_id = 1;
- ChangePack change_pack = 2;
+ bytes client_id = 1;
+ string document_id = 2;
+ ChangePack change_pack = 3;
}
message DetachDocumentRequest {
- string client_id = 1;
+ bytes client_id = 1;
string document_id = 2;
ChangePack change_pack = 3;
bool remove_if_not_attached = 4;
}
message DetachDocumentResponse {
+ string client_key = 1;
ChangePack change_pack = 2;
}
message WatchDocumentRequest {
- string client_id = 1;
+ bytes client_id = 1;
string document_id = 2;
}
message WatchDocumentResponse {
message Initialization {
- repeated string client_ids = 1;
+ repeated bytes client_ids = 1;
}
oneof body {
@@ -90,22 +94,24 @@ message WatchDocumentResponse {
}
message RemoveDocumentRequest {
- string client_id = 1;
+ bytes client_id = 1;
string document_id = 2;
ChangePack change_pack = 3;
}
message RemoveDocumentResponse {
- ChangePack change_pack = 1;
+ string client_key = 1;
+ ChangePack change_pack = 2;
}
message PushPullChangesRequest {
- string client_id = 1;
+ bytes client_id = 1;
string document_id = 2;
ChangePack change_pack = 3;
bool push_only = 4;
}
message PushPullChangesResponse {
- ChangePack change_pack = 1;
+ bytes client_id = 1;
+ ChangePack change_pack = 2;
}
diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt
index f245d4c03..4c296bb87 100644
--- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt
+++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt
@@ -3,8 +3,8 @@ package dev.yorkie.core
import androidx.test.ext.junit.runners.AndroidJUnit4
import dev.yorkie.assertJsonContentEquals
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.Event.DocumentsChanged
import dev.yorkie.core.Client.StreamConnectionStatus
import dev.yorkie.document.Document
import dev.yorkie.document.Document.Event.LocalChange
@@ -89,8 +89,8 @@ class ClientTest {
delay(50)
}
}
- val changeEvent = assertIs(
- client2Events.first { it is DocumentChanged },
+ val changeEvent = assertIs(
+ client2Events.first { it is DocumentsChanged },
)
assertContentEquals(listOf(documentKey), changeEvent.documentKeys)
var syncEvent = assertIs(client2Events.first { it is DocumentSynced })
@@ -121,15 +121,11 @@ class ClientTest {
root.remove("k1")
}.await()
- withTimeout(GENERAL_TIMEOUT) {
- while (client1Events.none { it is DocumentSynced }) {
- delay(50)
- }
+ while (client1Events.none { it is DocumentSynced }) {
+ delay(50)
}
- withTimeout(GENERAL_TIMEOUT) {
- while (client2Events.isEmpty()) {
- delay(50)
- }
+ while (client2Events.isEmpty()) {
+ delay(50)
}
syncEvent = assertIs(client2Events.first { it is DocumentSynced })
assertIs(syncEvent.result)
@@ -195,7 +191,7 @@ class ClientTest {
root["version"] = "v2"
}.await()
client1.syncAsync().await()
- withTimeout(GENERAL_TIMEOUT) {
+ withTimeout(5_000) {
while (client2Events.size < 2) {
delay(50)
}
diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/DocumentTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/DocumentTest.kt
index 74b605130..a92813cb6 100644
--- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/DocumentTest.kt
+++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/DocumentTest.kt
@@ -147,9 +147,7 @@ class DocumentTest {
assertEquals(document1.toJson(), document2.toJson())
client1.removeAsync(document1).await()
- if (document2.status != DocumentStatus.Removed) {
- client2.detachAsync(document2).await()
- }
+ client2.detachAsync(document2).await()
assertEquals(DocumentStatus.Removed, document1.status)
assertEquals(DocumentStatus.Removed, document2.status)
}
diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt
index 4a097cf26..02576e2c8 100644
--- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt
+++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt
@@ -8,6 +8,7 @@ import dev.yorkie.gson
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNot
@@ -17,7 +18,6 @@ import kotlinx.coroutines.withTimeout
import org.junit.Test
import org.junit.runner.RunWith
import java.util.UUID
-import kotlin.test.assertIs
@RunWith(AndroidJUnit4::class)
class PresenceTest {
@@ -83,42 +83,22 @@ class PresenceTest {
@Test
fun test_presence_sync() {
- runBlocking {
- val c1 = createClient()
- val c2 = createClient()
- val documentKey = UUID.randomUUID().toString().toDocKey()
- val d1 = Document(documentKey)
- val d2 = Document(documentKey)
+ withTwoClientsAndDocuments(
+ presences = mapOf("name" to "a") to mapOf("name" to "b"),
+ ) { c1, c2, d1, d2, _ ->
val d1Events = mutableListOf()
val d2Events = mutableListOf()
-
- 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 }
- .collect(d1Events::add)
- }
-
- c2.attachAsync(d2, initialPresence = mapOf("name" to "b")).await()
- val d2Job = launch(start = CoroutineStart.UNDISPATCHED) {
- d2.events.filterIsInstance()
- .filterNot { it is MyPresence.Initialized }
- .collect(d2Events::add)
- }
-
- withTimeout(GENERAL_TIMEOUT) {
- // watched from c2
- while (d1Events.isEmpty()) {
- delay(50)
- }
- }
-
- assertEquals(
- Others.Watched(PresenceInfo(c2.requireClientId(), mapOf("name" to "b"))),
- d1Events.last(),
+ val collectJobs = listOf(
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ d1.events.filterIsInstance()
+ .filterNot { it is MyPresence.Initialized }
+ .collect(d1Events::add)
+ },
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ d2.events.filterIsInstance()
+ .filterNot { it is MyPresence.Initialized }
+ .collect(d2Events::add)
+ },
)
d1.updateAsync { _, presence ->
@@ -129,7 +109,7 @@ class PresenceTest {
}.await()
withTimeout(GENERAL_TIMEOUT) {
- while (d1Events.size < 3) {
+ while (d1Events.size < 3 || d2Events.size < 2) {
delay(50)
}
}
@@ -139,19 +119,13 @@ class PresenceTest {
MyPresence.PresenceChanged(
PresenceInfo(c1.requireClientId(), mapOf("name" to "A")),
),
+ Others.Watched(PresenceInfo(c2.requireClientId(), mapOf("name" to "b"))),
Others.PresenceChanged(
PresenceInfo(c2.requireClientId(), mapOf("name" to "B")),
),
),
- d1Events.takeLast(2),
+ d1Events,
)
-
- withTimeout(GENERAL_TIMEOUT) {
- while (d2Events.size < 2) {
- delay(50)
- }
- }
-
assertEquals(
listOf(
MyPresence.PresenceChanged(
@@ -166,14 +140,9 @@ class PresenceTest {
),
d2Events,
)
- assertEquals(d1.presences.value.toMap(), d2.presences.value.toMap())
+ assertEquals(d1.presences.value.entries, d2.presences.value.entries)
- d1Job.cancel()
- d2Job.cancel()
- c1.detachAsync(d1).await()
- c2.detachAsync(d2).await()
- c1.deactivateAsync().await()
- c2.deactivateAsync().await()
+ collectJobs.forEach(Job::cancel)
}
}
@@ -213,45 +182,25 @@ class PresenceTest {
val previousCursor = gson.toJson(Cursor(0, 0))
val updatedCursor = gson.toJson(Cursor(1, 1))
- runBlocking {
- val c1 = createClient()
- val c2 = createClient()
- val documentKey = UUID.randomUUID().toString().toDocKey()
- val d1 = Document(documentKey)
- val d2 = Document(documentKey)
+ withTwoClientsAndDocuments(
+ presences = mapOf("name" to "a", "cursor" to previousCursor) to mapOf(
+ "name" to "b",
+ "cursor" to previousCursor,
+ ),
+ ) { c1, c2, d1, d2, _ ->
val d1Events = mutableListOf()
val d2Events = mutableListOf()
-
- c1.activateAsync().await()
- c2.activateAsync().await()
-
- 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)
- }
-
- println("c2 attach")
- 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)
- }
-
- withTimeout(GENERAL_TIMEOUT + 1) {
- while (d1Events.isEmpty()) {
- delay(50)
- }
- }
-
- assertEquals(
- Others.Watched(PresenceInfo(c2ID, d2.presences.value[c2ID]!!)),
- d1Events.last(),
+ val collectJobs = listOf(
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ d1.events.filterIsInstance()
+ .filterNot { it is MyPresence.Initialized }
+ .collect(d1Events::add)
+ },
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ d2.events.filterIsInstance()
+ .filterNot { it is MyPresence.Initialized }
+ .collect(d2Events::add)
+ },
)
d1.updateAsync { _, presence ->
@@ -260,31 +209,42 @@ class PresenceTest {
presence.put(mapOf("name" to "X"))
}.await()
- withTimeout(GENERAL_TIMEOUT + 2) {
+ withTimeout(GENERAL_TIMEOUT) {
while (d1Events.size < 2 || d2Events.isEmpty()) {
delay(50)
}
}
assertEquals(
- MyPresence.PresenceChanged(
- PresenceInfo(c1ID, mapOf("name" to "X", "cursor" to updatedCursor)),
+ listOf(
+ MyPresence.PresenceChanged(
+ PresenceInfo(
+ c1.requireClientId(),
+ mapOf("name" to "X", "cursor" to updatedCursor),
+ ),
+ ),
+ Others.Watched(
+ PresenceInfo(
+ c2.requireClientId(),
+ mapOf("name" to "b", "cursor" to previousCursor),
+ ),
+ ),
),
- d1Events.last(),
+ d1Events,
)
assertEquals(
- Others.PresenceChanged(
- PresenceInfo(c1ID, mapOf("name" to "X", "cursor" to updatedCursor)),
+ listOf(
+ Others.PresenceChanged(
+ PresenceInfo(
+ c1.requireClientId(),
+ mapOf("name" to "X", "cursor" to updatedCursor),
+ ),
+ ),
),
- d2Events.last(),
+ d2Events,
)
- d1Job.cancel()
- d2Job.cancel()
- c1.detachAsync(d1).await()
- c2.detachAsync(d2).await()
- c1.deactivateAsync().await()
- c2.deactivateAsync().await()
+ collectJobs.forEach(Job::cancel)
}
}
@@ -346,275 +306,5 @@ class PresenceTest {
}
}
- @Test
- fun test_returning_online_clients_only() {
- val cursor = gson.toJson(Cursor(0, 0))
-
- runBlocking {
- val c1 = createClient()
- val c2 = createClient()
- val c3 = createClient()
-
- val documentKey = UUID.randomUUID().toString().toDocKey()
- val d1 = Document(documentKey)
- val d2 = Document(documentKey)
- val d3 = Document(documentKey)
- val d1Events = mutableListOf()
-
- c1.activateAsync().await()
- c2.activateAsync().await()
- c3.activateAsync().await()
-
- val c1ID = c1.requireClientId()
- val c2ID = c2.requireClientId()
- val c3ID = c3.requireClientId()
-
- c1.attachAsync(d1, initialPresence = mapOf("name" to "a1", "cursor" to cursor)).await()
-
- val d1CollectJob = launch(start = CoroutineStart.UNDISPATCHED) {
- d1.events.filterIsInstance()
- .collect(d1Events::add)
- }
-
- // 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()
-
- withTimeout(GENERAL_TIMEOUT) {
- // c2 watched
- while (d1Events.isEmpty()) {
- delay(50)
- }
- }
-
- assertIs(d1Events.last())
- assertEquals(
- mapOf(c1ID to d1.presences.value[c1ID], c2ID to d2.presences.value[c2ID]),
- d1.presences.value.toMap(),
- )
- assertNull(d1.presences.value[c3ID])
-
- // 02. c2 pauses the document (in manual sync), c3 resumes the document (in realtime sync).
- c2.pause(d2)
-
- withTimeout(GENERAL_TIMEOUT) {
- // c2 unwatched
- while (d1Events.size < 2) {
- delay(50)
- }
- }
-
- assertIs(d1Events.last())
- c3.resume(d3)
-
- withTimeout(GENERAL_TIMEOUT) {
- // c3 watched
- while (d1Events.size < 3) {
- delay(50)
- }
- }
-
- assertIs(d1Events.last())
- assertEquals(
- mapOf(c1ID to d1.presences.value[c1ID], c3ID to d3.presences.value[c3ID]),
- d1.presences.value.toMap(),
- )
- assertNull(d1.presences.value[c2ID])
-
- d1CollectJob.cancel()
- c1.detachAsync(d1).await()
- c3.detachAsync(d3).await()
- c1.deactivateAsync().await()
- c2.deactivateAsync().await()
- c3.deactivateAsync().await()
- }
- }
-
- @Test
- fun test_receiving_presence_events_only_for_realtime_sync() {
- val cursor = gson.toJson(Cursor(0, 0))
- runBlocking {
- val c1 = createClient()
- val c2 = createClient()
- val c3 = createClient()
-
- val documentKey = UUID.randomUUID().toString().toDocKey()
- val d1 = Document(documentKey)
- val d2 = Document(documentKey)
- val d3 = Document(documentKey)
- val d1Events = mutableListOf()
-
- c1.activateAsync().await()
- c2.activateAsync().await()
- c3.activateAsync().await()
-
- val c2ID = c2.requireClientId()
- val c3ID = c3.requireClientId()
-
- c1.attachAsync(d1, initialPresence = mapOf("name" to "a1", "cursor" to cursor))
- .await()
-
- val d1CollectJob = launch(start = CoroutineStart.UNDISPATCHED) {
- 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()
-
- withTimeout(GENERAL_TIMEOUT) {
- // c2 watched
- while (d1Events.isEmpty()) {
- delay(50)
- }
- }
-
- assertEquals(1, d1Events.size)
- assertEquals(
- Others.Watched(PresenceInfo(c2ID, mapOf("name" to "b1", "cursor" to cursor))),
- d1Events.last(),
- )
-
- // 02. c2 and c3 update the presence.
- // c1 receives the presence-changed event from c2.
- d2.updateAsync { _, presence ->
- presence.put(mapOf("name" to "b2"))
- }.await()
-
- d3.updateAsync { _, presence ->
- presence.put(mapOf("name" to "c2"))
- }.await()
-
- withTimeout(GENERAL_TIMEOUT) {
- // c2 presence changed
- while (d1Events.size < 2) {
- delay(50)
- }
- }
-
- assertEquals(2, d1Events.size)
- assertEquals(
- Others.PresenceChanged(
- PresenceInfo(
- c2ID,
- mapOf("name" to "b2", "cursor" to cursor),
- ),
- ),
- d1Events.last(),
- )
-
- // 03-1. c2 pauses the document, c1 receives an unwatched event from c2.
- c2.pause(d2)
-
- withTimeout(GENERAL_TIMEOUT) {
- // c2 unwatched
- while (d1Events.size < 3) {
- delay(50)
- }
- }
-
- assertEquals(3, d1Events.size)
- assertEquals(
- Others.Unwatched(PresenceInfo(c2ID, mapOf("name" to "b2", "cursor" to cursor))),
- d1Events.last(),
- )
-
- // 03-2. c3 resumes the document, c1 receives a watched event from c3.
- // NOTE(chacha912): The events are influenced by the timing of realtime sync
- // and watch stream resolution. For deterministic testing, the resume is performed
- // after the sync. Since the sync updates c1 with all previous presence changes
- // from c3, only the watched event is triggered.
- c3.syncAsync().await()
- c1.syncAsync().await()
- c3.resume(d3)
-
- withTimeout(GENERAL_TIMEOUT) {
- // c3 watched
- while (d1Events.size < 4) {
- delay(50)
- }
- }
-
- assertEquals(4, d1Events.size)
- assertEquals(
- Others.Watched(PresenceInfo(c3ID, mapOf("name" to "c2", "cursor" to cursor))),
- d1Events.last(),
- )
-
- // 04. c2 and c3 update the presence.
- // c1 receives the presence-changed event from c3.
- d2.updateAsync { _, presence ->
- presence.put(mapOf("name" to "b3"))
- }.await()
-
- d3.updateAsync { _, presence ->
- presence.put(mapOf("name" to "c3"))
- }.await()
-
- withTimeout(GENERAL_TIMEOUT) {
- // c3 presence changed
- while (d1Events.size < 5) {
- delay(50)
- }
- }
-
- assertEquals(5, d1Events.size)
- assertEquals(
- Others.PresenceChanged(
- PresenceInfo(
- c3ID,
- mapOf("name" to "c3", "cursor" to cursor),
- ),
- ),
- d1Events.last(),
- )
-
- // 05-1. c3 pauses the document, c1 receives an unwatched event from c3.
- c3.pause(d3)
-
- withTimeout(GENERAL_TIMEOUT) {
- // c3 unwatched
- while (d1Events.size < 6) {
- delay(50)
- }
- }
-
- assertEquals(6, d1Events.size)
- assertEquals(
- Others.Unwatched(PresenceInfo(c3ID, mapOf("name" to "c3", "cursor" to cursor))),
- d1Events.last(),
- )
-
- // 05-2. c2 resumes the document, c1 receives a watched event from c2.
- c2.syncAsync().await()
- c1.syncAsync().await()
- c2.resume(d2)
-
- withTimeout(GENERAL_TIMEOUT) {
- // c3 unwatched
- while (d1Events.size < 7) {
- delay(50)
- }
- }
-
- assertEquals(7, d1Events.size)
- assertEquals(
- Others.Watched(PresenceInfo(c2ID, mapOf("name" to "b3", "cursor" to cursor))),
- d1Events.last(),
- )
-
- d1CollectJob.cancel()
- c1.detachAsync(d1).await()
- c2.detachAsync(d2).await()
- c3.detachAsync(d3).await()
- c1.deactivateAsync().await()
- c2.deactivateAsync().await()
- c3.deactivateAsync().await()
- }
- }
-
private data class Cursor(val x: Int, val y: Int)
}
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 f6cb811aa..d97c7a272 100644
--- a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt
+++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt
@@ -1,7 +1,6 @@
package dev.yorkie.document.json
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
import dev.yorkie.core.Client
import dev.yorkie.core.Presence
import dev.yorkie.core.withTwoClientsAndDocuments
@@ -13,7 +12,6 @@ import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
-@LargeTest
@RunWith(AndroidJUnit4::class)
class JsonTreeTest {
@@ -69,6 +67,8 @@ class JsonTreeTest {
)
assertTreesXmlEquals("12
", d1, d2)
+ // TODO: add inserting on the leftmost after tree is fixed
+
updateAndSync(
Updater(c1, d1) { root, _ ->
root.rootTree().edit(2, 2, text { "A" })
@@ -118,7 +118,7 @@ class JsonTreeTest {
updateAndSync(
Updater(c1, d1) { root, _ ->
- root.rootTree().style(0, 1, mapOf("bold" to "true"))
+ root.rootTree().style(6, 7, mapOf("bold" to "true"))
},
Updater(c2, d2),
)
@@ -130,1333 +130,6 @@ class JsonTreeTest {
}
}
- @Test
- fun test_deleting_overlapping_elements_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p")
- element("i")
- element("b")
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(0, 4)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(2, 6)
- }.await()
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals("", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_deleting_overlapping_text_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "abcd" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("abcd
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 4)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(2, 5)
- }.await()
- assertEquals("d
", d1.getRoot().rootTree().toXml())
- assertEquals("a
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_insert_and_delete_contained_elements_of_the_same_depth_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "1234" }
- }
- element("p") {
- text { "abcd" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("1234
abcd
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(6, 6, element("p"))
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(0, 12)
- }.await()
- assertEquals("1234
abcd
", d1.getRoot().rootTree().toXml())
- assertEquals("", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_multiple_insert_and_delete_contained_elements_of_the_same_depth_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "1234" }
- }
- element("p") {
- text { "abcd" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("1234
abcd
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(6, 6, element("p"))
- root.rootTree().edit(8, 8, element("p"))
- root.rootTree().edit(10, 10, element("p"))
- root.rootTree().edit(12, 12, element("p"))
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(0, 12)
- }.await()
- assertEquals(
- "1234
abcd
",
- d1.getRoot().rootTree().toXml(),
- )
- assertEquals("", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_detecting_error_when_inserting_and_deleting_contained_elements_at_different_depths() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- element("i")
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(2, 2, element("i"))
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(1, 3)
- }.await()
- assertEquals("
", d1.getRoot().rootTree().toXml())
- assertEquals("", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_deleting_contained_elements_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- element("i") {
- text { "1234" }
- }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("1234
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(0, 8)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(1, 7)
- }.await()
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals("", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_insert_and_delete_contained_text_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "1234" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("1234
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 5)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(3, 3, text { "a" })
- }.await()
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals("12a34
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("a
", d1, d2)
- }
- }
-
- @Test
- fun test_deleting_contained_text_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "1234" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("1234
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 5)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(2, 4)
- }.await()
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals("14
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_insert_and_delete_contained_text_and_elements_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "1234" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("1234
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(0, 6)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(3, 3, text { "a" })
- }.await()
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals("12a34
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_delete_contained_text_and_elements_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "1234" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("1234
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(0, 6)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(1, 5)
- }.await()
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals("", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_insert_side_by_side_elements_into_left_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p")
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(0, 0, element("b"))
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(0, 0, element("i"))
- }.await()
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals("", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_insert_side_by_side_elements_into_middle_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p")
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 1, element("b"))
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(1, 1, element("i"))
- }.await()
- assertEquals("
", d1.getRoot().rootTree().toXml())
- assertEquals("
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("
", d1, d2)
- }
- }
-
- @Test
- fun test_insert_side_by_side_elements_into_right_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p")
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(2, 2, element("b"))
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(2, 2, element("i"))
- }.await()
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals("", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_insert_and_delete_side_by_side_elements_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- element("b")
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 3)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(1, 1, element("i"))
- }.await()
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals("
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("
", d1, d2)
- }
- }
-
- @Test
- fun test_delete_and_insert_side_by_side_elements_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- element("b")
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 3)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(3, 3, element("i"))
- }.await()
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals("
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("
", d1, d2)
- }
- }
-
- @Test
- fun test_deleting_side_by_side_elements_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- element("b")
- element("i")
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 3)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(3, 5)
- }.await()
- assertEquals("
", d1.getRoot().rootTree().toXml())
- assertEquals("
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_insert_text_to_the_same_left_position_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "12" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("12
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 1, text { "A" })
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(1, 1, text { "B" })
- }.await()
- assertEquals("A12
", d1.getRoot().rootTree().toXml())
- assertEquals("B12
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("BA12
", d1, d2)
- }
- }
-
- @Test
- fun test_insert_text_to_the_same_middle_position_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "12" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("12
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(2, 2, text { "A" })
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(2, 2, text { "B" })
- }.await()
- assertEquals("1A2
", d1.getRoot().rootTree().toXml())
- assertEquals("1B2
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("1BA2
", d1, d2)
- }
- }
-
- @Test
- fun test_insert_text_to_the_same_right_position_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "12" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("12
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(3, 3, text { "A" })
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(3, 3, text { "B" })
- }.await()
- assertEquals("12A
", d1.getRoot().rootTree().toXml())
- assertEquals("12B
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("12BA
", d1, d2)
- }
- }
-
- @Test
- fun test_insert_and_delete_side_by_side_text_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "1234" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("1234
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(3, 3, text { "a" })
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(3, 5)
- }.await()
- assertEquals("12a34
", d1.getRoot().rootTree().toXml())
- assertEquals("12
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("12a
", d1, d2)
- }
- }
-
- @Test
- fun test_delete_and_insert_side_by_side_text_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "1234" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("1234
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(3, 3, text { "a" })
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(1, 3)
- }.await()
- assertEquals("12a34
", d1.getRoot().rootTree().toXml())
- assertEquals("34
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("a34
", d1, d2)
- }
- }
-
- @Test
- fun test_delete_side_by_side_text_blocks_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "1234" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("1234
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(3, 5)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(1, 3)
- }.await()
- assertEquals("12
", d1.getRoot().rootTree().toXml())
- assertEquals("34
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_delete_text_content_at_the_same_left_position_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "123" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("123
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 2)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(1, 2)
- }.await()
- assertEquals("23
", d1.getRoot().rootTree().toXml())
- assertEquals("23
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("23
", d1, d2)
- }
- }
-
- @Test
- fun test_delete_text_content_at_the_same_middle_position_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "123" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("123
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(2, 3)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(2, 3)
- }.await()
- assertEquals("13
", d1.getRoot().rootTree().toXml())
- assertEquals("13
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("13
", d1, d2)
- }
- }
-
- @Test
- fun test_delete_text_content_at_the_same_right_position_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "123" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("123
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(3, 4)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(3, 4)
- }.await()
- assertEquals("12
", d1.getRoot().rootTree().toXml())
- assertEquals("12
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("12
", d1, d2)
- }
- }
-
- @Test
- fun test_delete_text_content_anchored_to_another_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "123" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("123
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 2)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(2, 3)
- }.await()
- assertEquals("23
", d1.getRoot().rootTree().toXml())
- assertEquals("13
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("3
", d1, d2)
- }
- }
-
- @Test
- fun test_producing_complete_deletion_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "123" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("123
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 2)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(2, 4)
- }.await()
- assertEquals("23
", d1.getRoot().rootTree().toXml())
- assertEquals("1
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("", d1, d2)
- }
- }
-
- @Test
- fun test_handling_block_delete_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "12345" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("12345
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 3)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(4, 6)
- }.await()
- assertEquals("345
", d1.getRoot().rootTree().toXml())
- assertEquals("123
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("3
", d1, d2)
- }
- }
-
- @Test
- fun test_handling_insertion_within_block_delete_concurrently_case1() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "12345" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("12345
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(2, 5)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(3, 3, text { "B" })
- }.await()
- assertEquals("15
", d1.getRoot().rootTree().toXml())
- assertEquals("12B345
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("1B5
", d1, d2)
- }
- }
-
- @Test
- fun test_handling_insertion_within_block_delete_concurrently_case2() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "12345" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("12345
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(2, 6)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(3, 3, text { "a" }, text { "bc" })
- }.await()
- assertEquals("1
", d1.getRoot().rootTree().toXml())
- assertEquals("12abc345
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("1abc
", d1, d2)
- }
- }
-
- @Test
- fun test_handling_block_element_insertion_within_deletion() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "1234" }
- }
- element("p") {
- text { "5678" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("1234
5678
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(0, 12)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(
- 6,
- 6,
- element("p") { text { "cd" } },
- element("i") { text { "fg" } },
- )
- }.await()
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals(
- "1234
cd
fg5678
",
- d2.getRoot().rootTree().toXml(),
- )
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("cd
fg", d1, d2)
- }
- }
-
- @Test
- fun test_handling_concurrent_element_insertion_and_deletion_to_left() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "12345" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("12345
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(0, 7)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(
- 0,
- 0,
- element("p") { text { "cd" } },
- element("i") { text { "fg" } },
- )
- }.await()
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals(
- "cd
fg12345
",
- d2.getRoot().rootTree().toXml(),
- )
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("cd
fg", d1, d2)
- }
- }
-
- @Test
- fun test_handling_concurrent_element_insertion_and_deletion_to_right() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "12345" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("12345
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(0, 7)
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(
- 7,
- 7,
- element("p") { text { "cd" } },
- element("i") { text { "fg" } },
- )
- }.await()
- assertEquals("", d1.getRoot().rootTree().toXml())
- assertEquals(
- "12345
cd
fg",
- d2.getRoot().rootTree().toXml(),
- )
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("cd
fg", d1, d2)
- }
- }
-
- @Test
- fun test_handling_deletion_of_insertion_anchor_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "12" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("12
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(2, 2, text { "A" })
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(1, 2)
- }.await()
- assertEquals("1A2
", d1.getRoot().rootTree().toXml())
- assertEquals("2
", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("A2
", d1, d2)
- }
- }
-
- @Test
- fun test_handling_deletion_after_insertion_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "12" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("12
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(1, 1, text { "A" })
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(1, 3)
- }.await()
- assertEquals("A12
", d1.getRoot().rootTree().toXml())
- assertEquals("", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("A
", d1, d2)
- }
- }
-
- @Test
- fun test_handling_deletion_before_insertion_concurrently() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("r") {
- element("p") {
- text { "12" }
- }
- },
- )
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("12
", d1, d2)
-
- d1.updateAsync { root, _ ->
- root.rootTree().edit(3, 3, text { "A" })
- }.await()
- d2.updateAsync { root, _ ->
- root.rootTree().edit(1, 3)
- }.await()
- assertEquals("12A
", d1.getRoot().rootTree().toXml())
- assertEquals("", d2.getRoot().rootTree().toXml())
-
- c1.syncAsync().await()
- c2.syncAsync().await()
- c1.syncAsync().await()
- assertTreesXmlEquals("A
", d1, d2)
- }
- }
-
- @Test
- fun test_whether_split_link_can_be_transmitted_through_rpc() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- updateAndSync(
- Updater(c1, d1) { root, _ ->
- root.setNewTree(
- "t",
- element("doc") {
- element("p") {
- text { "ab" }
- }
- },
- ).edit(2, 2, text { "1" })
- },
- Updater(c2, d2),
- )
- assertTreesXmlEquals("a1b
", d1, d2)
-
- d2.updateAsync { root, _ ->
- root.rootTree().edit(3, 3, text { "1" })
- }.await()
- assertEquals("a11b
", d2.getRoot().rootTree().toXml())
-
- d2.updateAsync { root, _ ->
- root.rootTree().apply {
- edit(2, 3, text { "12" })
- edit(4, 5, text { "21" })
- }
- }.await()
- assertEquals("a1221b
", d2.getRoot().rootTree().toXml())
-
- // if split link is not transmitted, then left sibling in from index below, is "b" not "a"
- d2.updateAsync { root, _ ->
- root.rootTree().edit(2, 4, text { "123" })
- }.await()
- assertEquals("a12321b
", d2.getRoot().rootTree().toXml())
- }
- }
-
- @Test
- fun test_calculating_size_of_index_tree() {
- withTwoClientsAndDocuments(realTimeSync = false) { c1, c2, d1, d2, _ ->
- d1.updateAsync { root, _ ->
- root.setNewTree(
- "t",
- element("doc") {
- element("p") {
- text { "ab" }
- }
- },
- ).apply {
- edit(2, 2, text { "123" })
- edit(2, 2, text { "456" })
- edit(2, 2, text { "789" })
- edit(2, 2, text { "0123" })
- }
- }.await()
-
- assertEquals("a0123789456123b
", d1.getRoot().rootTree().toXml())
- c1.syncAsync().await()
- c2.syncAsync().await()
-
- val size = d1.getRoot().rootTree().indexTree.root.size
- assertEquals(size, d2.getRoot().rootTree().indexTree.root.size)
- }
- }
-
companion object {
fun JsonObject.rootTree() = getAs("t")
diff --git a/yorkie/src/main/kotlin/dev/yorkie/api/ElementConverter.kt b/yorkie/src/main/kotlin/dev/yorkie/api/ElementConverter.kt
index 4439e0d05..1cc546047 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/api/ElementConverter.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/api/ElementConverter.kt
@@ -19,7 +19,6 @@ import dev.yorkie.api.v1.textNode
import dev.yorkie.api.v1.textNodeID
import dev.yorkie.api.v1.textNodePos
import dev.yorkie.api.v1.treeNode
-import dev.yorkie.api.v1.treeNodeID
import dev.yorkie.api.v1.treeNodes
import dev.yorkie.api.v1.treePos
import dev.yorkie.core.P
@@ -35,7 +34,6 @@ import dev.yorkie.document.crdt.CrdtTree
import dev.yorkie.document.crdt.CrdtTreeNode
import dev.yorkie.document.crdt.CrdtTreeNode.Companion.CrdtTreeElement
import dev.yorkie.document.crdt.CrdtTreeNode.Companion.CrdtTreeText
-import dev.yorkie.document.crdt.CrdtTreeNodeID
import dev.yorkie.document.crdt.CrdtTreePos
import dev.yorkie.document.crdt.ElementRht
import dev.yorkie.document.crdt.RgaTreeList
@@ -66,7 +64,6 @@ internal typealias PBTextNode = dev.yorkie.api.v1.TextNode
internal typealias PBTree = dev.yorkie.api.v1.JSONElement.Tree
internal typealias PBTreeNode = dev.yorkie.api.v1.TreeNode
internal typealias PBTreePos = dev.yorkie.api.v1.TreePos
-internal typealias PBTreeNodeID = dev.yorkie.api.v1.TreeNodeID
internal typealias PBTreeNodes = dev.yorkie.api.v1.TreeNodes
internal typealias PBSnapshot = dev.yorkie.api.v1.Snapshot
@@ -228,13 +225,12 @@ internal fun List.toCrdtTreeRootNode(): CrdtTreeNode? {
}
internal fun PBTreeNode.toCrdtTreeNode(): CrdtTreeNode {
- val id = id.toCrdtTreeNodeID()
- val convertedRemovedAt = removedAtOrNull?.toTimeTicket()
+ val pos = pos.toCrdtTreePos()
return if (type == IndexTreeNode.DEFAULT_TEXT_TYPE) {
- CrdtTreeText(id, value)
+ CrdtTreeText(pos, value)
} else {
CrdtTreeElement(
- id,
+ pos,
type,
attributes = Rht().also {
attributesMap.forEach { (key, value) ->
@@ -242,24 +238,10 @@ internal fun PBTreeNode.toCrdtTreeNode(): CrdtTreeNode {
}
},
)
- }.apply {
- convertedRemovedAt?.let(::remove)
- if (hasInsPrevId()) {
- insPrevID = insPrevId.toCrdtTreeNodeID()
- }
- if (hasInsNextId()) {
- insNextID = insNextId.toCrdtTreeNodeID()
- }
}
}
-internal fun PBTreePos.toCrdtTreePos(): CrdtTreePos {
- return CrdtTreePos(parentId.toCrdtTreeNodeID(), leftSiblingId.toCrdtTreeNodeID())
-}
-
-internal fun PBTreeNodeID.toCrdtTreeNodeID(): CrdtTreeNodeID {
- return CrdtTreeNodeID(createdAt.toTimeTicket(), offset)
-}
+internal fun PBTreePos.toCrdtTreePos(): CrdtTreePos = CrdtTreePos(createdAt.toTimeTicket(), offset)
internal fun CrdtElement.toPBJsonElement(): PBJsonElement {
return when (this) {
@@ -313,21 +295,14 @@ internal fun RgaTreeList.toPBRgaNodes(): List {
}
internal fun CrdtTreeNode.toPBTreeNodes(): List {
- val treeNode = this
return buildList {
- traverse(treeNode) { node, nodeDepth ->
+ traverse(this@toPBTreeNodes) { node, nodeDepth ->
val pbTreeNode = treeNode {
- id = node.id.toPBTreeNodeID()
+ pos = node.pos.toPBTreePos()
type = node.type
if (node.isText) {
value = node.value
}
- node.insPrevID?.let {
- insPrevId = it.toPBTreeNodeID()
- }
- node.insNextID?.let {
- insNextId = it.toPBTreeNodeID()
- }
node.removedAt?.toPBTimeTicket()?.let {
removedAt = it
}
@@ -361,16 +336,8 @@ internal fun List.toCrdtTreeNodesWhenEdit(): List? {
internal fun CrdtTreePos.toPBTreePos(): PBTreePos {
val crdtTreePos = this
return treePos {
- parentId = crdtTreePos.parentID.toPBTreeNodeID()
- leftSiblingId = crdtTreePos.leftSiblingID.toPBTreeNodeID()
- }
-}
-
-internal fun CrdtTreeNodeID.toPBTreeNodeID(): PBTreeNodeID {
- val nodeID = this
- return treeNodeID {
- createdAt = nodeID.createdAt.toPBTimeTicket()
- offset = nodeID.offset
+ createdAt = crdtTreePos.createdAt.toPBTimeTicket()
+ offset = crdtTreePos.offset
}
}
diff --git a/yorkie/src/main/kotlin/dev/yorkie/api/OperationConverter.kt b/yorkie/src/main/kotlin/dev/yorkie/api/OperationConverter.kt
index 4907d73b4..aba0f2d40 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/api/OperationConverter.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/api/OperationConverter.kt
@@ -5,6 +5,7 @@ import dev.yorkie.api.v1.OperationKt.edit
import dev.yorkie.api.v1.OperationKt.increase
import dev.yorkie.api.v1.OperationKt.move
import dev.yorkie.api.v1.OperationKt.remove
+import dev.yorkie.api.v1.OperationKt.select
import dev.yorkie.api.v1.OperationKt.set
import dev.yorkie.api.v1.OperationKt.style
import dev.yorkie.api.v1.OperationKt.treeEdit
@@ -16,6 +17,7 @@ import dev.yorkie.document.operation.IncreaseOperation
import dev.yorkie.document.operation.MoveOperation
import dev.yorkie.document.operation.Operation
import dev.yorkie.document.operation.RemoveOperation
+import dev.yorkie.document.operation.SelectOperation
import dev.yorkie.document.operation.SetOperation
import dev.yorkie.document.operation.StyleOperation
import dev.yorkie.document.operation.TreeEditOperation
@@ -25,7 +27,7 @@ import dev.yorkie.document.time.ActorID
internal typealias PBOperation = dev.yorkie.api.v1.Operation
internal fun List.toOperations(): List {
- return mapNotNull {
+ return map {
when {
it.hasSet() -> SetOperation(
key = it.set.key,
@@ -75,7 +77,12 @@ internal fun List.toOperations(): List {
?: mapOf(),
)
- it.hasSelect() -> null
+ it.hasSelect() -> SelectOperation(
+ fromPos = it.select.from.toRgaTreeSplitNodePos(),
+ toPos = it.select.to.toRgaTreeSplitNodePos(),
+ parentCreatedAt = it.select.parentCreatedAt.toTimeTicket(),
+ executedAt = it.select.executedAt.toTimeTicket(),
+ )
it.hasStyle() -> StyleOperation(
fromPos = it.style.from.toRgaTreeSplitNodePos(),
@@ -91,10 +98,6 @@ internal fun List.toOperations(): List {
toPos = it.treeEdit.to.toCrdtTreePos(),
contents = it.treeEdit.contentsList.toCrdtTreeNodesWhenEdit(),
executedAt = it.treeEdit.executedAt.toTimeTicket(),
- maxCreatedAtMapByActor =
- it.treeEdit.createdAtMapByActorMap.entries.associate { (key, value) ->
- ActorID(key) to value.toTimeTicket()
- },
)
it.hasTreeStyle() -> TreeStyleOperation(
@@ -181,6 +184,17 @@ internal fun Operation.toPBOperation(): PBOperation {
}
}
+ is SelectOperation -> {
+ operation {
+ select = select {
+ parentCreatedAt = operation.parentCreatedAt.toPBTimeTicket()
+ from = operation.fromPos.toPBTextNodePos()
+ to = operation.toPos.toPBTextNodePos()
+ executedAt = operation.executedAt.toPBTimeTicket()
+ }
+ }
+ }
+
is StyleOperation -> {
operation {
style = style {
@@ -201,11 +215,6 @@ internal fun Operation.toPBOperation(): PBOperation {
to = operation.toPos.toPBTreePos()
executedAt = operation.executedAt.toPBTimeTicket()
contents.addAll(operation.contents?.toPBTreeNodesWhenEdit().orEmpty())
- createdAtMapByActor.putAll(
- operation.maxCreatedAtMapByActor.entries.associate {
- it.key.value to it.value.toPBTimeTicket()
- },
- )
}
}
}
diff --git a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt
index e3c66f07c..c45b07ede 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt
@@ -2,6 +2,8 @@ package dev.yorkie.core
import android.content.Context
import com.google.common.annotations.VisibleForTesting
+import dev.yorkie.api.toActorID
+import dev.yorkie.api.toByteString
import dev.yorkie.api.toChangePack
import dev.yorkie.api.toPBChangePack
import dev.yorkie.api.v1.DocEventType
@@ -17,7 +19,6 @@ import dev.yorkie.api.v1.watchDocumentRequest
import dev.yorkie.core.Client.DocumentSyncResult.SyncFailed
import dev.yorkie.core.Client.DocumentSyncResult.Synced
import dev.yorkie.core.Client.Event.DocumentSynced
-import dev.yorkie.core.Presences.Companion.UninitializedPresences
import dev.yorkie.core.Presences.Companion.asPresences
import dev.yorkie.document.Document
import dev.yorkie.document.Document.DocumentStatus
@@ -43,7 +44,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.fold
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.retry
@@ -133,7 +133,12 @@ public class Client @VisibleForTesting internal constructor(
YorkieLogger.e("Client.activate", e.stackTraceToString())
return@async false
}
- _status.emit(Status.Activated(ActorID(activateResponse.clientId)))
+ _status.emit(
+ Status.Activated(
+ activateResponse.clientId.toActorID(),
+ activateResponse.clientKey,
+ ),
+ )
runSyncLoop()
runWatchLoop()
true
@@ -203,7 +208,7 @@ public class Client @VisibleForTesting internal constructor(
document,
runCatching {
val request = pushPullChangesRequest {
- clientId = requireClientId().value
+ clientId = requireClientId().toByteString()
changePack = document.createChangePack().toPBChangePack()
documentId = documentID
pushOnly = syncMode == SyncMode.PushOnly
@@ -255,7 +260,7 @@ public class Client @VisibleForTesting internal constructor(
return scope.launch(activationJob) {
service.watchDocument(
watchDocumentRequest {
- clientId = requireClientId().value
+ clientId = requireClientId().toByteString()
documentId = attachment.documentID
},
documentBasedRequestHeader(attachment.document.key),
@@ -275,12 +280,14 @@ public class Client @VisibleForTesting internal constructor(
response: WatchDocumentResponse,
) {
if (response.hasInitialization()) {
- val document = attachments.value[documentKey]?.document ?: return
- val clientIDs = response.initialization.clientIdsList.map { ActorID(it) }
- document.onlineClients.value = document.onlineClients.value + clientIDs
- document.publish(
- PresenceChange.MyPresence.Initialized(document.presences.value.asPresences()),
- )
+ response.initialization.clientIdsList.forEach { pbClientID ->
+ val clientID = pbClientID.toActorID()
+ val document = attachments.value[documentKey]?.document ?: return
+ document.onlineClients.add(clientID)
+ document.publish(
+ PresenceChange.MyPresence.Initialized(document.presences.value.asPresences()),
+ )
+ }
return
}
@@ -288,36 +295,32 @@ public class Client @VisibleForTesting internal constructor(
val eventType = checkNotNull(watchEvent.type)
// only single key will be received since 0.3.1 server.
val attachment = attachments.value[documentKey] ?: return
- val document = attachment.document
- val publisher = ActorID(watchEvent.publisher)
+ val publisher = watchEvent.publisher.toActorID()
when (eventType) {
- DocEventType.DOC_EVENT_TYPE_DOCUMENT_WATCHED -> {
- // NOTE(chacha912): We added to onlineClients, but we won't trigger watched event
- // unless we also know their initial presence data at this point.
- document.onlineClients.value = document.onlineClients.value + publisher
- if (publisher in document.allPresences.value) {
- val presence = document.presences.first { publisher in it }[publisher] ?: return
- document.publish(
+ DocEventType.DOC_EVENT_TYPE_DOCUMENTS_WATCHED -> {
+ attachment.document.onlineClients.add(publisher)
+ if (publisher in attachment.document.presences.value) {
+ val presence = attachment.document.presences.value[publisher] ?: return
+ attachment.document.publish(
PresenceChange.Others.Watched(PresenceInfo(publisher, presence)),
)
}
}
- DocEventType.DOC_EVENT_TYPE_DOCUMENT_UNWATCHED -> {
- // NOTE(chacha912): There is no presence,
- // when PresenceChange(clear) is applied before unwatching. In that case,
- // the 'unwatched' event is triggered while handling the PresenceChange.
- val presence = document.presences.value[publisher] ?: return
- document.onlineClients.value = document.onlineClients.value - publisher
- document.publish(PresenceChange.Others.Unwatched(PresenceInfo(publisher, presence)))
+ DocEventType.DOC_EVENT_TYPE_DOCUMENTS_UNWATCHED -> {
+ attachment.document.onlineClients.remove(publisher)
+ val presence = attachment.document.presences.value[publisher] ?: return
+ attachment.document.publish(
+ PresenceChange.Others.Unwatched(PresenceInfo(publisher, presence)),
+ )
}
- DocEventType.DOC_EVENT_TYPE_DOCUMENT_CHANGED -> {
+ DocEventType.DOC_EVENT_TYPE_DOCUMENTS_CHANGED -> {
attachments.value += documentKey to attachment.copy(
remoteChangeEventReceived = true,
)
- eventStream.emit(Event.DocumentChanged(listOf(documentKey)))
+ eventStream.emit(Event.DocumentsChanged(listOf(documentKey)))
}
DocEventType.UNRECOGNIZED -> {
@@ -348,7 +351,7 @@ public class Client @VisibleForTesting internal constructor(
}.await()
val request = attachDocumentRequest {
- clientId = requireClientId().value
+ clientId = requireClientId().toByteString()
changePack = document.createChangePack().toPBChangePack()
}
val response = try {
@@ -373,7 +376,6 @@ public class Client @VisibleForTesting internal constructor(
response.documentId,
isRealTimeSync,
)
- waitForInitialization(document.key)
true
}
}
@@ -399,7 +401,7 @@ public class Client @VisibleForTesting internal constructor(
}.await()
val request = detachDocumentRequest {
- clientId = requireClientId().value
+ clientId = requireClientId().toByteString()
changePack = document.createChangePack().toPBChangePack()
documentId = attachment.documentID
}
@@ -436,7 +438,7 @@ public class Client @VisibleForTesting internal constructor(
try {
service.deactivateClient(
deactivateClientRequest {
- clientId = requireClientId().value
+ clientId = requireClientId().toByteString()
},
projectBasedRequestHeader,
)
@@ -461,7 +463,7 @@ public class Client @VisibleForTesting internal constructor(
?: throw IllegalArgumentException("document is not attached")
val request = removeDocumentRequest {
- clientId = requireClientId().value
+ clientId = requireClientId().toByteString()
changePack = document.createChangePack(forceRemove = true).toPBChangePack()
documentId = attachment.documentID
}
@@ -481,13 +483,6 @@ public class Client @VisibleForTesting internal constructor(
}
}
- private suspend fun waitForInitialization(documentKey: Document.Key) {
- val attachment = attachments.value[documentKey] ?: return
- if (attachment.isRealTimeSync) {
- attachment.document.presences.first { it != UninitializedPresences }
- }
- }
-
public fun requireClientId() = (status.value as Status.Activated).clientId
/**
@@ -557,7 +552,10 @@ public class Client @VisibleForTesting internal constructor(
* Means that the client is activated. If the client is activated,
* all [Document]s of the client are ready to be used.
*/
- public class Activated internal constructor(public val clientId: ActorID) : Status
+ public class Activated internal constructor(
+ public val clientId: ActorID,
+ public val clientKey: String,
+ ) : Status
/**
* Means that the client is not activated. It is the initial stastus of the client.
@@ -639,7 +637,7 @@ public class Client @VisibleForTesting internal constructor(
/**
* Means that the documents of the client has changed.
*/
- public class DocumentChanged(public val documentKeys: List) : Event
+ public class DocumentsChanged(public val documentKeys: List) : Event
/**
* Means that the document has synced with the server.
diff --git a/yorkie/src/main/kotlin/dev/yorkie/core/Presence.kt b/yorkie/src/main/kotlin/dev/yorkie/core/Presence.kt
index 4cdec0fae..a00659b9a 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/core/Presence.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/core/Presence.kt
@@ -13,7 +13,7 @@ public data class Presence internal constructor(
public fun put(data: Map) {
presence.putAll(data)
- changeContext.presenceChange = PresenceChange.Put(presence)
+ changeContext.presenceChange = PresenceChange.Put(presence + data)
}
public fun clear() {
diff --git a/yorkie/src/main/kotlin/dev/yorkie/core/Presences.kt b/yorkie/src/main/kotlin/dev/yorkie/core/Presences.kt
index ec156a386..c4d082123 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/core/Presences.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/core/Presences.kt
@@ -30,13 +30,6 @@ public class Presences private constructor(
public fun Pair.asPresences(): Presences {
return Presences(mutableMapOf(first to second.toMutableMap()))
}
-
- internal val UninitializedPresences = Presences(
- object : HashMap>() {
-
- override fun equals(other: Any?): Boolean = this === other
- },
- )
}
}
diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt b/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt
index 6b4399439..b95393f70 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt
@@ -8,7 +8,6 @@ import dev.yorkie.core.Presence
import dev.yorkie.core.PresenceChange
import dev.yorkie.core.PresenceInfo
import dev.yorkie.core.Presences
-import dev.yorkie.core.Presences.Companion.UninitializedPresences
import dev.yorkie.core.Presences.Companion.asPresences
import dev.yorkie.document.Document.Event.PresenceChange.MyPresence
import dev.yorkie.document.Document.Event.PresenceChange.Others
@@ -40,9 +39,9 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
@@ -83,14 +82,14 @@ public class Document(public val key: Key) {
internal val garbageLength: Int
get() = root.getGarbageLength()
- internal val onlineClients = MutableStateFlow(setOf())
+ internal val onlineClients = mutableSetOf()
- private val _presences = MutableStateFlow(UninitializedPresences)
- public val presences: StateFlow =
- combine(_presences, onlineClients) { presences, onlineClients ->
- presences.filterKeys { it in onlineClients }.asPresences()
- }.stateIn(scope, SharingStarted.Eagerly, _presences.value.asPresences())
+ private val _presences = MutableStateFlow(Presences())
+ public val presences: StateFlow = _presences.map { presences ->
+ presences.filterKeys { it in onlineClients }.asPresences()
+ }.stateIn(scope, SharingStarted.Eagerly, _presences.value.asPresences())
+ @VisibleForTesting
internal val allPresences: StateFlow = _presences.asStateFlow()
/**
@@ -131,7 +130,7 @@ public class Document(public val key: Key) {
val change = context.getChange()
val (operationInfos, newPresences) = change.execute(root, _presences.value)
- newPresences?.let { emitPresences(it) }
+ newPresences?.let { _presences.emit(it) }
localChanges += change
changeID = change.id
@@ -264,31 +263,22 @@ public class Document(public val key: Key) {
this.clone = clone.copy(presences = newPresences ?: return@also)
}
val actorID = change.id.actor
- var presenceEvent: Event.PresenceChange? = null
- if (change.hasPresenceChange && actorID in onlineClients.value) {
+ var event: Event? = null
+ if (change.hasPresenceChange && actorID in onlineClients) {
val presenceChange = change.presenceChange ?: return@forEach
- presenceEvent = when (presenceChange) {
+ event = when (presenceChange) {
is PresenceChange.Put -> {
if (actorID in _presences.value) {
createPresenceChangedEvent(actorID, presenceChange.presence)
} else {
- // NOTE(chacha912): When the user exists in onlineClients, but
- // their presence was initially absent, we can consider that we have
- // received their initial presence, so trigger the 'watched' event.
Others.Watched(PresenceInfo(actorID, presenceChange.presence))
}
}
is PresenceChange.Clear -> {
- // NOTE(chacha912): When the user exists in onlineClients, but
- // PresenceChange(clear) is received, we can consider it as detachment
- // occurring before unwatching.
- // Detached user is no longer participating in the document, we remove
- // them from the online clients and trigger the 'unwatched' event.
+ onlineClients.remove(actorID)
presences.value[actorID]?.let { presence ->
Others.Unwatched(PresenceInfo(actorID, presence))
- }.also {
- onlineClients.value = onlineClients.value - actorID
}
}
}
@@ -299,8 +289,8 @@ public class Document(public val key: Key) {
eventStream.emit(Event.RemoteChange(change.toChangeInfo(opInfos)))
}
- newPresences?.let { emitPresences(it) }
- presenceEvent?.let { eventStream.emit(it) }
+ event?.let { eventStream.emit(it) }
+ newPresences?.let { _presences.emit(it) }
changeID = changeID.syncLamport(change.id.lamport)
}
}
@@ -309,11 +299,6 @@ public class Document(public val key: Key) {
clone ?: RootClone(root.deepCopy(), _presences.value.asPresences()).also { clone = it }
}
- private suspend fun emitPresences(newPresences: Presences) {
- _presences.emit(newPresences)
- clone = ensureClone().copy(presences = newPresences)
- }
-
/**
* Create [ChangePack] of [localChanges] to send to the remote server.
*/
diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/JsonSerializableStruct.kt b/yorkie/src/main/kotlin/dev/yorkie/document/JsonSerializableStruct.kt
index 41170b644..1afb5a88a 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/document/JsonSerializableStruct.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/document/JsonSerializableStruct.kt
@@ -1,6 +1,5 @@
package dev.yorkie.document
-import dev.yorkie.document.crdt.CrdtTreeNodeID
import dev.yorkie.document.crdt.CrdtTreePos
import dev.yorkie.document.crdt.RgaTreeSplitNodeID
import dev.yorkie.document.crdt.RgaTreeSplitPos
@@ -17,27 +16,17 @@ internal interface JsonSerializable> {
fun toStruct(): O
}
-public data class CrdtTreePosStruct(
- val parentID: CrdtTreeNodeIDStruct,
- val leftSiblingID: CrdtTreeNodeIDStruct,
-) : JsonSerializable.Struct {
-
- override fun toOriginal(): CrdtTreePos {
- return CrdtTreePos(parentID.toOriginal(), leftSiblingID.toOriginal())
- }
-}
-
/**
- * [CrdtTreeNodeIDStruct] represents the structure of [CrdtTreeNodeID].
+ * [CrdtTreePosStruct] represents the structure of [CrdtTreePos].
* It is used to serialize and deserialize the CRDTTreePos.
*/
-public data class CrdtTreeNodeIDStruct(
+public data class CrdtTreePosStruct(
val createdAt: TimeTicketStruct,
val offset: Int,
-) : JsonSerializable.Struct {
+) : JsonSerializable.Struct {
- override fun toOriginal(): CrdtTreeNodeID {
- return CrdtTreeNodeID(createdAt.toOriginal(), offset)
+ override fun toOriginal(): CrdtTreePos {
+ return CrdtTreePos(createdAt.toOriginal(), offset)
}
}
diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtText.kt b/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtText.kt
index 241a63f1f..94118926c 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtText.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtText.kt
@@ -12,6 +12,8 @@ internal data class CrdtText(
override var _movedAt: TimeTicket? = null,
override var _removedAt: TimeTicket? = null,
) : CrdtGCElement() {
+ private val selectionMap = mutableMapOf()
+
override val removedNodesLength: Int
get() = rgaTreeSplit.removedNodesLength
@@ -66,6 +68,17 @@ internal data class CrdtText(
return Triple(latestCreatedAtMap, changes, caretPos to caretPos)
}
+ private fun selectPrev(range: RgaTreeSplitPosRange, executedAt: TimeTicket): TextChange? {
+ val prevSelection = selectionMap[executedAt.actorID]
+ return if (prevSelection == null || prevSelection.executedAt < executedAt) {
+ selectionMap[executedAt.actorID] = Selection(range.first, range.second, executedAt)
+ val (from, to) = rgaTreeSplit.findIndexesFromRange(range)
+ TextChange(TextChangeType.Selection, executedAt.actorID, from, to)
+ } else {
+ null
+ }
+ }
+
/**
* Applies the style of the given [range].
* 1. Split nodes with from and to.
@@ -97,6 +110,13 @@ internal data class CrdtText(
}
}
+ /**
+ * Stores that the given [range] has been selected.
+ */
+ fun select(range: RgaTreeSplitPosRange, executedAt: TimeTicket): TextChange? {
+ return selectPrev(range, executedAt)
+ }
+
/**
* Returns a pair of [RgaTreeSplitPos] of the given integer offsets.
*/
diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtTree.kt b/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtTree.kt
index c94869b2c..a16c10789 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtTree.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtTree.kt
@@ -1,18 +1,16 @@
package dev.yorkie.document.crdt
-import dev.yorkie.document.CrdtTreeNodeIDStruct
import dev.yorkie.document.CrdtTreePosStruct
import dev.yorkie.document.JsonSerializable
+import dev.yorkie.document.crdt.CrdtTreeNode.Companion.CrdtTreeElement
import dev.yorkie.document.json.TreePosStructRange
-import dev.yorkie.document.time.ActorID
import dev.yorkie.document.time.TimeTicket
import dev.yorkie.document.time.TimeTicket.Companion.InitialTimeTicket
-import dev.yorkie.document.time.TimeTicket.Companion.MaxTimeTicket
import dev.yorkie.document.time.TimeTicket.Companion.compareTo
import dev.yorkie.util.IndexTree
import dev.yorkie.util.IndexTreeNode
-import dev.yorkie.util.TagContained
import dev.yorkie.util.TreePos
+import dev.yorkie.util.traverse
import java.util.TreeMap
public typealias TreePosRange = Pair
@@ -22,26 +20,49 @@ internal class CrdtTree(
override val createdAt: TimeTicket,
override var _movedAt: TimeTicket? = null,
override var _removedAt: TimeTicket? = null,
-) : CrdtGCElement() {
+) : CrdtGCElement(), Collection {
+
+ private val head = CrdtTreeElement(CrdtTreePos.InitialCrdtTreePos, INITIAL_NODE_TYPE)
internal val indexTree = IndexTree(root)
- private val nodeMapByID = TreeMap()
+ private val nodeMapByPos = TreeMap()
private val removedNodeMap = mutableMapOf, CrdtTreeNode>()
init {
+ var previous = head
indexTree.traverse { node, _ ->
- nodeMapByID[node.id] = node
+ insertAfter(previous, node)
+ previous = node
}
}
override val removedNodesLength: Int
get() = removedNodeMap.size
- val size: Int
+ override val size: Int
get() = indexTree.size
+ /**
+ * Returns the nodes between the given range.
+ */
+ fun nodesBetweenByTree(
+ from: Int,
+ to: Int,
+ action: ((CrdtTreeNode) -> Unit),
+ ) {
+ indexTree.nodesBetween(from, to, action)
+ }
+
+ /**
+ * Finds the right node of the given [index] in postorder.
+ */
+ fun findPostorderRight(index: Int): CrdtTreeNode? {
+ val pos = indexTree.findTreePos(index, true)
+ return indexTree.findPostorderRight(pos)
+ }
+
/**
* Applies the given [attributes] of the given [range].
*/
@@ -50,28 +71,22 @@ internal class CrdtTree(
attributes: Map?,
executedAt: TimeTicket,
): List {
- val (fromParent, fromLeft) = findNodesAndSplitText(range.first, executedAt)
- val (toParent, toLeft) = findNodesAndSplitText(range.second, executedAt)
- // TODO(7hong13): check whether toPath is set correctly
+ val (_, toRight) = findTreePos(range.second, executedAt)
+ val (_, fromRight) = findTreePos(range.first, executedAt)
val changes = listOf(
TreeChange(
type = TreeChangeType.Style.type,
- from = toIndex(fromParent, fromLeft),
- to = toIndex(toParent, toLeft),
- fromPath = toPath(fromParent, fromLeft),
- toPath = toPath(toParent, toLeft),
+ from = toIndex(range.first),
+ to = toIndex(range.second),
+ fromPath = indexTree.indexToPath(posToStartIndex(range.first)),
+ toPath = indexTree.indexToPath(posToStartIndex(range.first)),
actorID = executedAt.actorID,
attributes = attributes,
),
)
- traverseInPosRange(
- fromParent = fromParent,
- fromLeft = fromLeft,
- toParent = toParent,
- toLeft = toLeft,
- ) { node, _ ->
- if (!node.isRemoved && attributes != null && !node.isText) {
+ nodesBetween(fromRight, toRight) { node ->
+ if (!node.isRemoved && attributes != null) {
attributes.forEach { (key, value) ->
node.setAttribute(key, value, executedAt)
}
@@ -81,40 +96,43 @@ internal class CrdtTree(
return changes
}
- private fun toPath(parentNode: CrdtTreeNode, leftSiblingNode: CrdtTreeNode): List {
- return indexTree.treePosToPath(toTreePos(parentNode, leftSiblingNode))
- }
-
- private fun toTreePos(
- parentNode: CrdtTreeNode,
- leftSiblingNode: CrdtTreeNode,
- ): TreePos {
- return when {
- parentNode.isRemoved -> {
- var child = parentNode
- var parent = parentNode
- while (parent.isRemoved) {
- child = parent
- parent = child.parent ?: break
- }
-
- val childOffset = parent.findOffset(child)
- TreePos(parent, childOffset)
- }
-
- parentNode == leftSiblingNode -> TreePos(parentNode, 0)
+ /**
+ * Finds [TreePos] of the given [CrdtTreePos].
+ */
+ private fun findTreePos(
+ pos: CrdtTreePos,
+ executedAt: TimeTicket,
+ ): Pair, CrdtTreeNode> {
+ val treePos = toTreePos(pos) ?: throw IllegalArgumentException("cannot find node at $pos")
- else -> {
- var offset = parentNode.findOffset(leftSiblingNode)
- if (!leftSiblingNode.isRemoved) {
- if (leftSiblingNode.isText) {
- return TreePos(leftSiblingNode, leftSiblingNode.paddedSize)
- } else {
- offset++
- }
+ // Find the appropriate position. This logic is similar to
+ // handling the insertion of the same position in RGA.
+ var current = treePos
+ while (executedAt < current.node.next?.pos?.createdAt &&
+ current.node.parent == current.node.next?.parent
+ ) {
+ val nextNode = current.node.next ?: break
+ current = TreePos(nextNode, nextNode.size)
+ }
+ val right = requireNotNull(indexTree.findPostorderRight(treePos))
+ return current to right
+ }
+
+ private fun toTreePos(pos: CrdtTreePos): TreePos? {
+ val (key, value) = nodeMapByPos.floorEntry(pos) ?: return null
+ return if (key?.createdAt == pos.createdAt) {
+ val node =
+ // Choose the left node if the position is on the boundary of the split nodes.
+ if (pos.offset > 0 && pos.offset == value.pos.offset &&
+ value.insertionPrev != null
+ ) {
+ value.insertionPrev
+ } else {
+ value
}
- TreePos(parentNode, offset)
- }
+ TreePos(requireNotNull(node), pos.offset - node.pos.offset)
+ } else {
+ null
}
}
@@ -126,115 +144,88 @@ internal class CrdtTree(
range: TreePosRange,
contents: List?,
executedAt: TimeTicket,
- latestCreatedAtMapByActor: Map? = null,
- ): Pair, Map> {
+ ): List {
// 01. split text nodes at the given range if needed.
- val (fromParent, fromLeft) = findNodesAndSplitText(range.first, executedAt)
- val (toParent, toLeft) = findNodesAndSplitText(range.second, executedAt)
+ val (toPos, toRight) = findTreePosWithSplitText(range.second, executedAt)
+ val (fromPos, fromRight) = findTreePosWithSplitText(range.first, executedAt)
// NOTE(hackerwins): If concurrent deletion happens, we need to separate the
// range(from, to) into multiple ranges.
val changes = listOf(
TreeChange(
type = TreeChangeType.Content.type,
- from = toIndex(fromParent, fromLeft),
- to = toIndex(toParent, toLeft),
- fromPath = toPath(fromParent, fromLeft),
- toPath = toPath(toParent, toLeft),
+ from = toIndex(range.first),
+ to = toIndex(range.second),
+ fromPath = indexTree.treePosToPath(fromPos),
+ toPath = indexTree.treePosToPath(toPos),
actorID = executedAt.actorID,
value = contents?.map(CrdtTreeNode::toJson),
),
)
val toBeRemoved = mutableListOf()
- val latestCreatedAtMap = mutableMapOf()
-
- traverseInPosRange(
- fromParent = fromParent,
- fromLeft = fromLeft,
- toParent = toParent,
- toLeft = toLeft,
- ) { node, contained ->
- // If node is a element node and half-contained in the range,
- // it should not be removed.
- if (!node.isText && contained != TagContained.All) {
- return@traverseInPosRange
+ // 02. remove the nodes and update linked list and index tree.
+ if (fromRight != toRight) {
+ nodesBetween(fromRight, toRight) { node ->
+ if (!node.isRemoved) {
+ toBeRemoved.add(node)
+ }
}
- val actorID = node.createdAt.actorID
- val latestCreatedAt = latestCreatedAtMapByActor?.let {
- latestCreatedAtMapByActor[actorID] ?: InitialTimeTicket
- } ?: MaxTimeTicket
-
- if (node.canDelete(executedAt, latestCreatedAt)) {
- val latest = latestCreatedAtMap[actorID]
- val createdAt = node.createdAt
-
- if (latest == null || latest < createdAt) {
- latestCreatedAtMap[actorID] = createdAt
+ val isRangeOnSameBranch = toPos.node.isAncestorOf(fromPos.node)
+ toBeRemoved.forEach { node ->
+ node.remove(executedAt)
+ if (node.isRemoved) {
+ removedNodeMap[node.createdAt to node.pos.offset] = node
}
-
- toBeRemoved.add(node)
}
- }
- toBeRemoved.forEach { node ->
- node.remove(executedAt)
- if (node.isRemoved) {
- removedNodeMap[node.createdAt to node.id.offset] = node
+ // move the alive children of the removed element node
+ if (isRangeOnSameBranch) {
+ val removedElementNode = when {
+ fromPos.node.parent?.isRemoved == true -> fromPos.node.parent
+ !fromPos.node.isText && fromPos.node.isRemoved -> fromPos.node
+ else -> null
+ }
+ removedElementNode?.let { removedNode ->
+ val elementNode = toPos.node
+ val offset = elementNode.findBranchOffset(removedNode)
+ removedNode.children.reversed().forEach { node ->
+ elementNode.insertAt(offset, node)
+ }
+ }
+ } else if (fromPos.node.parent?.isRemoved == true) {
+ val parent = requireNotNull(fromPos.node.parent)
+ toPos.node.parent?.prepend(*parent.children.toTypedArray())
}
}
// 03. insert the given node at the given position.
if (contents?.isNotEmpty() == true) {
- var leftInChildren = fromLeft
-
+ // 03-1. insert the content nodes to the list.
+ var previous = requireNotNull(fromRight.prev)
+ var offset = fromPos.offset
contents.forEach { content ->
- // 03-1. insert the content nodes to the list.
- if (leftInChildren == fromParent) {
- // 03-1-1. when there's no leftSibling, then insert content into very front of parent's children List
- fromParent.insertAt(0, content)
- } else {
- // 03-1-2. insert after leftSibling
- fromParent.insertAfter(leftInChildren, content)
+ traverse(content) { node, _ ->
+ insertAfter(previous, node)
+ previous = node
}
- leftInChildren = content
- traverseAll(content) { node, _ ->
- // if insertion happens during concurrent editing and parent node has been removed,
- // make new nodes as tombstone immediately
- if (fromParent.isRemoved) {
- node.remove(executedAt)
- removedNodeMap[node.id.createdAt to node.id.offset] = node
+ // 03-2. insert the content nodes to the tree.
+ val node = fromPos.node
+ if (node.isText) {
+ if (fromPos.offset == 0) {
+ node.parent?.insertBefore(node, content)
+ } else {
+ node.parent?.insertAfter(node, content)
}
- nodeMapByID[node.id] = node
+ } else {
+ node.insertAt(offset, content)
+ offset++
}
}
}
- return changes to latestCreatedAtMap
- }
-
- private fun traverseAll(
- node: CrdtTreeNode,
- depth: Int = 0,
- action: ((CrdtTreeNode, Int) -> Unit),
- ) {
- node.allChildren.forEach { child ->
- traverseAll(child, depth + 1, action)
- }
- action.invoke(node, depth)
- }
-
- private fun traverseInPosRange(
- fromParent: CrdtTreeNode,
- fromLeft: CrdtTreeNode,
- toParent: CrdtTreeNode,
- toLeft: CrdtTreeNode,
- callback: (CrdtTreeNode, TagContained) -> Unit,
- ) {
- val fromIndex = toIndex(fromParent, fromLeft)
- val toIndex = toIndex(toParent, toLeft)
- indexTree.nodesBetween(fromIndex, toIndex, callback)
+ return changes
}
/**
@@ -243,70 +234,81 @@ internal class CrdtTree(
* [CrdtTreePos] is a position in the CRDT perspective. This is
* different from [TreePos] which is a position of the tree in the local perspective.
*/
- fun findNodesAndSplitText(
+ private fun findTreePosWithSplitText(
pos: CrdtTreePos,
executedAt: TimeTicket,
- ): Pair {
- val treeNodes =
- toTreeNodes(pos) ?: throw IllegalArgumentException("cannot find node at $pos")
- val (parentNode, leftSiblingNode) = treeNodes
+ ): Pair, CrdtTreeNode> {
+ val treePos = toTreePos(pos) ?: throw IllegalArgumentException("cannot find node at $pos")
// Find the appropriate position. This logic is similar to
// handling the insertion of the same position in RGA.
- if (leftSiblingNode.isText) {
- val absOffset = leftSiblingNode.id.offset
- val split = leftSiblingNode.split(pos.leftSiblingID.offset - absOffset, absOffset)
- split?.let {
- split.insPrevID = leftSiblingNode.id
- nodeMapByID[split.id] = split
-
- leftSiblingNode.insNextID?.let { insNextID ->
- val insNext = findFloorNode(insNextID)
- insNext?.insPrevID = split.id
- split.insNextID = insNextID
- }
+ var current = treePos
+ while (executedAt < current.node.next?.pos?.createdAt &&
+ current.node.parent == current.node.next?.parent
+ ) {
+ val nextNode = current.node.next ?: break
+ current = TreePos(nextNode, nextNode.size)
+ }
- leftSiblingNode.insNextID = split.id
+ if (current.node.isText) {
+ current.node.split(current.offset)?.let { split ->
+ insertAfter(current.node, split)
+ split.insertionPrev = current.node
}
}
- val index = if (parentNode == leftSiblingNode) {
- 0
- } else {
- parentNode.allChildren.indexOf(leftSiblingNode) + 1
- }
- var updatedLeftSiblingNode = leftSiblingNode
- for (i in index until parentNode.allChildren.size) {
- val next = parentNode.allChildren[i]
- if (executedAt < next.id.createdAt) {
- updatedLeftSiblingNode = next
- } else {
- break
- }
+ val right = requireNotNull(indexTree.findPostorderRight(treePos))
+ return current to right
+ }
+
+ /**
+ * Inserts the [newNode] after the [prevNode]
+ */
+ private fun insertAfter(prevNode: CrdtTreeNode, newNode: CrdtTreeNode) {
+ val next = prevNode.next
+ prevNode.next = newNode
+ newNode.prev = prevNode
+ next?.let {
+ newNode.next = next
+ next.prev = newNode
}
- return parentNode to updatedLeftSiblingNode
+ nodeMapByPos[newNode.pos] = newNode
}
- private fun findFloorNode(id: CrdtTreeNodeID): CrdtTreeNode? {
- val (key, value) = nodeMapByID.floorEntry(id) ?: return null
- return if (key == null || key.createdAt != id.createdAt) null else value
+ /**
+ * Returns the nodes between the given range.
+ * [left] is inclusive, while [right] is exclusive.
+ */
+ private fun nodesBetween(
+ left: CrdtTreeNode,
+ right: CrdtTreeNode,
+ action: (CrdtTreeNode) -> Unit,
+ ) {
+ var current: CrdtTreeNode? = left
+ while (current != right) {
+ current?.let(action)
+ ?: throw IllegalArgumentException("left and right are not in the same list")
+ current = current.next
+ }
}
- private fun toTreeNodes(pos: CrdtTreePos): Pair? {
- val parentID = pos.parentID
- val leftSiblingID = pos.leftSiblingID
- val parentNode = findFloorNode(parentID) ?: return null
- val leftSiblingNode = findFloorNode(leftSiblingID) ?: return null
+ /**
+ * Returns start index of pos
+ * 0 1 2 3 4 5 6 7 8
+ * t e x t
+ * If tree is just like above, and the pos is pointing index of 7
+ * this returns 0 (start index of tag)
+ */
+ private fun posToStartIndex(pos: CrdtTreePos): Int {
+ val treePos = toTreePos(pos)
+ val index = toIndex(pos)
+ val size = if (treePos?.node?.isText == true) {
+ treePos.node.parent?.size
+ } else {
+ treePos?.node?.size
+ } ?: -1
- val updatedLeftSiblingNode =
- if (leftSiblingID.offset > 0 && leftSiblingID.offset == leftSiblingNode.id.offset &&
- leftSiblingNode.insPrevID != null
- ) {
- findFloorNode(requireNotNull(leftSiblingNode.insPrevID)) ?: leftSiblingNode
- } else {
- leftSiblingNode
- }
- return parentNode to updatedLeftSiblingNode
+ return index - size - 1
}
/**
@@ -351,9 +353,9 @@ internal class CrdtTree(
nodesToBeRemoved.forEach { node ->
node.parent?.removeChild(node)
- nodeMapByID.remove(node.id)
+ nodeMapByPos.remove(node.pos)
delete(node)
- removedNodeMap.remove(node.createdAt to node.id.offset)
+ removedNodeMap.remove(node.createdAt to node.pos.offset)
}
return nodesToBeRemoved.size
@@ -363,20 +365,14 @@ internal class CrdtTree(
* Physically deletes the given [node] from [IndexTree].
*/
private fun delete(node: CrdtTreeNode) {
- val insPrevID = node.insPrevID
- val insNextID = node.insNextID
-
- insPrevID?.let {
- val insPrev = findFloorNode(it)
- insPrev?.insNextID = insNextID
- }
- insNextID?.let {
- val insNext = findFloorNode(it)
- insNext?.insPrevID = insPrevID
- }
+ val prev = node.prev
+ val next = node.next
+ prev?.next = next
+ next?.prev = prev
- node.insPrevID = null
- node.insNextID = null
+ node.prev = null
+ node.next = null
+ node.insertionPrev = null
}
/**
@@ -384,21 +380,7 @@ internal class CrdtTree(
*/
fun findPos(index: Int, preferText: Boolean = true): CrdtTreePos {
val (node, offset) = indexTree.findTreePos(index, preferText)
- var updatedNode = node
- val leftSibling = if (node.isText) {
- updatedNode = requireNotNull(node.parent)
- if (node.parent?.children?.firstOrNull() == node && offset == 0) {
- node.parent
- } else {
- node
- }
- } else {
- if (offset == 0) node else node.children[offset - 1]
- } ?: throw IllegalArgumentException("left sibling should not be null")
- return CrdtTreePos(
- updatedNode.id,
- CrdtTreeNodeID(leftSibling.createdAt, leftSibling.offset + offset),
- )
+ return CrdtTreePos(node.pos.createdAt, node.pos.offset + offset)
}
/**
@@ -411,15 +393,23 @@ internal class CrdtTree(
/**
* Converts the given [pos] to the index of the tree.
*/
- fun toIndex(parentNode: CrdtTreeNode, leftSiblingNode: CrdtTreeNode): Int {
- return indexTree.indexOf(toTreePos(parentNode, leftSiblingNode))
+ fun toIndex(pos: CrdtTreePos): Int {
+ return toTreePos(pos)?.let(indexTree::indexOf) ?: -1
}
/**
* Converts the given path of the node to the range of the position.
*/
fun pathToPosRange(path: List): TreePosRange {
- val fromIndex = pathToIndex(path)
+ val index = pathToIndex(path)
+ val (parentNode, offset) = pathToTreePos(path)
+
+ if (parentNode.hasTextChild) {
+ throw IllegalArgumentException("invalid path: $path")
+ }
+
+ val node = parentNode.children[offset]
+ val fromIndex = index + node.size + 1
return findPos(fromIndex) to findPos(fromIndex + 1)
}
@@ -434,7 +424,8 @@ internal class CrdtTree(
* Finds the position of the given index in the tree by [path].
*/
fun pathToPos(path: List): CrdtTreePos {
- return findPos(indexTree.pathToIndex(path))
+ val (node, offset) = indexTree.pathToTreePos(path)
+ return CrdtTreePos(node.pos.createdAt, node.pos.offset + offset)
}
/**
@@ -475,37 +466,60 @@ internal class CrdtTree(
* Converts the [range] into [TreePosStructRange].
*/
fun indexRangeToPosStructRange(range: Pair): TreePosStructRange {
- val (fromIndex, toIndex) = range
- val fromPos = findPos(fromIndex)
- return if (fromIndex == toIndex) {
- fromPos.toStruct() to fromPos.toStruct()
- } else {
- fromPos.toStruct() to findPos(toIndex).toStruct()
- }
+ val (fromPos, toPos) = indexRangeToPosRange(range)
+ return fromPos.toStruct() to toPos.toStruct()
}
/**
- * Converts the given position [range] to the path range.
+ * Returns a pair of integer offsets of the tree.
*/
- fun posRangeToPathRange(
- range: TreePosRange,
- executedAt: TimeTicket,
- ): Pair, List> {
- val (fromParent, fromLeft) = findNodesAndSplitText(range.first, executedAt)
- val (toParent, toLeft) = findNodesAndSplitText(range.second, executedAt)
- return toPath(fromParent, fromLeft) to toPath(toParent, toLeft)
+ fun rangeToIndex(range: TreePosRange): Pair {
+ return toIndex(range.first) to toIndex(range.second)
}
/**
- * Converts the given position range to the path range.
+ * Converts the given position [range] to the path range.
*/
- fun posRangeToIndexRange(
- range: TreePosRange,
- executedAt: TimeTicket,
- ): Pair {
- val (fromParent, fromLeft) = findNodesAndSplitText(range.first, executedAt)
- val (toParent, toLeft) = findNodesAndSplitText(range.second, executedAt)
- return toIndex(fromParent, fromLeft) to toIndex(toParent, toLeft)
+ fun posRangeToPathRange(range: TreePosRange): Pair, List> {
+ val fromPath = indexTree.indexToPath(toIndex(range.first))
+ val toPath = indexTree.indexToPath(toIndex(range.second))
+ return fromPath to toPath
+ }
+
+ override fun isEmpty() = indexTree.size == 0
+
+ override fun iterator(): Iterator {
+ return object : Iterator {
+ var node = head.next
+
+ override fun hasNext(): Boolean {
+ while (node != null) {
+ if (node?.isRemoved == false) {
+ return true
+ }
+ node = node?.next
+ }
+ return false
+ }
+
+ override fun next(): CrdtTreeNode {
+ return requireNotNull(node).also {
+ node = node?.next
+ }
+ }
+ }
+ }
+
+ override fun containsAll(elements: Collection): Boolean {
+ return indexTree.root.children.containsAll(elements)
+ }
+
+ override fun contains(element: CrdtTreeNode): Boolean {
+ return indexTree.root.children.contains(element)
+ }
+
+ companion object {
+ private const val INITIAL_NODE_TYPE = "dummy"
}
}
@@ -515,7 +529,7 @@ internal class CrdtTree(
*/
@Suppress("DataClassPrivateConstructor")
internal data class CrdtTreeNode private constructor(
- val id: CrdtTreeNodeID,
+ val pos: CrdtTreePos,
override val type: String,
private val _value: String? = null,
private val childNodes: MutableList = mutableListOf(),
@@ -529,7 +543,7 @@ internal data class CrdtTreeNode private constructor(
get() = _attributes.toXml()
val createdAt: TimeTicket
- get() = id.createdAt
+ get() = pos.createdAt
var removedAt: TimeTicket? = null
private set
@@ -537,9 +551,6 @@ internal data class CrdtTreeNode private constructor(
override val isRemoved: Boolean
get() = removedAt != null
- val offset: Int
- get() = id.offset
-
override var value: String = _value.orEmpty()
get() {
check(isText) {
@@ -555,9 +566,11 @@ internal data class CrdtTreeNode private constructor(
size = value.length
}
- var insPrevID: CrdtTreeNodeID? = null
+ var next: CrdtTreeNode? = null
+
+ var prev: CrdtTreeNode? = null
- var insNextID: CrdtTreeNodeID? = null
+ var insertionPrev: CrdtTreeNode? = null
val rhtNodes: List
get() = _attributes.toList()
@@ -570,13 +583,7 @@ internal data class CrdtTreeNode private constructor(
* Clones this node with the given [offset].
*/
override fun clone(offset: Int): CrdtTreeNode {
- return copy(
- id = CrdtTreeNodeID(id.createdAt, offset),
- _value = null,
- childNodes = mutableListOf(),
- ).also {
- it.removedAt = this.removedAt
- }
+ return copy(pos = CrdtTreePos(pos.createdAt, offset))
}
fun setAttribute(key: String, value: String, executedAt: TimeTicket) {
@@ -616,53 +623,28 @@ internal data class CrdtTreeNode private constructor(
}
}
- /**
- * Checks if node is able to delete.
- */
- fun canDelete(executedAt: TimeTicket, latestCreatedAt: TimeTicket): Boolean {
- return createdAt <= latestCreatedAt && (removedAt == null || removedAt < executedAt)
- }
-
@Suppress("FunctionName")
companion object {
- fun CrdtTreeText(id: CrdtTreeNodeID, value: String): CrdtTreeNode {
- return CrdtTreeNode(id, DEFAULT_TEXT_TYPE, value)
+ fun CrdtTreeText(pos: CrdtTreePos, value: String): CrdtTreeNode {
+ return CrdtTreeNode(pos, DEFAULT_TEXT_TYPE, value)
}
fun CrdtTreeElement(
- id: CrdtTreeNodeID,
+ pos: CrdtTreePos,
type: String,
children: List = emptyList(),
attributes: Rht = Rht(),
- ) = CrdtTreeNode(id, type, null, children.toMutableList(), attributes)
+ ) = CrdtTreeNode(pos, type, null, children.toMutableList(), attributes)
}
}
/**
- * [CrdtTreePos] represent a position in the tree. It is used to identify a
- * position in the tree. It is composed of the parent ID and the left sibling ID.
- * If there's no left sibling in parent's children, then left sibling is parent.
+ * [CrdtTreePos] represents a position in the tree. It indicates the virtual
+ * location in the tree, so whether the node is split or not, we can find
+ * the adjacent node to pos by calling `map.floorEntry()`.
*/
-public data class CrdtTreePos internal constructor(
- val parentID: CrdtTreeNodeID,
- val leftSiblingID: CrdtTreeNodeID,
-) : JsonSerializable {
-
- override fun toStruct(): CrdtTreePosStruct {
- return CrdtTreePosStruct(parentID.toStruct(), leftSiblingID.toStruct())
- }
-}
-
-/**
- * [CrdtTreeNodeID] represent an ID of a node in the tree. It is used to
- * identify a node in the tree. It is composed of the creation time of the node
- * and the offset from the beginning of the node if the node is split.
- *
- * Some of replicas may have nodes that are not split yet. In this case, we can
- * use `map.floorEntry()` to find the adjacent node.
- */
-public data class CrdtTreeNodeID internal constructor(
+public data class CrdtTreePos(
/**
* Creation time of the node.
*/
@@ -672,17 +654,17 @@ public data class CrdtTreeNodeID internal constructor(
* The distance from the beginning of the node when the node is split.
*/
val offset: Int,
-) : Comparable, JsonSerializable {
+) : Comparable, JsonSerializable {
- override fun compareTo(other: CrdtTreeNodeID): Int {
+ override fun compareTo(other: CrdtTreePos): Int {
return compareValuesBy(this, other, { it.createdAt }, { it.offset })
}
- override fun toStruct(): CrdtTreeNodeIDStruct {
- return CrdtTreeNodeIDStruct(createdAt.toStruct(), offset)
+ override fun toStruct(): CrdtTreePosStruct {
+ return CrdtTreePosStruct(createdAt.toStruct(), offset)
}
companion object {
- internal val InitialCrdtTreeNodeID = CrdtTreeNodeID(InitialTimeTicket, 0)
+ internal val InitialCrdtTreePos = CrdtTreePos(InitialTimeTicket, 0)
}
}
diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/crdt/TextInfo.kt b/yorkie/src/main/kotlin/dev/yorkie/document/crdt/TextInfo.kt
index bdd447a30..8f51d60a7 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/document/crdt/TextInfo.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/document/crdt/TextInfo.kt
@@ -17,9 +17,18 @@ internal data class TextChange(
* The type of [TextChange].
*/
internal enum class TextChangeType {
- Content, Style
+ Content, Selection, Style
}
+/**
+ * Represents the selection of text range in the editor.
+ */
+internal data class Selection(
+ val from: RgaTreeSplitPos,
+ val to: RgaTreeSplitPos,
+ val executedAt: TimeTicket,
+)
+
internal data class TextValue(
val content: String,
private val _attributes: Rht = Rht(),
diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonElement.kt b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonElement.kt
index b85db0e34..121fe13d9 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonElement.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonElement.kt
@@ -31,7 +31,6 @@ public abstract class JsonElement {
CrdtText::class.java to JsonText::class.java,
CrdtPrimitive::class.java to JsonPrimitive::class.java,
CrdtCounter::class.java to JsonCounter::class.java,
- CrdtTree::class.java to JsonTree::class.java,
)
internal inline fun CrdtElement.toJsonElement(
diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonObject.kt b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonObject.kt
index 11433d83e..c140f7d55 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonObject.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonObject.kt
@@ -113,7 +113,7 @@ public class JsonObject internal constructor(
val ticket = context.issueTimeTicket()
val tree = CrdtTree(JsonTree.buildRoot(initialRoot, context), ticket)
setAndRegister(key, tree)
- return tree.toJsonElement(context)
+ return JsonTree(context, tree)
}
private fun setAndRegister(key: String, element: CrdtElement) {
diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonText.kt b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonText.kt
index 4b4efcaea..1fa6b46ec 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonText.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonText.kt
@@ -5,6 +5,7 @@ import dev.yorkie.document.crdt.CrdtText
import dev.yorkie.document.crdt.RgaTreeSplitPosRange
import dev.yorkie.document.crdt.TextWithAttributes
import dev.yorkie.document.operation.EditOperation
+import dev.yorkie.document.operation.SelectOperation
import dev.yorkie.document.operation.StyleOperation
import dev.yorkie.util.YorkieLogger
import dev.yorkie.document.RgaTreeSplitPosStruct as TextPosStruct
@@ -124,6 +125,41 @@ public class JsonText internal constructor(
return true
}
+ /**
+ * Selects the given range.
+ */
+ public fun select(fromIndex: Int, toIndex: Int): Boolean {
+ if (fromIndex > toIndex) {
+ YorkieLogger.e(TAG, "fromIndex should be less than or equal to toIndex")
+ return false
+ }
+
+ val range = createRange(fromIndex, toIndex) ?: return false
+
+ YorkieLogger.d(
+ TAG,
+ "SELT: f:$fromIndex->${range.first}, t:$toIndex->${range.second}",
+ )
+
+ val executedAt = context.issueTimeTicket()
+ try {
+ target.select(range, executedAt)
+ } catch (e: NoSuchElementException) {
+ YorkieLogger.e(TAG, "can't select text")
+ return false
+ }
+
+ context.push(
+ SelectOperation(
+ parentCreatedAt = target.createdAt,
+ fromPos = range.first,
+ toPos = range.second,
+ executedAt = executedAt,
+ ),
+ )
+ return true
+ }
+
private fun createRange(fromIndex: Int, toIndex: Int): RgaTreeSplitPosRange? {
return runCatching {
target.indexRangeToPosRange(fromIndex, toIndex)
diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt
index 67ad12a3b..d3cec3252 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt
@@ -5,10 +5,10 @@ import dev.yorkie.document.crdt.CrdtTree
import dev.yorkie.document.crdt.CrdtTreeNode
import dev.yorkie.document.crdt.CrdtTreeNode.Companion.CrdtTreeElement
import dev.yorkie.document.crdt.CrdtTreeNode.Companion.CrdtTreeText
-import dev.yorkie.document.crdt.CrdtTreeNodeID
import dev.yorkie.document.crdt.CrdtTreePos
import dev.yorkie.document.crdt.Rht
import dev.yorkie.document.crdt.TreePosRange
+import dev.yorkie.document.json.JsonTree.TreeNode
import dev.yorkie.document.operation.TreeEditOperation
import dev.yorkie.document.operation.TreeStyleOperation
import dev.yorkie.util.IndexTreeNode.Companion.DEFAULT_ROOT_TYPE
@@ -24,8 +24,8 @@ public typealias TreePosStructRange = Pair
public class JsonTree internal constructor(
internal val context: ChangeContext,
override val target: CrdtTree,
-) : JsonElement() {
- public val size: Int by target::size
+) : JsonElement(), Collection {
+ public override val size: Int by target::size
internal val indexTree by target::indexTree
@@ -105,28 +105,26 @@ public class JsonTree internal constructor(
editByPos(fromPos, toPos, contents.toList())
}
- private fun editByPos(
- fromPos: CrdtTreePos,
- toPos: CrdtTreePos,
- contents: List,
- ) {
+ private fun editByPos(fromPos: CrdtTreePos, toPos: CrdtTreePos, contents: List) {
if (contents.isNotEmpty()) {
validateTreeNodes(contents)
if (contents.first().type != DEFAULT_TEXT_TYPE) {
- contents.forEach { validateTreeNodes(listOf(it)) }
+ val children =
+ contents.filterIsInstance().flatMap(ElementNode::children)
+ validateTreeNodes(children)
}
}
- val ticket = context.lastTimeTicket
val crdtNodes = if (contents.firstOrNull()?.type == DEFAULT_TEXT_TYPE) {
val compVal = contents
.filterIsInstance()
.joinToString("") { it.value }
- listOf(CrdtTreeText(CrdtTreeNodeID(context.issueTimeTicket(), 0), compVal))
+ listOf(CrdtTreeText(CrdtTreePos(context.issueTimeTicket(), 0), compVal))
} else {
contents.map { createCrdtTreeNode(context, it) }
}
- val (_, maxCreatedAtMapByActor) = target.edit(
+ val ticket = context.lastTimeTicket
+ target.edit(
fromPos to toPos,
crdtNodes.map { it.deepCopy() }.ifEmpty { null },
ticket,
@@ -137,13 +135,12 @@ public class JsonTree internal constructor(
target.createdAt,
fromPos,
toPos,
- maxCreatedAtMapByActor,
crdtNodes.ifEmpty { null },
ticket,
),
)
- if (fromPos != toPos) {
+ if (fromPos.createdAt != toPos.createdAt || fromPos.offset != toPos.offset) {
context.registerElementHasRemovedNodes(target)
}
}
@@ -221,7 +218,7 @@ public class JsonTree internal constructor(
*/
public fun posRangeToIndexRange(range: TreePosStructRange): Pair {
val posRange = range.first.toOriginal() to range.second.toOriginal()
- return target.posRangeToIndexRange(posRange, context.lastTimeTicket)
+ return target.toIndex(posRange.first) to target.toIndex(posRange.second)
}
/**
@@ -229,7 +226,45 @@ public class JsonTree internal constructor(
*/
public fun posRangeToPathRange(range: TreePosStructRange): Pair, List> {
val posRange = range.first.toOriginal() to range.second.toOriginal()
- return target.posRangeToPathRange(posRange, context.lastTimeTicket)
+ return target.posRangeToPathRange(posRange)
+ }
+
+ override fun isEmpty(): Boolean = target.isEmpty()
+
+ override fun contains(element: TreeNode): Boolean {
+ return find { it == element } != null
+ }
+
+ override fun containsAll(elements: Collection): Boolean {
+ return toList().containsAll(elements)
+ }
+
+ override fun iterator(): Iterator {
+ return object : Iterator {
+ val targetIterator = target.iterator()
+
+ override fun hasNext(): Boolean {
+ return targetIterator.hasNext()
+ }
+
+ override fun next(): TreeNode {
+ return targetIterator.next().toTreeNode()
+ }
+
+ private fun CrdtTreeNode.toTreeNode(): TreeNode {
+ return if (isText) {
+ TextNode(value)
+ } else {
+ ElementNode(
+ type,
+ attributes,
+ children.map {
+ it.toTreeNode()
+ },
+ )
+ }
+ }
+ }
}
companion object {
@@ -238,12 +273,12 @@ public class JsonTree internal constructor(
* Returns the root node of this tree.
*/
internal fun buildRoot(initialRoot: ElementNode?, context: ChangeContext): CrdtTreeNode {
- val id = CrdtTreeNodeID(context.issueTimeTicket(), 0)
+ val pos = CrdtTreePos(context.issueTimeTicket(), 0)
if (initialRoot == null) {
- return CrdtTreeElement(id, DEFAULT_ROOT_TYPE)
+ return CrdtTreeElement(pos, DEFAULT_ROOT_TYPE)
}
// TODO(hackerwins): Need to use the ticket of operation of creating tree.
- return CrdtTreeElement(id, initialRoot.type).also { root ->
+ return CrdtTreeElement(pos, initialRoot.type).also { root ->
initialRoot.children.forEach { child ->
buildDescendants(child, root, context)
}
@@ -257,11 +292,11 @@ public class JsonTree internal constructor(
) {
val type = treeNode.type
val ticket = context.issueTimeTicket()
- val id = CrdtTreeNodeID(ticket, 0)
+ val pos = CrdtTreePos(ticket, 0)
when (treeNode) {
is TextNode -> {
- val textNode = CrdtTreeText(id, treeNode.value)
+ val textNode = CrdtTreeText(pos, treeNode.value)
parent.append(textNode)
return
}
@@ -275,7 +310,7 @@ public class JsonTree internal constructor(
attrs.set(key, value, ticket)
}
}
- val elementNode = CrdtTreeElement(id, type, attributes = attrs)
+ val elementNode = CrdtTreeElement(pos, type, attributes = attrs)
parent.append(elementNode)
treeNode.children.forEach { child ->
buildDescendants(child, elementNode, context)
@@ -289,16 +324,16 @@ public class JsonTree internal constructor(
*/
private fun createCrdtTreeNode(context: ChangeContext, content: TreeNode): CrdtTreeNode {
val ticket = context.issueTimeTicket()
- val id = CrdtTreeNodeID(ticket, 0)
+ val pos = CrdtTreePos(ticket, 0)
return when (content) {
is TextNode -> {
- CrdtTreeText(id, content.value)
+ CrdtTreeText(pos, content.value)
}
is ElementNode -> {
CrdtTreeElement(
- id,
+ pos,
content.type,
attributes = Rht().apply {
content.attributes.forEach { (key, value) ->
diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/operation/OperationInfo.kt b/yorkie/src/main/kotlin/dev/yorkie/document/operation/OperationInfo.kt
index 273e9a2a7..dfea2d74f 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/document/operation/OperationInfo.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/document/operation/OperationInfo.kt
@@ -2,11 +2,6 @@ package dev.yorkie.document.operation
import dev.yorkie.document.crdt.TextWithAttributes
import dev.yorkie.document.crdt.TreeNode
-import dev.yorkie.document.json.JsonArray
-import dev.yorkie.document.json.JsonCounter
-import dev.yorkie.document.json.JsonObject
-import dev.yorkie.document.json.JsonText
-import dev.yorkie.document.json.JsonTree
import dev.yorkie.document.time.TimeTicket
/**
@@ -19,71 +14,48 @@ public sealed class OperationInfo {
internal var executedAt: TimeTicket = TimeTicket.InitialTimeTicket
- /**
- * [TextOperationInfo] represents the [OperationInfo] for the [JsonText].
- */
- public interface TextOperationInfo
-
- /**
- * [CounterOperationInfo] represents the [OperationInfo] for the [JsonCounter].
- */
- public interface CounterOperationInfo
-
- /**
- * [ArrayOperationInfo] represents the [OperationInfo] for the [JsonArray].
- */
- public interface ArrayOperationInfo
-
- /**
- * [ObjectOperationInfo] represents the [OperationInfo] for the [JsonObject].
- */
- public interface ObjectOperationInfo
-
- /**
- * [TreeOperationInfo] represents the [OperationInfo] for the [JsonTree].
- */
- public interface TreeOperationInfo
+ public interface TextOpInfo
public data class AddOpInfo(val index: Int, override var path: String = INITIAL_PATH) :
- OperationInfo(), ArrayOperationInfo
+ OperationInfo()
public data class MoveOpInfo(
val previousIndex: Int,
val index: Int,
override var path: String = INITIAL_PATH,
- ) : OperationInfo(), ArrayOperationInfo
+ ) : OperationInfo()
public data class SetOpInfo(val key: String, override var path: String = INITIAL_PATH) :
- OperationInfo(), ObjectOperationInfo
+ OperationInfo()
public data class RemoveOpInfo(
val key: String?,
val index: Int?,
override var path: String = INITIAL_PATH,
- ) : OperationInfo(), ArrayOperationInfo, ObjectOperationInfo
+ ) : OperationInfo()
public data class IncreaseOpInfo(val value: Number, override var path: String = INITIAL_PATH) :
- OperationInfo(), CounterOperationInfo
+ OperationInfo()
public data class EditOpInfo(
val from: Int,
val to: Int,
val value: TextWithAttributes,
override var path: String = INITIAL_PATH,
- ) : OperationInfo(), TextOperationInfo
+ ) : OperationInfo(), TextOpInfo
public data class StyleOpInfo(
val from: Int,
val to: Int,
val attributes: Map,
override var path: String = INITIAL_PATH,
- ) : OperationInfo(), TextOperationInfo
+ ) : OperationInfo(), TextOpInfo
public data class SelectOpInfo(
val from: Int,
val to: Int,
override var path: String = INITIAL_PATH,
- ) : OperationInfo(), TextOperationInfo
+ ) : OperationInfo(), TextOpInfo
public data class TreeEditOpInfo(
val from: Int,
@@ -92,7 +64,7 @@ public sealed class OperationInfo {
val toPath: List,
val nodes: List?,
override var path: String = INITIAL_PATH,
- ) : OperationInfo(), TreeOperationInfo
+ ) : OperationInfo()
public data class TreeStyleOpInfo(
val from: Int,
@@ -101,7 +73,7 @@ public sealed class OperationInfo {
val toPath: List,
val attributes: Map,
override var path: String = INITIAL_PATH,
- ) : OperationInfo(), TreeOperationInfo
+ ) : OperationInfo()
companion object {
private const val INITIAL_PATH = "initial path"
diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/operation/SelectOperation.kt b/yorkie/src/main/kotlin/dev/yorkie/document/operation/SelectOperation.kt
new file mode 100644
index 000000000..96522dfed
--- /dev/null
+++ b/yorkie/src/main/kotlin/dev/yorkie/document/operation/SelectOperation.kt
@@ -0,0 +1,43 @@
+package dev.yorkie.document.operation
+
+import dev.yorkie.document.crdt.CrdtRoot
+import dev.yorkie.document.crdt.CrdtText
+import dev.yorkie.document.crdt.RgaTreeSplitPos
+import dev.yorkie.document.crdt.RgaTreeSplitPosRange
+import dev.yorkie.document.time.TimeTicket
+import dev.yorkie.util.YorkieLogger
+
+internal data class SelectOperation(
+ val fromPos: RgaTreeSplitPos,
+ val toPos: RgaTreeSplitPos,
+ override val parentCreatedAt: TimeTicket,
+ override var executedAt: TimeTicket,
+) : Operation() {
+
+ override val effectedCreatedAt: TimeTicket
+ get() = parentCreatedAt
+
+ /**
+ * Returns the created time of the effected element.
+ */
+ override fun execute(root: CrdtRoot): List {
+ val parentObject = root.findByCreatedAt(parentCreatedAt)
+ return if (parentObject is CrdtText) {
+ val change = parentObject.select(RgaTreeSplitPosRange(fromPos, toPos), executedAt)
+ ?: return emptyList()
+ listOf(
+ OperationInfo.SelectOpInfo(from = change.from, to = change.to).apply {
+ executedAt = parentCreatedAt
+ },
+ )
+ } else {
+ parentObject ?: YorkieLogger.e(TAG, "fail to find $parentCreatedAt")
+ YorkieLogger.e(TAG, "fail to execute, only Text, RichText can execute select")
+ emptyList()
+ }
+ }
+
+ companion object {
+ private const val TAG = "SelectOperation"
+ }
+}
diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/operation/TreeEditOperation.kt b/yorkie/src/main/kotlin/dev/yorkie/document/operation/TreeEditOperation.kt
index f0af2f7ce..8835350d8 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/document/operation/TreeEditOperation.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/document/operation/TreeEditOperation.kt
@@ -4,7 +4,6 @@ import dev.yorkie.document.crdt.CrdtRoot
import dev.yorkie.document.crdt.CrdtTree
import dev.yorkie.document.crdt.CrdtTreeNode
import dev.yorkie.document.crdt.CrdtTreePos
-import dev.yorkie.document.time.ActorID
import dev.yorkie.document.time.TimeTicket
import dev.yorkie.util.YorkieLogger
@@ -15,7 +14,6 @@ internal data class TreeEditOperation(
override val parentCreatedAt: TimeTicket,
val fromPos: CrdtTreePos,
val toPos: CrdtTreePos,
- val maxCreatedAtMapByActor: Map,
val contents: List?,
override var executedAt: TimeTicket,
) : Operation() {
@@ -32,14 +30,9 @@ internal data class TreeEditOperation(
return emptyList()
}
val changes =
- tree.edit(
- fromPos to toPos,
- contents?.map(CrdtTreeNode::deepCopy),
- executedAt,
- maxCreatedAtMapByActor,
- ).first
+ tree.edit(fromPos to toPos, contents?.map(CrdtTreeNode::deepCopy), executedAt)
- if (fromPos != toPos) {
+ if (fromPos.createdAt != toPos.createdAt || fromPos.offset != toPos.offset) {
root.registerElementHasRemovedNodes(tree)
}
diff --git a/yorkie/src/main/kotlin/dev/yorkie/util/IndexTree.kt b/yorkie/src/main/kotlin/dev/yorkie/util/IndexTree.kt
index 583fba1dc..b28b1c50a 100644
--- a/yorkie/src/main/kotlin/dev/yorkie/util/IndexTree.kt
+++ b/yorkie/src/main/kotlin/dev/yorkie/util/IndexTree.kt
@@ -67,7 +67,7 @@ internal class IndexTree>(val root: T) {
fun nodesBetween(
from: Int,
to: Int,
- action: (T, TagContained) -> Unit,
+ action: (T) -> Unit,
) {
nodesBetweenInternal(root, from, to, action)
}
@@ -76,13 +76,12 @@ internal class IndexTree>(val root: T) {
* Iterates the nodes between the given range.
* If the given range is collapsed, the callback is not called.
* It traverses the tree with postorder traversal.
- * NOTE(sejongk): Nodes should not be removed in callback, because it leads to wrong behaviors.
*/
private fun nodesBetweenInternal(
root: T,
from: Int,
to: Int,
- action: ((T, TagContained) -> Unit),
+ action: ((T) -> Unit),
) {
if (from > to) {
throw IllegalArgumentException("from is greater than to: $from > $to")
@@ -114,12 +113,7 @@ internal class IndexTree>(val root: T) {
// If the range spans outside the child,
// the callback is called with the child.
if (fromChild < 0 || toChild > child.size || child.isText) {
- val contained = when {
- (fromChild < 0 && toChild > child.size) || child.isText -> TagContained.All
- fromChild < 0 -> TagContained.Opening
- else -> TagContained.Closing
- }
- action.invoke(child, contained)
+ action.invoke(child)
}
}
pos += child.paddedSize
@@ -145,7 +139,7 @@ internal class IndexTree>(val root: T) {
val currentNode = node
if (currentNode == null || currentNode == root) return@repeat
- currentNode.split(offset, 0)
+ currentNode.split(offset)
val nextOffset = currentNode.parent?.findOffset(currentNode) ?: return@repeat
offset = if (offset == 0) nextOffset else nextOffset + 1
@@ -245,7 +239,7 @@ internal class IndexTree>(val root: T) {
private fun addSizeOfLeftSiblings(parent: T, offset: Int): Int {
return parent.children.take(offset).fold(0) { acc, leftSibling ->
- acc + if (leftSibling.isRemoved) 0 else leftSibling.paddedSize
+ acc + leftSibling.paddedSize
}
}
@@ -300,6 +294,18 @@ internal class IndexTree>(val root: T) {
return TreePos(updatedNode, updatedPathElement)
}
+ /**
+ * Finds right node of the given [treePos] with postorder traversal.
+ */
+ fun findPostorderRight(treePos: TreePos): T? {
+ val (node, offset) = treePos
+ return when {
+ node.isText -> if (node.size == offset) node.nextSibling ?: node.parent else node
+ node.children.size == offset -> node
+ else -> findLeftMost(node.children[offset])
+ }
+ }
+
private fun findLeftMost(node: T): T {
return if (node.isText || node.children.isEmpty()) {
node
@@ -350,19 +356,12 @@ internal class IndexTree>(val root: T) {
/**
* Returns the path of the given [index].
*/
- fun indexToPath(index: Int): List {
+ public fun indexToPath(index: Int): List {
val treePos = findTreePos(index)
return treePosToPath(treePos)
}
}
-/**
- * [TagContained] represents whether the opening or closing tag of a element is selected.
- */
-internal enum class TagContained {
- All, Opening, Closing,
-}
-
/**
* [TreePos] is the position of a node in the tree.
*
@@ -468,13 +467,6 @@ internal abstract class IndexTreeNode>(children: MutableLis
check(!isText) {
"Text node cannot have children"
}
- if (node.isRemoved) {
- val index = _children.indexOf(node)
-
- // If nodes are removed, the offset of the removed node is the number of
- // nodes before the node excluding the removed nodes.
- return allChildren.take(index).filterNot { it.isRemoved }.size
- }
return children.indexOf(node)
}
@@ -510,10 +502,7 @@ internal abstract class IndexTreeNode>(children: MutableLis
_children.addAll(newNode)
newNode.forEach { node ->
node.parent = this as T
-
- if (!node.isRemoved) {
- node.updateAncestorSize()
- }
+ node.updateAncestorSize()
}
}
@@ -528,10 +517,7 @@ internal abstract class IndexTreeNode>(children: MutableLis
_children.addAll(0, newNode.toList())
newNode.forEach { node ->
node.parent = this as T
-
- if (!node.isRemoved) {
- node.updateAncestorSize()
- }
+ node.updateAncestorSize()
}
}
@@ -604,24 +590,24 @@ internal abstract class IndexTreeNode>(children: MutableLis
/**
* Splits the node at the given [offset].
*/
- fun split(offset: Int, absOffset: Int): T? {
+ fun split(offset: Int): T? {
return if (isText) {
- splitText(offset, absOffset)
+ splitText(offset)
} else {
splitElement(offset)
}
}
- private fun splitText(offset: Int, absOffset: Int): T? {
+ private fun splitText(offset: Int): T? {
if (offset == 0 || offset == size) {
return null
}
val leftValue = value.substring(0, offset)
- val rightValue = value.substring(offset).takeUnless { it.isEmpty() } ?: return null
+ val rightValue = value.substring(offset)
value = leftValue
- val rightNode = clone(offset + absOffset)
+ val rightNode = clone(offset)
rightNode.value = rightValue
parent?.insertAfterInternal(this as T, rightNode)
diff --git a/yorkie/src/test/kotlin/dev/yorkie/api/ConverterTest.kt b/yorkie/src/test/kotlin/dev/yorkie/api/ConverterTest.kt
index b91b6b109..206a8f7d5 100644
--- a/yorkie/src/test/kotlin/dev/yorkie/api/ConverterTest.kt
+++ b/yorkie/src/test/kotlin/dev/yorkie/api/ConverterTest.kt
@@ -18,7 +18,6 @@ import dev.yorkie.document.crdt.CrdtText
import dev.yorkie.document.crdt.CrdtTree
import dev.yorkie.document.crdt.CrdtTreeNode.Companion.CrdtTreeElement
import dev.yorkie.document.crdt.CrdtTreeNode.Companion.CrdtTreeText
-import dev.yorkie.document.crdt.CrdtTreeNodeID
import dev.yorkie.document.crdt.CrdtTreePos
import dev.yorkie.document.crdt.ElementRht
import dev.yorkie.document.crdt.RgaTreeList
@@ -32,15 +31,14 @@ import dev.yorkie.document.operation.MoveOperation
import dev.yorkie.document.operation.Operation
import dev.yorkie.document.operation.OperationInfo
import dev.yorkie.document.operation.RemoveOperation
+import dev.yorkie.document.operation.SelectOperation
import dev.yorkie.document.operation.SetOperation
import dev.yorkie.document.operation.StyleOperation
import dev.yorkie.document.operation.TreeEditOperation
import dev.yorkie.document.operation.TreeStyleOperation
import dev.yorkie.document.time.ActorID
-import dev.yorkie.document.time.ActorID.Companion.INITIAL_ACTOR_ID
import dev.yorkie.document.time.TimeTicket
import dev.yorkie.document.time.TimeTicket.Companion.InitialTimeTicket
-import dev.yorkie.document.time.TimeTicket.Companion.MaxTimeTicket
import dev.yorkie.util.IndexTreeNode.Companion.DEFAULT_ROOT_TYPE
import org.junit.Assert.assertThrows
import org.junit.Test
@@ -52,7 +50,7 @@ class ConverterTest {
@Test
fun `should convert ByteString`() {
- val actorID = INITIAL_ACTOR_ID
+ val actorID = ActorID.INITIAL_ACTOR_ID
val converted = actorID.toByteString().toActorID()
val maxActorID = ActorID.MAX_ACTOR_ID
val maxConverted = maxActorID.toByteString().toActorID()
@@ -234,6 +232,12 @@ class ConverterTest {
InitialTimeTicket,
mapOf("style" to "bold"),
)
+ val selectOperation = SelectOperation(
+ nodePos,
+ nodePos,
+ InitialTimeTicket,
+ InitialTimeTicket,
+ )
val styleOperation = StyleOperation(
nodePos,
nodePos,
@@ -243,22 +247,15 @@ class ConverterTest {
)
val treeEditOperation = TreeEditOperation(
InitialTimeTicket,
- CrdtTreePos(CrdtTreeNodeID(InitialTimeTicket, 5), CrdtTreeNodeID(InitialTimeTicket, 5)),
- CrdtTreePos(
- CrdtTreeNodeID(InitialTimeTicket, 10),
- CrdtTreeNodeID(InitialTimeTicket, 10),
- ),
- mapOf(INITIAL_ACTOR_ID to MaxTimeTicket),
- listOf(CrdtTreeText(CrdtTreeNodeID(InitialTimeTicket, 0), "hi")),
+ CrdtTreePos(InitialTimeTicket, 5),
+ CrdtTreePos(InitialTimeTicket, 10),
+ listOf(CrdtTreeText(CrdtTreePos(InitialTimeTicket, 0), "hi")),
InitialTimeTicket,
)
val treeStyleOperation = TreeStyleOperation(
InitialTimeTicket,
- CrdtTreePos(CrdtTreeNodeID(InitialTimeTicket, 5), CrdtTreeNodeID(InitialTimeTicket, 5)),
- CrdtTreePos(
- CrdtTreeNodeID(InitialTimeTicket, 10),
- CrdtTreeNodeID(InitialTimeTicket, 10),
- ),
+ CrdtTreePos(InitialTimeTicket, 5),
+ CrdtTreePos(InitialTimeTicket, 10),
mapOf("a" to "b"),
InitialTimeTicket,
)
@@ -270,6 +267,7 @@ class ConverterTest {
increaseOperation.toPBOperation(),
editOperationWithoutAttrs.toPBOperation(),
editOperationWithAttrs.toPBOperation(),
+ selectOperation.toPBOperation(),
styleOperation.toPBOperation(),
treeEditOperation.toPBOperation(),
treeStyleOperation.toPBOperation(),
@@ -282,9 +280,10 @@ class ConverterTest {
assertEquals(increaseOperation, converted[4])
assertEquals(editOperationWithoutAttrs, converted[5])
assertEquals(editOperationWithAttrs, converted[6])
- assertEquals(styleOperation, converted[7])
- assertEquals(treeEditOperation, converted[8])
- assertEquals(treeStyleOperation, converted[9])
+ assertEquals(selectOperation, converted[7])
+ assertEquals(styleOperation, converted[8])
+ assertEquals(treeEditOperation, converted[9])
+ assertEquals(treeStyleOperation, converted[10])
}
@Test
@@ -392,7 +391,7 @@ class ConverterTest {
val crdtCounter = CrdtCounter(1, InitialTimeTicket)
val crdtText = CrdtText(RgaTreeSplit(), InitialTimeTicket)
val crdtTree = CrdtTree(
- CrdtTreeElement(CrdtTreeNodeID(InitialTimeTicket, 0), DEFAULT_ROOT_TYPE),
+ CrdtTreeElement(CrdtTreePos(InitialTimeTicket, 0), DEFAULT_ROOT_TYPE),
InitialTimeTicket,
)
val crdtElements = listOf(
diff --git a/yorkie/src/test/kotlin/dev/yorkie/core/ClientTest.kt b/yorkie/src/test/kotlin/dev/yorkie/core/ClientTest.kt
index 8e7bdbef1..2016995a4 100644
--- a/yorkie/src/test/kotlin/dev/yorkie/core/ClientTest.kt
+++ b/yorkie/src/test/kotlin/dev/yorkie/core/ClientTest.kt
@@ -1,6 +1,8 @@
package dev.yorkie.core
+import com.google.protobuf.ByteString
import dev.yorkie.api.PBChangePack
+import dev.yorkie.api.toActorID
import dev.yorkie.api.toChangePack
import dev.yorkie.api.v1.ActivateClientRequest
import dev.yorkie.api.v1.AttachDocumentRequest
@@ -11,8 +13,8 @@ import dev.yorkie.api.v1.RemoveDocumentRequest
import dev.yorkie.api.v1.WatchDocumentRequest
import dev.yorkie.api.v1.YorkieServiceGrpcKt
import dev.yorkie.assertJsonContentEquals
-import dev.yorkie.core.Client.Event.DocumentChanged
import dev.yorkie.core.Client.Event.DocumentSynced
+import dev.yorkie.core.Client.Event.DocumentsChanged
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
@@ -26,7 +28,6 @@ import dev.yorkie.document.change.Change
import dev.yorkie.document.change.ChangeID
import dev.yorkie.document.change.ChangePack
import dev.yorkie.document.change.CheckPoint
-import dev.yorkie.document.time.ActorID
import io.grpc.Channel
import io.grpc.inprocess.InProcessChannelBuilder
import io.grpc.inprocess.InProcessServerBuilder
@@ -86,6 +87,7 @@ class ClientTest {
assertTrue(target.isActive)
val activatedStatus = assertIs(target.status.value)
+ assertEquals(TEST_KEY, activatedStatus.clientKey)
assertEquals(TEST_ACTOR_ID, activatedStatus.clientId)
val deactivateRequestCaptor = argumentCaptor()
@@ -136,8 +138,8 @@ class ClientTest {
val watchRequestCaptor = argumentCaptor()
target.attachAsync(document).await()
- val event = target.events.first { it is DocumentChanged }
- val changeEvent = assertIs(event)
+ val event = target.events.first { it is DocumentsChanged }
+ val changeEvent = assertIs(event)
verify(service, atLeastOnce()).watchDocument(watchRequestCaptor.capture())
assertIsTestActorID(watchRequestCaptor.firstValue.clientId)
assertEquals(1, changeEvent.documentKeys.size)
@@ -281,8 +283,8 @@ class ClientTest {
}
}
- private fun assertIsTestActorID(clientId: String) {
- assertEquals(TEST_ACTOR_ID, ActorID(clientId))
+ private fun assertIsTestActorID(clientId: ByteString) {
+ assertEquals(TEST_ACTOR_ID, clientId.toActorID())
}
private fun assertIsInitialChangePack(changePack: PBChangePack) {
diff --git a/yorkie/src/test/kotlin/dev/yorkie/core/MockYorkieService.kt b/yorkie/src/test/kotlin/dev/yorkie/core/MockYorkieService.kt
index 63f6f3979..e033f4b6c 100644
--- a/yorkie/src/test/kotlin/dev/yorkie/core/MockYorkieService.kt
+++ b/yorkie/src/test/kotlin/dev/yorkie/core/MockYorkieService.kt
@@ -2,6 +2,7 @@ package dev.yorkie.core
import com.google.protobuf.kotlin.toByteString
import dev.yorkie.api.PBTimeTicket
+import dev.yorkie.api.toByteString
import dev.yorkie.api.toPBChange
import dev.yorkie.api.toPBTimeTicket
import dev.yorkie.api.v1.ActivateClientRequest
@@ -53,14 +54,17 @@ class MockYorkieService : YorkieServiceGrpcKt.YorkieServiceCoroutineImplBase() {
override suspend fun activateClient(request: ActivateClientRequest): ActivateClientResponse {
return activateClientResponse {
- clientId = TEST_ACTOR_ID.value
+ clientId = TEST_ACTOR_ID.toByteString()
+ clientKey = request.clientKey
}
}
override suspend fun deactivateClient(
request: DeactivateClientRequest,
): DeactivateClientResponse {
- return deactivateClientResponse { }
+ return deactivateClientResponse {
+ clientId = request.clientId
+ }
}
override suspend fun attachDocument(request: AttachDocumentRequest): AttachDocumentResponse {
@@ -68,6 +72,7 @@ class MockYorkieService : YorkieServiceGrpcKt.YorkieServiceCoroutineImplBase() {
throw StatusException(Status.UNKNOWN)
}
return attachDocumentResponse {
+ clientId = request.clientId
changePack = changePack {
documentKey = request.changePack.documentKey
changes.add(
@@ -93,7 +98,9 @@ class MockYorkieService : YorkieServiceGrpcKt.YorkieServiceCoroutineImplBase() {
if (request.changePack.documentKey == DETACH_ERROR_DOCUMENT_KEY) {
throw StatusException(Status.UNKNOWN)
}
- return detachDocumentResponse { }
+ return detachDocumentResponse {
+ clientKey = TEST_KEY
+ }
}
override suspend fun pushPullChanges(request: PushPullChangesRequest): PushPullChangesResponse {
@@ -101,6 +108,7 @@ class MockYorkieService : YorkieServiceGrpcKt.YorkieServiceCoroutineImplBase() {
throw StatusException(Status.UNAVAILABLE)
}
return pushPullChangesResponse {
+ clientId = request.clientId
changePack = changePack {
minSyncedTicket = InitialTimeTicket.toPBTimeTicket()
changes.add(
@@ -141,7 +149,7 @@ class MockYorkieService : YorkieServiceGrpcKt.YorkieServiceCoroutineImplBase() {
emit(
watchDocumentResponse {
initialization = initialization {
- clientIds.add(TEST_ACTOR_ID.value)
+ clientIds.add(TEST_ACTOR_ID.toByteString())
}
},
)
@@ -152,7 +160,7 @@ class MockYorkieService : YorkieServiceGrpcKt.YorkieServiceCoroutineImplBase() {
emit(
watchDocumentResponse {
event = docEvent {
- type = DocEventType.DOC_EVENT_TYPE_DOCUMENT_CHANGED
+ type = DocEventType.DOC_EVENT_TYPE_DOCUMENTS_CHANGED
publisher = request.clientId
}
},
@@ -161,7 +169,7 @@ class MockYorkieService : YorkieServiceGrpcKt.YorkieServiceCoroutineImplBase() {
emit(
watchDocumentResponse {
event = docEvent {
- type = DocEventType.DOC_EVENT_TYPE_DOCUMENT_WATCHED
+ type = DocEventType.DOC_EVENT_TYPE_DOCUMENTS_WATCHED
publisher = request.clientId
}
},
@@ -170,7 +178,7 @@ class MockYorkieService : YorkieServiceGrpcKt.YorkieServiceCoroutineImplBase() {
emit(
watchDocumentResponse {
event = docEvent {
- type = DocEventType.DOC_EVENT_TYPE_DOCUMENT_UNWATCHED
+ type = DocEventType.DOC_EVENT_TYPE_DOCUMENTS_UNWATCHED
publisher = request.clientId
}
},
@@ -183,6 +191,7 @@ class MockYorkieService : YorkieServiceGrpcKt.YorkieServiceCoroutineImplBase() {
throw StatusException(Status.UNAVAILABLE)
}
return removeDocumentResponse {
+ clientKey = TEST_KEY
changePack = changePack {
minSyncedTicket = InitialTimeTicket.toPBTimeTicket()
changes.add(
diff --git a/yorkie/src/test/kotlin/dev/yorkie/document/crdt/CrdtTextTest.kt b/yorkie/src/test/kotlin/dev/yorkie/document/crdt/CrdtTextTest.kt
index 302973b05..3ccc5926b 100644
--- a/yorkie/src/test/kotlin/dev/yorkie/document/crdt/CrdtTextTest.kt
+++ b/yorkie/src/test/kotlin/dev/yorkie/document/crdt/CrdtTextTest.kt
@@ -1,5 +1,6 @@
package dev.yorkie.document.crdt
+import dev.yorkie.document.time.ActorID
import dev.yorkie.document.time.TimeTicket
import org.junit.Before
import org.junit.Test
@@ -45,4 +46,13 @@ class CrdtTextTest {
target.toJson(),
)
}
+
+ @Test
+ fun `should handle select operations`() {
+ target.edit(target.indexRangeToPosRange(0, 0), "ABCD", TimeTicket.InitialTimeTicket)
+ val executedAt = TimeTicket(1L, 1u, ActorID.INITIAL_ACTOR_ID)
+ val textChange = target.select(target.indexRangeToPosRange(2, 4), executedAt)
+ assertEquals(TextChangeType.Selection, textChange?.type)
+ assertEquals(2 to 4, textChange?.from to textChange?.to)
+ }
}
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 9b1752734..53e2252f2 100644
--- a/yorkie/src/test/kotlin/dev/yorkie/document/crdt/CrdtTreeTest.kt
+++ b/yorkie/src/test/kotlin/dev/yorkie/document/crdt/CrdtTreeTest.kt
@@ -11,6 +11,7 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
+import dev.yorkie.document.crdt.CrdtTreePos.Companion.InitialCrdtTreePos as ITP
class CrdtTreeTest {
@@ -23,8 +24,8 @@ class CrdtTreeTest {
@Test
fun `should create CrdtTreeNode`() {
- val node = CrdtTreeText(DIP, "hello")
- assertEquals(DIP, node.id)
+ val node = CrdtTreeText(ITP, "hello")
+ assertEquals(ITP, node.pos)
assertEquals(DEFAULT_TEXT_TYPE, node.type)
assertEquals("hello", node.value)
assertEquals(5, node.size)
@@ -34,39 +35,42 @@ class CrdtTreeTest {
@Test
fun `should split CrdtTreeNode`() {
- val para = CrdtTreeElement(DIP, "p")
- para.append(CrdtTreeText(DIP, "helloyorkie"))
+ val para = CrdtTreeElement(ITP, "p")
+ para.append(CrdtTreeText(ITP, "helloyorkie"))
assertEquals("helloyorkie
", para.toXml())
assertEquals(11, para.size)
assertFalse(para.isText)
val left = para.children.first()
- val right = left.split(5, 0)
+ val right = left.split(5)
assertEquals(11, para.size)
assertFalse(para.isText)
assertEquals("hello", left.value)
assertEquals("yorkie", right?.value)
- assertEquals(CrdtTreeNodeID(TimeTicket.InitialTimeTicket, 0), left.id)
- assertEquals(CrdtTreeNodeID(TimeTicket.InitialTimeTicket, 5), right?.id)
+ assertEquals(CrdtTreePos(TimeTicket.InitialTimeTicket, 0), left.pos)
+ assertEquals(CrdtTreePos(TimeTicket.InitialTimeTicket, 5), right?.pos)
}
@Test
fun `should insert nodes with editByIndex`() {
// 0
//
- assertTrue(target.size == 0)
+ assertTrue(target.isEmpty())
assertEquals("", target.toXml())
+ assertEquals(listOf("root"), target.toList())
// 1
//
target.editByIndex(0 to 0, CrdtTreeElement(issuePos(), "p").toList(), issueTime())
assertEquals("", target.toXml())
+ assertEquals(listOf("p", "root"), target.toList())
assertEquals(2, target.root.size)
// 1
// h e l l o
target.editByIndex(1 to 1, CrdtTreeText(issuePos(), "hello").toList(), issueTime())
assertEquals("hello
", target.toXml())
+ assertEquals(listOf("text.hello", "p", "root"), target.toList())
assertEquals(7, target.root.size)
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
@@ -76,16 +80,25 @@ class CrdtTreeTest {
}
target.editByIndex(7 to 7, p.toList(), issueTime())
assertEquals("hello
world
", target.toXml())
+ assertEquals(listOf("text.hello", "p", "text.world", "p", "root"), target.toList())
assertEquals(14, target.root.size)
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// h e l l o !
w o r l d
target.editByIndex(6 to 6, CrdtTreeText(issuePos(), "!").toList(), issueTime())
assertEquals("hello!
world
", target.toXml())
+ assertEquals(
+ listOf("text.hello", "text.!", "p", "text.world", "p", "root"),
+ target.toList(),
+ )
assertEquals(15, target.root.size)
target.editByIndex(6 to 6, CrdtTreeText(issuePos(), "~").toList(), issueTime())
assertEquals("hello~!
world
", target.toXml())
+ assertEquals(
+ listOf("text.hello", "text.~", "text.!", "p", "text.world", "p", "root"),
+ target.toList(),
+ )
assertEquals(16, target.root.size)
}
@@ -99,19 +112,16 @@ class CrdtTreeTest {
target.editByIndex(4 to 4, CrdtTreeElement(issuePos(), "p").toList(), issueTime())
target.editByIndex(5 to 5, CrdtTreeText(issuePos(), "cd").toList(), issueTime())
assertEquals("ab
cd
", target.toXml())
+ assertEquals(listOf("text.ab", "p", "text.cd", "p", "root"), target.toList())
assertEquals(8, target.root.size)
// 02. delete b from first paragraph
// 0 1 2 3 4 5 6 7
// a
c d
- target.editByIndex(2 to 6, null, issueTime())
- // 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.editByIndex(1 to 1, CrdtTreeText(issuePos(), "@").toList(), issueTime())
- // TODO(7hong13): should be resolved after the JS SDK implementation
- // assertEquals("root>@ad
", target.toXml())
+ target.editByIndex(2 to 3, null, issueTime())
+ assertEquals("a
cd
", target.toXml())
+ assertEquals(listOf("text.a", "p", "text.cd", "p", "root"), target.toList())
+ assertEquals(7, target.root.size)
}
@Test
@@ -124,117 +134,130 @@ class CrdtTreeTest {
target.editByIndex(4 to 4, CrdtTreeElement(issuePos(), "p").toList(), issueTime())
target.editByIndex(5 to 5, CrdtTreeText(issuePos(), "cd").toList(), issueTime())
assertEquals("ab
cd
", target.toXml())
+ assertEquals(listOf("text.ab", "p", "text.cd", "p", "root"), target.toList())
- // 02. delete b from first paragraph
- // 0 1 2 3 4 5 6 7
- // a
c d
- target.editByIndex(2 to 3, null, issueTime())
- assertEquals("a
cd
", target.toXml())
+ // 02. delete b, c and first paragraph.
+ // 0 1 2 3 4
+ // a d
+ target.editByIndex(2 to 6, null, issueTime())
+ assertEquals("ad
", target.toXml())
+
+ // 03. insert a new text node at the start of the first paragraph.
+ target.editByIndex(1 to 1, CrdtTreeText(issuePos(), "@").toList(), issueTime())
+ assertEquals("@ad
", target.toXml())
}
@Test
- fun `should find the closest TreePos when parentNode or leftSiblingNode does not exist`() {
- val pNode = CrdtTreeElement(issuePos(), "p")
- val textNode = CrdtTreeText(issuePos(), "ab")
+ fun `should merge and edit different levels with editByIndex`() {
+ fun initializeTree() {
+ setUp()
+ target.editByIndex(0 to 0, CrdtTreeElement(issuePos(), "p").toList(), issueTime())
+ target.editByIndex(1 to 1, CrdtTreeElement(issuePos(), "b").toList(), issueTime())
+ target.editByIndex(2 to 2, CrdtTreeElement(issuePos(), "i").toList(), issueTime())
+ target.editByIndex(3 to 3, CrdtTreeText(issuePos(), "ab").toList(), issueTime())
+ assertEquals("ab
", target.toXml())
+ }
- // 0 1 2 3 4
- // a b
- target.editByIndex(0 to 0, pNode.toList(), issueTime())
- target.editByIndex(1 to 1, textNode.toList(), issueTime())
+ // 01. edit between two element nodes in the same hierarchy.
+ // 0 1 2 3 4 5 6 7 8
+ // a b
+ initializeTree()
+ target.editByIndex(5 to 6, null, issueTime())
+ assertEquals("ab
", target.toXml())
+
+ // 02. edit between two element nodes in same hierarchy.
+ initializeTree()
+ target.editByIndex(6 to 7, null, issueTime())
+ assertEquals("ab
", target.toXml())
+
+ // 03. edit between text and element node in different hierarchy.
+ initializeTree()
+ target.editByIndex(4 to 6, null, issueTime())
+ assertEquals("a
", target.toXml())
+
+ // 04. edit between text and element node in different hierarchy.
+ initializeTree()
+ target.editByIndex(5 to 7, null, issueTime())
assertEquals("ab
", target.toXml())
- // Find the closest index.TreePos when leftSiblingNode in crdt.TreePos is removed.
- // 0 1 2
- //
- target.editByIndex(1 to 3, null, issueTime())
- assertEquals("", target.toXml())
+ // 05. edit between text and element node in different hierarchy.
+ initializeTree()
+ target.editByIndex(4 to 7, null, issueTime())
+ assertEquals("a
", target.toXml())
- var (parent, left) = target.findNodesAndSplitText(
- CrdtTreePos(pNode.id, textNode.id),
- issueTime(),
- )
- assertEquals(1, target.toIndex(parent, left))
+ // 06. edit between text and element node in different hierarchy.
+ initializeTree()
+ target.editByIndex(3 to 7, null, issueTime())
+ assertEquals("", target.toXml())
- // Find the closest index.TreePos when parentNode in crdt.TreePos is removed.
- // 0
- //
- target.editByIndex(0 to 2, null, issueTime())
- assertEquals("", target.toXml())
+ // 07. edit between text and element node in same hierarchy.
+ setUp()
+ target.editByIndex(0 to 0, CrdtTreeElement(issuePos(), "p").toList(), issueTime())
+ target.editByIndex(1 to 1, CrdtTreeText(issuePos(), "ab").toList(), issueTime())
+ target.editByIndex(4 to 4, CrdtTreeElement(issuePos(), "p").toList(), issueTime())
+ target.editByIndex(5 to 5, CrdtTreeElement(issuePos(), "b").toList(), issueTime())
+ target.editByIndex(6 to 6, CrdtTreeText(issuePos(), "cd").toList(), issueTime())
+ target.editByIndex(10 to 10, CrdtTreeElement(issuePos(), "p").toList(), issueTime())
+ target.editByIndex(11 to 11, CrdtTreeText(issuePos(), "ef").toList(), issueTime())
+ assertEquals("ab
cd
ef
", target.toXml())
- target.findNodesAndSplitText(CrdtTreePos(pNode.id, textNode.id), issueTime()).also {
- parent = it.first
- left = it.second
- }
- assertEquals(0, target.toIndex(parent, left))
+ target.editByIndex(9 to 10, null, issueTime())
+ assertEquals("ab
cdef
", target.toXml())
}
@Test
- fun `should merge and edit different levels with editByIndex`() {
- // TODO(7hong13): should be resolved after the JS SDK implementation
-// fun initializeTree() {
-// setUp()
-// target.editByIndex(0 to 0, CrdtTreeElement(issuePos(), "p").toList(), issueTime())
-// target.editByIndex(1 to 1, CrdtTreeElement(issuePos(), "b").toList(), issueTime())
-// target.editByIndex(2 to 2, CrdtTreeElement(issuePos(), "i").toList(), issueTime())
-// target.editByIndex(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.editByIndex(5 to 6, null, issueTime())
-// assertEquals("ab
", target.toXml())
-//
-// // 02. edit between two element nodes in same hierarchy.
-// initializeTree()
-// target.editByIndex(6 to 7, null, issueTime())
-// assertEquals("ab
", target.toXml())
-//
-// // 03. edit between text and element node in different hierarchy.
-// initializeTree()
-// target.editByIndex(4 to 6, null, issueTime())
-// assertEquals("a
", target.toXml())
-//
-// // 04. edit between text and element node in different hierarchy.
-// initializeTree()
-// target.editByIndex(5 to 7, null, issueTime())
-// assertEquals("ab
", target.toXml())
-//
-// // 05. edit between text and element node in different hierarchy.
-// initializeTree()
-// target.editByIndex(4 to 7, null, issueTime())
-// assertEquals("a
", target.toXml())
-//
-// // 06. edit between text and element node in different hierarchy.
-// initializeTree()
-// target.editByIndex(3 to 7, null, issueTime())
-// assertEquals("", target.toXml())
-//
-// // 07. edit between text and element node in same hierarchy.
-// setUp()
-// target.editByIndex(0 to 0, CrdtTreeElement(issuePos(), "p").toList(), issueTime())
-// target.editByIndex(1 to 1, CrdtTreeText(issuePos(), "ab").toList(), issueTime())
-// target.editByIndex(4 to 4, CrdtTreeElement(issuePos(), "p").toList(), issueTime())
-// target.editByIndex(5 to 5, CrdtTreeElement(issuePos(), "b").toList(), issueTime())
-// target.editByIndex(6 to 6, CrdtTreeText(issuePos(), "cd").toList(), issueTime())
-// target.editByIndex(10 to 10, CrdtTreeElement(issuePos(), "p").toList(), issueTime())
-// target.editByIndex(11 to 11, CrdtTreeText(issuePos(), "ef").toList(), issueTime())
-// assertEquals("ab
cd
ef
", target.toXml())
-//
-// target.editByIndex(9 to 10, null, issueTime())
-// assertEquals("ab
cdef
", target.toXml())
+ fun `should get correct index from CrdtTreePos`() {
+ // 0 1 2 3 4 5 6 7 8
+ // a b
+ target.editByIndex(0 to 0, CrdtTreeElement(issuePos(), "p").toList(), issueTime())
+ target.editByIndex(1 to 1, CrdtTreeElement(issuePos(), "b").toList(), issueTime())
+ target.editByIndex(2 to 2, CrdtTreeElement(issuePos(), "i").toList(), issueTime())
+ target.editByIndex(3 to 3, CrdtTreeText(issuePos(), "ab").toList(), issueTime())
+ assertEquals("ab
", target.toXml())
+
+ var (from, to) = target.pathToPosRange(listOf(0))
+ var fromIndex = target.toIndex(from)
+ var toIndex = target.toIndex(to)
+ assertEquals(7 to 8, fromIndex to toIndex)
+
+ target.pathToPosRange(listOf(0, 0)).also {
+ from = it.first
+ to = it.second
+ }
+ fromIndex = target.toIndex(from)
+ toIndex = target.toIndex(to)
+ assertEquals(6 to 7, fromIndex to toIndex)
+
+ target.pathToPosRange(listOf(0, 0, 0)).also {
+ from = it.first
+ to = it.second
+ }
+ fromIndex = target.toIndex(from)
+ toIndex = target.toIndex(to)
+ assertEquals(5 to 6, fromIndex to toIndex)
+
+ var range = target.indexRangeToPosRange(0 to 5)
+ assertEquals(0 to 5, target.rangeToIndex(range))
+
+ range = target.indexRangeToPosRange(5 to 7)
+ assertEquals(5 to 7, target.rangeToIndex(range))
}
- private fun issuePos(offset: Int = 0) = CrdtTreeNodeID(issueTime(), offset)
+ private fun issuePos(offset: Int = 0) = CrdtTreePos(issueTime(), offset)
private fun issueTime() = DummyContext.issueTimeTicket()
+ private fun CrdtTree.toList() = map { node ->
+ if (node.isText) {
+ "${node.type}.${node.value}"
+ } else {
+ node.type
+ }
+ }
+
private fun CrdtTreeNode.toList() = listOf(this)
companion object {
- private val DIP = CrdtTreeNodeID(TimeTicket.InitialTimeTicket, 0)
private val DummyContext = ChangeContext(
ChangeID.InitialChangeID,
diff --git a/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonTextTest.kt b/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonTextTest.kt
index ff6236d1e..b7ab24662 100644
--- a/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonTextTest.kt
+++ b/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonTextTest.kt
@@ -136,6 +136,13 @@ class JsonTextTest {
assertFalse(target.style(5, 7, emptyMap()))
}
+ @Test
+ fun `should return false when index range is invalid with select`() {
+ assertTrue(target.select(0, 0))
+ assertFalse(target.select(5, 0))
+ assertFalse(target.select(5, 7))
+ }
+
@Test
fun `should handle text clear operations`() {
target.edit(0, 0, "ABCD")
diff --git a/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonTreeTest.kt b/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonTreeTest.kt
index fef179425..a9bd5b7c1 100644
--- a/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonTreeTest.kt
+++ b/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonTreeTest.kt
@@ -9,7 +9,7 @@ import dev.yorkie.document.crdt.CrdtRoot
import dev.yorkie.document.crdt.CrdtTree
import dev.yorkie.document.crdt.CrdtTreeNode
import dev.yorkie.document.crdt.CrdtTreeNode.Companion.CrdtTreeElement
-import dev.yorkie.document.crdt.CrdtTreeNodeID
+import dev.yorkie.document.crdt.CrdtTreePos
import dev.yorkie.document.crdt.TreeNode
import dev.yorkie.document.json.TreeBuilder.element
import dev.yorkie.document.json.TreeBuilder.text
@@ -95,6 +95,36 @@ class JsonTreeTest {
target.toXml(),
)
assertEquals(18, target.size)
+ assertContentEquals(
+ listOf(
+ text { "ab" },
+ element("p") {
+ text { "ab" }
+ },
+ text { "cd" },
+ element("note") {
+ text { "cd" }
+ },
+ text { "ef" },
+ element("note") {
+ text { "ef" }
+ },
+ element("ng") {
+ element("note") {
+ text { "cd" }
+ }
+ element("note") {
+ text { "ef" }
+ }
+ },
+ text { "gh" },
+ element("bp") {
+ text { "gh" }
+ },
+ root,
+ ),
+ target.toList(),
+ )
}
@Test
@@ -225,19 +255,19 @@ class JsonTreeTest {
target.toXml(),
)
- target.style(1, 2, mapOf("c" to "d"))
+ target.style(4, 5, mapOf("c" to "d"))
assertEquals(
"""
""",
target.toXml(),
)
- target.style(1, 2, mapOf("c" to "q"))
+ target.style(4, 5, mapOf("c" to "q"))
assertEquals(
"""
""",
target.toXml(),
)
- target.style(2, 3, mapOf("z" to "m"))
+ target.style(3, 4, mapOf("z" to "m"))
assertEquals(
"""
""",
target.toXml(),
@@ -307,7 +337,7 @@ class JsonTreeTest {
root.tree().edit(1, 1, text { "X" })
assertEquals("Xab
", root.tree().toXml())
- root.tree().style(0, 1, mapOf("a" to "b"))
+ root.tree().style(4, 5, mapOf("a" to "b"))
}.await()
assertContentEquals(
listOf(
@@ -319,12 +349,11 @@ class JsonTreeTest {
listOf(TreeNode("text", value = "X")),
"$.t",
),
- // TODO(7hong13): need to check whether toPath is correctly passed
TreeStyleOpInfo(
- 0,
- 1,
+ 4,
+ 5,
+ listOf(0),
listOf(0),
- listOf(0, 0),
mapOf("a" to "b"),
"$.t",
),
@@ -386,12 +415,11 @@ class JsonTreeTest {
listOf(TreeNode("text", value = "X")),
"$.t",
),
- // TODO(7hong13): need to check whether toPath is correctly passed
TreeStyleOpInfo(
- 2,
- 3,
+ 6,
+ 7,
+ listOf(0, 0, 0),
listOf(0, 0, 0),
- listOf(0, 0, 0, 0),
mapOf("a" to "b"),
"$.t",
),
@@ -432,286 +460,6 @@ class JsonTreeTest {
assertEquals(5 to 7, tree.posRangeToIndexRange(posRange))
}
- @Test
- fun `should find pos range from path and vice versa`() = runTest {
- val document = Document(Document.Key(""))
- fun JsonObject.tree() = getAs("t")
-
- document.updateAsync { root, _ ->
- root.setNewTree(
- "t",
- element("root") {
- element("p") {
- element("b") {
- element("i") {
- text { "ab" }
- }
- }
- }
- },
- )
- }.await()
- assertEquals(
- """ab
""",
- document.getRoot().tree().toXml(),
- )
-
- val tree = document.getRoot().tree()
- var range = tree.pathRangeToPosRange(listOf(0) to listOf(0, 0, 0, 2))
- assertEquals(listOf(0) to listOf(0, 0, 0, 2), tree.posRangeToPathRange(range))
-
- range = tree.pathRangeToPosRange(listOf(0) to listOf(1))
- assertEquals(listOf(0) to listOf(1), tree.posRangeToPathRange(range))
- }
-
- @Test
- fun `should insert multiple text nodes`() = runTest {
- val document = Document(Document.Key(""))
- fun JsonObject.tree() = getAs("t")
-
- document.updateAsync { root, _ ->
- root.setNewTree(
- "t",
- element("doc") {
- element("p") {
- text { "ab" }
- }
- },
- )
- }.await()
- assertEquals("ab
", document.getRoot().tree().toXml())
-
- document.updateAsync { root, _ ->
- root.tree().edit(3, 3, text { "c" }, text { "d" })
- }.await()
- assertEquals("abcd
", document.getRoot().tree().toXml())
- }
-
- @Test
- fun `should insert multiple element nodes`() = runTest {
- val document = Document(Document.Key(""))
- fun JsonObject.tree() = getAs("t")
-
- document.updateAsync { root, _ ->
- root.setNewTree(
- "t",
- element("doc") {
- element("p") {
- text { "ab" }
- }
- },
- )
- }.await()
- assertEquals("ab
", document.getRoot().tree().toXml())
-
- document.updateAsync { root, _ ->
- root.tree().edit(
- 4,
- 4,
- element("p") { text { "cd" } },
- element("i") { text { "fg" } },
- )
- }.await()
- assertEquals("ab
cd
fg", document.getRoot().tree().toXml())
- }
-
- @Suppress("ktlint:standard:max-line-length")
- @Test
- fun `should edit content with path when multi tree nodes passed`() = runTest {
- val document = Document(Document.Key(""))
- fun JsonObject.tree() = getAs("t")
-
- document.updateAsync { root, _ ->
- root.setNewTree(
- "t",
- element("doc") {
- element("tc") {
- element("p") {
- element("tn") {
- text { "ab" }
- }
- }
- }
- },
- )
- assertEquals(
- "ab
",
- document.getRoot().tree().toXml(),
- )
-
- root.tree().editByPath(
- listOf(0, 0, 0, 1),
- listOf(0, 0, 0, 1),
- text { "X" },
- text { "X" },
- )
- assertEquals(
- "aXXb
",
- document.getRoot().tree().toXml(),
- )
-
- root.tree().editByPath(
- listOf(0, 1),
- listOf(0, 1),
- element("p") {
- element("tn") {
- text { "te" }
- text { "st" }
- }
- },
- element("p") {
- element("tn") {
- text { "te" }
- text { "xt" }
- }
- },
- )
- assertEquals(
- "aXXb
test
text
",
- document.getRoot().tree().toXml(),
- )
-
- root.tree().editByPath(
- listOf(0, 3),
- listOf(0, 3),
- element("p") {
- element("tn") {
- text { "te" }
- text { "st" }
- }
- },
- element("tn") {
- text { "te" }
- text { "xt" }
- },
- )
- assertEquals(
- "aXXb
test
text
test
text",
- document.getRoot().tree().toXml(),
- )
- }.await()
- }
-
- @Test
- fun `should delete the first text with tombstone in front of target text`() = runTest {
- val document = Document(Document.Key(""))
- fun JsonObject.tree() = getAs("t")
-
- document.updateAsync { root, _ ->
- root.setNewTree("t").edit(
- 0,
- 0,
- element("p") { text { "abcdefghi" } },
- )
- assertEquals("abcdefghi
", document.getRoot().tree().toXml())
-
- root.tree().edit(1, 1, text { "12345" })
- assertEquals("12345abcdefghi
", root.tree().toXml())
-
- root.tree().edit(2, 5)
- assertEquals("15abcdefghi
", root.tree().toXml())
-
- root.tree().edit(3, 5)
- assertEquals("15cdefghi
", root.tree().toXml())
-
- root.tree().edit(2, 4)
- assertEquals("1defghi
", root.tree().toXml())
-
- root.tree().edit(1, 3)
- assertEquals("efghi
", root.tree().toXml())
-
- root.tree().edit(1, 2)
- assertEquals("fghi
", root.tree().toXml())
-
- root.tree().edit(2, 5)
- assertEquals("f
", root.tree().toXml())
-
- root.tree().edit(1, 2)
- assertEquals("", root.tree().toXml())
- }.await()
- }
-
- @Test
- fun `should delete node with a text node in front whose size is bigger than 1`() = runTest {
- val document = Document(Document.Key(""))
- fun JsonObject.tree() = getAs("t")
-
- document.updateAsync { root, _ ->
- root.setNewTree("t").edit(
- 0,
- 0,
- element("p") { text { "abcde" } },
- )
- assertEquals("abcde
", document.getRoot().tree().toXml())
-
- root.tree().edit(6, 6, text { "f" })
- assertEquals("abcdef
", root.tree().toXml())
-
- root.tree().edit(7, 7, text { "g" })
- assertEquals("abcdefg
", root.tree().toXml())
-
- root.tree().edit(7, 8)
- assertEquals("<abcdef
", root.tree().toXml())
-
- root.tree().edit(6, 7)
- assertEquals("<abcde
", root.tree().toXml())
-
- root.tree().edit(5, 6)
- assertEquals("<abcd
", root.tree().toXml())
-
- root.tree().edit(4, 5)
- assertEquals("<abc
", root.tree().toXml())
-
- root.tree().edit(3, 4)
- assertEquals("<ab
", root.tree().toXml())
-
- root.tree().edit(2, 3)
- assertEquals("<a
", root.tree().toXml())
-
- root.tree().edit(1, 2)
- assertEquals("<", root.tree().toXml())
- }.await()
- }
-
- @Test
- fun `should delete nodes correctly in a multi-level range`() = runTest {
- val document = Document(Document.Key(""))
- fun JsonObject.tree() = getAs("t")
-
- document.updateAsync { root, _ ->
- root.setNewTree(
- "t",
- element("doc") {
- element("p") {
- text { "ab" }
- element("p") {
- text { "x" }
- }
- }
- element("p") {
- element("p") {
- text { "cd" }
- }
- }
- element("p") {
- element("p") {
- text { "y" }
- }
- text { "ef" }
- }
- },
- )
- assertEquals(
- "ab
x
cd
y
ef",
- document.getRoot().tree().toXml(),
- )
-
- root.tree().edit(2, 18)
- // TODO(7hong13): should be resolved after implementing Tree.move()
- // assertEquals("a
af
", root.tree().toXml())
- }.await()
- }
-
companion object {
private val DummyContext = ChangeContext(
ChangeID.InitialChangeID,
@@ -722,7 +470,7 @@ class JsonTreeTest {
get() = CrdtTree(rootCrdtTreeNode, InitialTimeTicket)
private val rootCrdtTreeNode: CrdtTreeNode
- get() = CrdtTreeElement(CrdtTreeNodeID(InitialTimeTicket, 0), DEFAULT_ROOT_TYPE)
+ get() = CrdtTreeElement(CrdtTreePos(InitialTimeTicket, 0), DEFAULT_ROOT_TYPE)
private fun createTreeWithStyle(): JsonTree {
val root = element("doc") {
diff --git a/yorkie/src/test/kotlin/dev/yorkie/util/IndexTreeTest.kt b/yorkie/src/test/kotlin/dev/yorkie/util/IndexTreeTest.kt
index a3b8936c5..8c60702a3 100644
--- a/yorkie/src/test/kotlin/dev/yorkie/util/IndexTreeTest.kt
+++ b/yorkie/src/test/kotlin/dev/yorkie/util/IndexTreeTest.kt
@@ -3,7 +3,7 @@ package dev.yorkie.util
import dev.yorkie.document.crdt.CrdtTreeNode
import dev.yorkie.document.crdt.CrdtTreeNode.Companion.CrdtTreeElement
import dev.yorkie.document.crdt.CrdtTreeNode.Companion.CrdtTreeText
-import dev.yorkie.document.crdt.CrdtTreeNodeID.Companion.InitialCrdtTreeNodeID
+import dev.yorkie.document.crdt.CrdtTreePos.Companion.InitialCrdtTreePos
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
@@ -48,6 +48,28 @@ class IndexTreeTest {
}
}
+ @Test
+ fun `should find right node from the given offset in postorder traversal`() {
+ // 0 1 2 3 4 5 6 7 8
+ // a b
c d
+ val tree = createIndexTree(
+ createElementNode(
+ "root",
+ createElementNode("p", createTextNode("ab")),
+ createElementNode("p", createTextNode("cd")),
+ ),
+ )
+
+ // postorder traversal: "ab", , "cd", ,
+ assertEquals("text", tree.findPostorderRight(tree.findTreePos(0))?.type)
+ assertEquals("text", tree.findPostorderRight(tree.findTreePos(1))?.type)
+ assertEquals("p", tree.findPostorderRight(tree.findTreePos(3))?.type)
+ assertEquals("text", tree.findPostorderRight(tree.findTreePos(4))?.type)
+ assertEquals("text", tree.findPostorderRight(tree.findTreePos(5))?.type)
+ assertEquals("p", tree.findPostorderRight(tree.findTreePos(7))?.type)
+ assertEquals("root", tree.findPostorderRight(tree.findTreePos(8))?.type)
+ }
+
@Test
fun `should find common ancestor of two given nodes`() {
val tree = createIndexTree(
@@ -82,16 +104,12 @@ class IndexTreeTest {
),
)
assertEquals(
- listOf("text.b:All", "p:Closing", "text.cde:All", "p:All", "text.fg:All", "p:Opening"),
+ listOf("text.b", "p", "text.cde", "p", "text.fg", "p"),
tree.nodesBetween(2, 11),
)
- assertEquals(
- listOf("text.b:All", "p:Closing", "text.cde:All", "p:Opening"),
- tree.nodesBetween(2, 6),
- )
- assertEquals(listOf("p:Opening"), tree.nodesBetween(0, 1))
- assertEquals(listOf("p:Closing"), tree.nodesBetween(3, 4))
- assertEquals(listOf("p:Closing", "p:Opening"), tree.nodesBetween(3, 5))
+ assertEquals(listOf("p"), tree.nodesBetween(0, 1))
+ assertEquals(listOf("p"), tree.nodesBetween(3, 4))
+ assertEquals(listOf("p", "p"), tree.nodesBetween(3, 5))
}
@Test
@@ -380,18 +398,18 @@ class IndexTreeTest {
}
private fun IndexTree.nodesBetween(from: Int, to: Int) = buildList {
- nodesBetween(from, to) { node, contain ->
- add("${node.toDiagnostic()}:${contain.name}")
+ nodesBetween(from, to) { node ->
+ add(node.toDiagnostic())
}
}
companion object {
private fun createElementNode(type: String, vararg childNode: CrdtTreeNode): CrdtTreeNode {
- return CrdtTreeElement(InitialCrdtTreeNodeID, type, childNode.toList())
+ return CrdtTreeElement(InitialCrdtTreePos, type, childNode.toList())
}
private fun createTextNode(value: String) =
- CrdtTreeText(InitialCrdtTreeNodeID, value)
+ CrdtTreeText(InitialCrdtTreePos, value)
private val DefaultRootNode = createElementNode(
"root",