diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9123b65dc..65efcde98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,8 +62,7 @@ jobs: - run: chmod +x gradlew - uses: gradle/gradle-build-action@v2 - name: Setup Docker on macOS - uses: 7hong13/setup-docker-macos-action@fix_formula_paths - id: docker + uses: douglascamata/setup-docker-macos-action@fix-python-dep - run: docker-compose -f docker/docker-compose-ci.yml up --build -d - uses: actions/cache@v3 id: avd-cache diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index 5313b0289..2ebf0ada3 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.7' container_name: 'yorkie' command: [ 'server', diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 7f4b860fd..18bc6b1b9 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.7' 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..bd574779f 100644 --- a/examples/texteditor/src/main/java/com/example/texteditor/EditorViewModel.kt +++ b/examples/texteditor/src/main/java/com/example/texteditor/EditorViewModel.kt @@ -15,6 +15,7 @@ import dev.yorkie.document.time.ActorID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.filterIsInstance @@ -30,16 +31,18 @@ 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 _editOpInfos = MutableSharedFlow() + val editOpInfos = _editOpInfos.asSharedFlow() + + private val _selections = MutableSharedFlow() + val selections = _selections.asSharedFlow() val removedPeers = document.events.filterIsInstance() - .map { it.unwatched.actorID } + .map { it.changed.actorID } - private val _peerSelectionInfos = mutableMapOf() - val peerSelectionInfos: Map - get() = _peerSelectionInfos + private val _selectionColors = mutableMapOf() + val selectionColors: Map + get() = _selectionColors private val gson = Gson() @@ -63,7 +66,7 @@ class EditorViewModel(private val client: Client) : ViewModel(), YorkieEditText. is Document.Event.RemoteChange -> emitEditOpInfos(event.changeInfo) - is PresenceChange.Others.PresenceChanged -> event.changed.emitSelectOpInfo() + is PresenceChange.Others.PresenceChanged -> event.changed.emitSelection() else -> {} } @@ -71,24 +74,6 @@ 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) - } - } - - private suspend fun PresenceInfo.emitSelectOpInfo() { - val jsonArray = JSONArray(presence["selection"] ?: return) - val fromPos = - gson.fromJson(jsonArray.getString(0), TextPosStructure::class.java) ?: return - val toPos = - 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)) - } - fun syncText() { viewModelScope.launch { val content = document.getRoot().getAsOrNull(TEXT_KEY) @@ -96,6 +81,25 @@ class EditorViewModel(private val client: Client) : ViewModel(), YorkieEditText. } } + private suspend fun emitEditOpInfos(changeInfo: Document.Event.ChangeInfo) { + changeInfo.operations.filterIsInstance() + .forEach { _editOpInfos.emit(it) } + } + + private suspend fun PresenceInfo.emitSelection() { + runCatching { + delay(500) + val jsonArray = JSONArray(presence["selection"] ?: return) + val fromPos = + gson.fromJson(jsonArray.getString(0), TextPosStructure::class.java) ?: return + val toPos = + gson.fromJson(jsonArray.getString(1), TextPosStructure::class.java) ?: return + val (from, to) = document.getRoot().getAs(TEXT_KEY) + .posRangeToIndexRange(fromPos to toPos) + _selections.emit(Selection(actorID, from, to)) + } + } + override fun handleEditEvent(from: Int, to: Int, content: CharSequence) { viewModelScope.launch { document.updateAsync { root, _ -> @@ -119,21 +123,13 @@ class EditorViewModel(private val client: Client) : ViewModel(), YorkieEditText. @ColorInt fun getPeerSelectionColor(actorID: ActorID): Int { - return _peerSelectionInfos[actorID]?.color ?: run { - val newColor = - Color.argb(51, Random.nextInt(256), Random.nextInt(256), Random.nextInt(256)) - _peerSelectionInfos[actorID] = PeerSelectionInfo(newColor) - newColor + return _selectionColors.getOrPut(actorID) { + Color.argb(51, Random.nextInt(256), Random.nextInt(256), Random.nextInt(256)) } } - fun updatePeerPrevSelection(actorID: ActorID, prevSelection: Pair?) { - val peerSelectionInfo = _peerSelectionInfos[actorID] ?: return - _peerSelectionInfos[actorID] = peerSelectionInfo.copy(prevSelection = prevSelection) - } - - fun removeDetachedPeerSelectionInfo(actorID: ActorID) { - _peerSelectionInfos.remove(actorID) + fun removeUnwatchedPeerSelectionInfo(actorID: ActorID) { + _selectionColors.remove(actorID) } override fun handleHangulCompositionStart() { @@ -152,10 +148,7 @@ class EditorViewModel(private val client: Client) : ViewModel(), YorkieEditText. super.onCleared() } - data class PeerSelectionInfo( - @ColorInt val color: Int, - val prevSelection: Pair? = null, - ) + data class Selection(val clientID: ActorID, val from: Int, val to: Int) companion object { private const val DOCUMENT_KEY = "document-key" diff --git a/examples/texteditor/src/main/java/com/example/texteditor/MainActivity.kt b/examples/texteditor/src/main/java/com/example/texteditor/MainActivity.kt index 599302152..bef7e569c 100644 --- a/examples/texteditor/src/main/java/com/example/texteditor/MainActivity.kt +++ b/examples/texteditor/src/main/java/com/example/texteditor/MainActivity.kt @@ -11,6 +11,7 @@ import androidx.core.text.getSpans import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory +import com.example.texteditor.EditorViewModel.Selection import com.example.texteditor.databinding.ActivityMainBinding import dev.yorkie.core.Client import dev.yorkie.document.operation.OperationInfo @@ -51,18 +52,21 @@ class MainActivity : AppCompatActivity() { } launch { - viewModel.textOpInfos.collect { (actor, opInfo) -> - when (opInfo) { - is OperationInfo.EditOpInfo -> opInfo.handleContentChange() - is OperationInfo.SelectOpInfo -> opInfo.handleSelectChange(actor) - } + viewModel.editOpInfos.collect { opInfo -> + opInfo.handleContentChange() } } launch { viewModel.removedPeers.collect { binding.textEditor.text?.removePrevSpan(it) - viewModel.removeDetachedPeerSelectionInfo(it) + viewModel.removeUnwatchedPeerSelectionInfo(it) + } + } + + launch { + viewModel.selections.collect { selection -> + selection.handleSelectChange() } } } @@ -82,26 +86,21 @@ class MainActivity : AppCompatActivity() { } } - private fun OperationInfo.SelectOpInfo.handleSelectChange(actor: ActorID) { + private fun Selection.handleSelectChange() { val editable = binding.textEditor.text ?: return - if (editable.removePrevSpan(actor) && from == to) { - viewModel.updatePeerPrevSelection(actor, null) - } else { - editable.setSpan( - BackgroundColorSpan(viewModel.getPeerSelectionColor(actor)), - from.coerceAtMost(to), - to.coerceAtLeast(from), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, - ) - viewModel.updatePeerPrevSelection(actor, from to to) - } + editable.removePrevSpan(clientID) + editable.setSpan( + BackgroundColorSpan(viewModel.getPeerSelectionColor(clientID)), + from.coerceAtMost(to), + to.coerceAtLeast(from), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, + ) } private fun Editable.removePrevSpan(actorID: ActorID): Boolean { - val (start, end) = viewModel.peerSelectionInfos[actorID]?.prevSelection ?: return false - val backgroundSpan = getSpans(start, end).firstOrNull { - it.backgroundColor == viewModel.peerSelectionInfos[actorID]?.color + val backgroundSpan = getSpans(0, length).firstOrNull { + it.backgroundColor == viewModel.selectionColors[actorID] } backgroundSpan?.let(::removeSpan) return true diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt index f245d4c03..99a97fb4d 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt @@ -180,7 +180,9 @@ class ClientTest { document1.updateAsync { root, _ -> root["version"] = "v1" }.await() - assertNotEquals(document1.toJson(), document2.toJson()) + assertJsonContentEquals("""{"version":"v1"}""", document1.toJson()) + assertJsonContentEquals("""{}""", document2.toJson()) + client1.syncAsync().await() client2.syncAsync().await() assertEquals(document1.toJson(), document2.toJson()) @@ -190,11 +192,14 @@ class ClientTest { val collectJob = launch(start = CoroutineStart.UNDISPATCHED) { client2.events.collect(client2Events::add) } + client2.resume(document2) + delay(30) document1.updateAsync { root, _ -> root["version"] = "v2" }.await() client1.syncAsync().await() + withTimeout(GENERAL_TIMEOUT) { while (client2Events.size < 2) { delay(50) @@ -210,6 +215,7 @@ class ClientTest { root["version"] = "v3" }.await() assertNotEquals(document1.toJson(), document2.toJson()) + client1.syncAsync().await() client2.syncAsync().await() assertEquals(document1.toJson(), document2.toJson()) diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/DocumentTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/DocumentTest.kt index 74b605130..36cebb4d6 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/DocumentTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/DocumentTest.kt @@ -31,7 +31,6 @@ import org.junit.runner.RunWith import java.util.UUID import kotlin.test.assertEquals import kotlin.test.assertFailsWith -import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) class DocumentTest { @@ -167,8 +166,10 @@ class DocumentTest { client2.syncAsync().await() assertEquals(document1.toJson(), document2.toJson()) - assertTrue(client1.removeAsync(document1).await()) - assertTrue(client2.removeAsync(document2).await()) + client1.removeAsync(document1).await() + if (document2.status == DocumentStatus.Attached) { + client2.removeAsync(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..1c99b21e0 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout @@ -235,7 +236,6 @@ class PresenceTest { .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() @@ -243,7 +243,7 @@ class PresenceTest { .collect(d2Events::add) } - withTimeout(GENERAL_TIMEOUT + 1) { + withTimeout(GENERAL_TIMEOUT) { while (d1Events.isEmpty()) { delay(50) } @@ -260,7 +260,7 @@ class PresenceTest { presence.put(mapOf("name" to "X")) }.await() - withTimeout(GENERAL_TIMEOUT + 2) { + withTimeout(GENERAL_TIMEOUT) { while (d1Events.size < 2 || d2Events.isEmpty()) { delay(50) } @@ -372,15 +372,14 @@ class PresenceTest { c1.attachAsync(d1, initialPresence = mapOf("name" to "a1", "cursor" to cursor)).await() val d1CollectJob = launch(start = CoroutineStart.UNDISPATCHED) { - d1.events.filterIsInstance() - .collect(d1Events::add) + d1.events.filterIsInstance().collect(d1Events::add) } // 01. c2 attaches doc in realtime sync, and c3 attached doc in manual sync. 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) { + withTimeout(GENERAL_TIMEOUT + 1) { // c2 watched while (d1Events.isEmpty()) { delay(50) @@ -397,7 +396,7 @@ class PresenceTest { // 02. c2 pauses the document (in manual sync), c3 resumes the document (in realtime sync). c2.pause(d2) - withTimeout(GENERAL_TIMEOUT) { + withTimeout(GENERAL_TIMEOUT + 2) { // c2 unwatched while (d1Events.size < 2) { delay(50) @@ -407,7 +406,7 @@ class PresenceTest { assertIs(d1Events.last()) c3.resume(d3) - withTimeout(GENERAL_TIMEOUT) { + withTimeout(GENERAL_TIMEOUT + 3) { // c3 watched while (d1Events.size < 3) { delay(50) @@ -415,6 +414,9 @@ class PresenceTest { } assertIs(d1Events.last()) + withTimeout(GENERAL_TIMEOUT) { + d3.presences.first { c3ID in d3.presences.value } + } assertEquals( mapOf(c1ID to d1.presences.value[c1ID], c3ID to d3.presences.value[c3ID]), d1.presences.value.toMap(), @@ -455,7 +457,7 @@ class PresenceTest { .await() val d1CollectJob = launch(start = CoroutineStart.UNDISPATCHED) { - d1.events.filterIsInstance() + d1.events.filterIsInstance() .collect(d1Events::add) } @@ -529,6 +531,7 @@ class PresenceTest { // from c3, only the watched event is triggered. c3.syncAsync().await() c1.syncAsync().await() + delay(50) c3.resume(d3) withTimeout(GENERAL_TIMEOUT) { @@ -591,10 +594,11 @@ class PresenceTest { // 05-2. c2 resumes the document, c1 receives a watched event from c2. c2.syncAsync().await() c1.syncAsync().await() + delay(50) c2.resume(d2) withTimeout(GENERAL_TIMEOUT) { - // c3 unwatched + // c2 watched while (d1Events.size < 7) { delay(50) } diff --git a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt index e3c66f07c..fc45c36e5 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/core/Client.kt @@ -277,10 +277,10 @@ public class Client @VisibleForTesting internal constructor( 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()), - ) + document.onlineClients.value += clientIDs + + val presences = document.presences.first { it.keys.containsAll(clientIDs) } + document.publish(PresenceChange.MyPresence.Initialized(presences.asPresences())) return } @@ -295,7 +295,7 @@ public class Client @VisibleForTesting internal constructor( 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 + document.onlineClients.value += publisher if (publisher in document.allPresences.value) { val presence = document.presences.first { publisher in it }[publisher] ?: return document.publish( @@ -309,7 +309,7 @@ public class Client @VisibleForTesting internal constructor( // 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.onlineClients.value -= publisher document.publish(PresenceChange.Others.Unwatched(PresenceInfo(publisher, presence))) } diff --git a/yorkie/src/main/kotlin/dev/yorkie/core/Presences.kt b/yorkie/src/main/kotlin/dev/yorkie/core/Presences.kt index ec156a386..d8384fc63 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/core/Presences.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/core/Presences.kt @@ -33,7 +33,6 @@ public class Presences private constructor( 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..529048de2 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt @@ -288,7 +288,7 @@ public class Document(public val key: Key) { presences.value[actorID]?.let { presence -> Others.Unwatched(PresenceInfo(actorID, presence)) }.also { - onlineClients.value = onlineClients.value - actorID + onlineClients.value -= actorID } } } @@ -363,11 +363,11 @@ public class Document(public val key: Key) { internal suspend fun publish(event: Event) { when (event) { is Others.Watched -> { - presences.first { event.watched.actorID in it.keys } + presences.first { event.changed.actorID in it.keys } } is Others.Unwatched -> { - presences.first { event.unwatched.actorID !in it.keys } + presences.first { event.changed.actorID !in it.keys } } is MyPresence.Initialized -> { @@ -380,12 +380,7 @@ public class Document(public val key: Key) { } private fun Change.toChangeInfo(operationInfos: List) = - Event.ChangeInfo(message.orEmpty(), operationInfos.map { it.updatePath() }, id.actor) - - private fun OperationInfo.updatePath(): OperationInfo { - val path = root.createSubPaths(executedAt).joinToString(".") - return apply { this.path = path } - } + Event.ChangeInfo(message.orEmpty(), operationInfos, id.actor) public fun toJson(): String { return root.toJson() @@ -428,22 +423,23 @@ public class Document(public val key: Key) { } public sealed interface Others : PresenceChange { + public val changed: PresenceInfo /** * Means that the client has established a connection with the server, * enabling real-time synchronization. */ - public data class Watched(public val watched: PresenceInfo) : Others + public data class Watched(override val changed: PresenceInfo) : Others /** * Means that the client has been disconnected. */ - public data class Unwatched(public val unwatched: PresenceInfo) : Others + public data class Unwatched(override val changed: PresenceInfo) : Others /** * Means that the presences of the client has been updated. */ - public data class PresenceChanged(public val changed: PresenceInfo) : Others + public data class PresenceChanged(override val changed: PresenceInfo) : Others } } 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..de74094b1 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtTree.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtTree.kt @@ -30,6 +30,9 @@ internal class CrdtTree( private val removedNodeMap = mutableMapOf, CrdtTreeNode>() + val rootTreeNode: TreeNode + get() = indexTree.root.toTreeNode() + init { indexTree.traverse { node, _ -> nodeMapByID[node.id] = node @@ -55,7 +58,7 @@ internal class CrdtTree( // TODO(7hong13): check whether toPath is set correctly val changes = listOf( TreeChange( - type = TreeChangeType.Style.type, + type = TreeChangeType.Style, from = toIndex(fromParent, fromLeft), to = toIndex(toParent, toLeft), fromPath = toPath(fromParent, fromLeft), @@ -136,13 +139,13 @@ internal class CrdtTree( // range(from, to) into multiple ranges. val changes = listOf( TreeChange( - type = TreeChangeType.Content.type, + type = TreeChangeType.Content, from = toIndex(fromParent, fromLeft), to = toIndex(toParent, toLeft), fromPath = toPath(fromParent, fromLeft), toPath = toPath(toParent, toLeft), actorID = executedAt.actorID, - value = contents?.map(CrdtTreeNode::toJson), + value = contents?.map(CrdtTreeNode::toTreeNode), ), ) diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtTreeExtensions.kt b/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtTreeExtensions.kt index 8d06b6870..2d415e56e 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtTreeExtensions.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtTreeExtensions.kt @@ -16,10 +16,10 @@ internal fun CrdtTreeNode.toXml(): String { /** * Converts the given node to JSON. */ -internal fun CrdtTreeNode.toJson(): TreeNode { +internal fun CrdtTreeNode.toTreeNode(): TreeNode { return if (isText) { TreeNode(type, value = value) } else { - TreeNode(type, children.map { it.toJson() }, attributes = attributes) + TreeNode(type, children.map { it.toTreeNode() }, attributes = attributes) } } diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/crdt/TreeInfo.kt b/yorkie/src/main/kotlin/dev/yorkie/document/crdt/TreeInfo.kt index 31fe0a3a4..8d4ffc216 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/crdt/TreeInfo.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/crdt/TreeInfo.kt @@ -1,5 +1,6 @@ package dev.yorkie.document.crdt +import dev.yorkie.document.json.JsonTree import dev.yorkie.document.time.ActorID import dev.yorkie.util.IndexTreeNode.Companion.DEFAULT_TEXT_TYPE @@ -7,13 +8,25 @@ import dev.yorkie.util.IndexTreeNode.Companion.DEFAULT_TEXT_TYPE * [TreeNode] represents the JSON representation of a node in the tree. * It is used to serialize and deserialize the tree. */ -public data class TreeNode( +internal data class TreeNode( val type: String, val children: List? = null, val value: String? = null, val attributes: Map? = null, ) { + fun toJsonTreeNode(): JsonTree.TreeNode { + return if (type == DEFAULT_TEXT_TYPE) { + JsonTree.TextNode(value.orEmpty()) + } else { + JsonTree.ElementNode( + type, + attributes.orEmpty(), + children?.map(TreeNode::toJsonTreeNode).orEmpty(), + ) + } + } + override fun toString(): String { return if (type == DEFAULT_TEXT_TYPE) { """{"type":"$type","value":"$value"}""" @@ -41,7 +54,7 @@ public data class TreeNode( internal data class TreeChange( val actorID: ActorID, - val type: String, + val type: TreeChangeType, val from: Int, val to: Int, val fromPath: List, @@ -50,6 +63,6 @@ internal data class TreeChange( val attributes: Map? = null, ) -internal enum class TreeChangeType(val type: String) { - Content("content"), Style("style") +internal enum class TreeChangeType { + Content, Style } diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonStringifier.kt b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonStringifier.kt index c22330f61..821a1f4d9 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonStringifier.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonStringifier.kt @@ -8,7 +8,6 @@ import dev.yorkie.document.crdt.CrdtObject import dev.yorkie.document.crdt.CrdtPrimitive import dev.yorkie.document.crdt.CrdtText import dev.yorkie.document.crdt.CrdtTree -import dev.yorkie.document.crdt.toJson import java.util.Date internal object JsonStringifier { @@ -69,7 +68,7 @@ internal object JsonStringifier { } is CrdtTree -> { - buffer.append(root.toJson()) + buffer.append(rootTreeNode) } } } 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..4cee716d2 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt @@ -29,6 +29,9 @@ public class JsonTree internal constructor( internal val indexTree by target::indexTree + public val rootTreeNode: TreeNode + get() = target.rootTreeNode.toJsonTreeNode() + /** * Sets the [attributes] to the elements of the given [path]. */ diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/operation/AddOperation.kt b/yorkie/src/main/kotlin/dev/yorkie/document/operation/AddOperation.kt index cc33a40f8..585ec5eda 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/operation/AddOperation.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/operation/AddOperation.kt @@ -32,9 +32,10 @@ internal data class AddOperation( parentObject.insertAfter(prevCreatedAt, copiedValue) root.registerElement(copiedValue, parentObject) listOf( - OperationInfo.AddOpInfo(parentObject.subPathOf(effectedCreatedAt).toInt()).apply { - executedAt = parentCreatedAt - }, + OperationInfo.AddOpInfo( + parentObject.subPathOf(effectedCreatedAt).toInt(), + root.createPath(parentCreatedAt), + ), ) } else { parentObject ?: YorkieLogger.e(TAG, "fail to find $parentCreatedAt") diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/operation/EditOperation.kt b/yorkie/src/main/kotlin/dev/yorkie/document/operation/EditOperation.kt index 6887261e8..58331cb54 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/operation/EditOperation.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/operation/EditOperation.kt @@ -4,7 +4,6 @@ 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.crdt.TextChangeType import dev.yorkie.document.crdt.TextWithAttributes import dev.yorkie.document.time.ActorID import dev.yorkie.document.time.TimeTicket @@ -42,18 +41,13 @@ internal data class EditOperation( if (fromPos != toPos) { root.registerElementHasRemovedNodes(parentObject) } - changes.map { (type, _, from, to, content, attributes) -> - if (type == TextChangeType.Content) { - OperationInfo.EditOpInfo( - from, - to, - TextWithAttributes(content.orEmpty() to attributes.orEmpty()), - ) - } else { - OperationInfo.SelectOpInfo(from, to) - }.apply { - executedAt = parentCreatedAt - } + changes.map { (_, _, from, to, content, attributes) -> + OperationInfo.EditOpInfo( + from, + to, + TextWithAttributes(content.orEmpty() to attributes.orEmpty()), + root.createPath(parentCreatedAt), + ) } } else { if (parentObject == null) { diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/operation/IncreaseOperation.kt b/yorkie/src/main/kotlin/dev/yorkie/document/operation/IncreaseOperation.kt index da4e208f1..9b9196eb9 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/operation/IncreaseOperation.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/operation/IncreaseOperation.kt @@ -37,11 +37,7 @@ internal data class IncreaseOperation( } else { copiedValue.value as Long } - listOf( - OperationInfo.IncreaseOpInfo(increasedValue).apply { - executedAt = effectedCreatedAt - }, - ) + listOf(OperationInfo.IncreaseOpInfo(increasedValue, root.createPath(parentCreatedAt))) } else { parentObject ?: YorkieLogger.e(TAG, "fail to find $parentCreatedAt") YorkieLogger.e(TAG, "fail to execute, only Counter can execute increase") diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/operation/MoveOperation.kt b/yorkie/src/main/kotlin/dev/yorkie/document/operation/MoveOperation.kt index a061f066b..e3445f479 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/operation/MoveOperation.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/operation/MoveOperation.kt @@ -31,9 +31,11 @@ internal data class MoveOperation( parentObject.moveAfter(prevCreatedAt, createdAt, executedAt) val index = parentObject.subPathOf(createdAt).toInt() listOf( - OperationInfo.MoveOpInfo(previousIndex = previousIndex, index = index).apply { - executedAt = parentCreatedAt - }, + OperationInfo.MoveOpInfo( + previousIndex = previousIndex, + index = index, + path = root.createPath(parentCreatedAt), + ), ) } else { parentObject ?: YorkieLogger.e(TAG, "fail to find $parentCreatedAt") 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..b1bd07536 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/operation/OperationInfo.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/operation/OperationInfo.kt @@ -1,7 +1,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 @@ -79,18 +78,12 @@ public sealed class OperationInfo { override var path: String = INITIAL_PATH, ) : OperationInfo(), TextOperationInfo - public data class SelectOpInfo( - val from: Int, - val to: Int, - override var path: String = INITIAL_PATH, - ) : OperationInfo(), TextOperationInfo - public data class TreeEditOpInfo( val from: Int, val to: Int, val fromPath: List, val toPath: List, - val nodes: List?, + val nodes: List?, override var path: String = INITIAL_PATH, ) : OperationInfo(), TreeOperationInfo diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/operation/RemoveOperation.kt b/yorkie/src/main/kotlin/dev/yorkie/document/operation/RemoveOperation.kt index e74662678..d32660a7a 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/operation/RemoveOperation.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/operation/RemoveOperation.kt @@ -31,11 +31,7 @@ internal data class RemoveOperation( val element = parentObject.remove(createdAt, executedAt) root.registerRemovedElement(element) val index = if (parentObject is CrdtArray) key?.toInt() else null - listOf( - OperationInfo.RemoveOpInfo(key, index).apply { - executedAt = effectedCreatedAt - }, - ) + listOf(OperationInfo.RemoveOpInfo(key, index, root.createPath(parentCreatedAt))) } else { parentObject ?: YorkieLogger.e(TAG, "fail to find $parentCreatedAt") YorkieLogger.e(TAG, "only object and array can execute remove: $parentObject") diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/operation/SetOperation.kt b/yorkie/src/main/kotlin/dev/yorkie/document/operation/SetOperation.kt index f3e289f6b..70f962519 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/operation/SetOperation.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/operation/SetOperation.kt @@ -32,11 +32,7 @@ internal data class SetOperation( val copiedValue = value.deepCopy() parentObject[key] = copiedValue root.registerElement(copiedValue, parentObject) - listOf( - OperationInfo.SetOpInfo(key).apply { - executedAt = parentCreatedAt - }, - ) + listOf(OperationInfo.SetOpInfo(key, root.createPath(parentCreatedAt))) } else { parentObject ?: YorkieLogger.e(TAG, "fail to find $parentCreatedAt") YorkieLogger.e(TAG, "fail to execute, only object can execute set") diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/operation/StyleOperation.kt b/yorkie/src/main/kotlin/dev/yorkie/document/operation/StyleOperation.kt index 8ff24d50a..766471a73 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/operation/StyleOperation.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/operation/StyleOperation.kt @@ -28,9 +28,8 @@ internal data class StyleOperation( it.from, it.to, it.attributes.orEmpty(), - ).apply { - executedAt = parentCreatedAt - } + root.createPath(parentCreatedAt), + ) } } else { parentObject ?: YorkieLogger.e(TAG, "fail to find $parentCreatedAt") 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..fd877f382 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/operation/TreeEditOperation.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/operation/TreeEditOperation.kt @@ -4,6 +4,7 @@ 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.crdt.TreeNode import dev.yorkie.document.time.ActorID import dev.yorkie.document.time.TimeTicket import dev.yorkie.util.YorkieLogger @@ -49,10 +50,9 @@ internal data class TreeEditOperation( it.to, it.fromPath, it.toPath, - it.value, - ).apply { - executedAt = parentCreatedAt - } + it.value?.map(TreeNode::toJsonTreeNode), + root.createPath(parentCreatedAt), + ) } } diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/operation/TreeStyleOperation.kt b/yorkie/src/main/kotlin/dev/yorkie/document/operation/TreeStyleOperation.kt index 51fcfb303..76eed880c 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/operation/TreeStyleOperation.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/operation/TreeStyleOperation.kt @@ -38,9 +38,8 @@ internal data class TreeStyleOperation( it.fromPath, it.toPath, it.attributes.orEmpty(), - ).apply { - executedAt = parentCreatedAt - } + root.createPath(parentCreatedAt), + ) } } 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..9496bb279 100644 --- a/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonTreeTest.kt +++ b/yorkie/src/test/kotlin/dev/yorkie/document/json/JsonTreeTest.kt @@ -10,7 +10,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.CrdtTreeNodeID -import dev.yorkie.document.crdt.TreeNode import dev.yorkie.document.json.TreeBuilder.element import dev.yorkie.document.json.TreeBuilder.text import dev.yorkie.document.operation.OperationInfo.TreeEditOpInfo @@ -316,7 +315,7 @@ class JsonTreeTest { 1, listOf(0, 0), listOf(0, 0), - listOf(TreeNode("text", value = "X")), + listOf(JsonTree.TextNode("X")), "$.t", ), // TODO(7hong13): need to check whether toPath is correctly passed @@ -383,7 +382,7 @@ class JsonTreeTest { 4, listOf(0, 0, 0, 1), listOf(0, 0, 0, 1), - listOf(TreeNode("text", value = "X")), + listOf(JsonTree.TextNode("X")), "$.t", ), // TODO(7hong13): need to check whether toPath is correctly passed