Skip to content

Commit 36daeb6

Browse files
authored
Fix incorrect tree snapshot encoding/decoding (#198)
1 parent 586a003 commit 36daeb6

File tree

5 files changed

+75
-20
lines changed

5 files changed

+75
-20
lines changed

yorkie/src/main/kotlin/dev/yorkie/api/ElementConverter.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import dev.yorkie.document.crdt.TextValue
4848
import dev.yorkie.document.time.ActorID
4949
import dev.yorkie.document.time.TimeTicket.Companion.InitialTimeTicket
5050
import dev.yorkie.util.IndexTreeNode
51-
import dev.yorkie.util.traverse
51+
import dev.yorkie.util.traverseAll
5252

5353
internal typealias PBJsonElement = dev.yorkie.api.v1.JSONElement
5454
internal typealias PBJsonElementSimple = dev.yorkie.api.v1.JSONElementSimple
@@ -224,6 +224,7 @@ internal fun List<PBTreeNode>.toCrdtTreeRootNode(): CrdtTreeNode? {
224224
}.takeIf { it > -1 }?.plus(i + 1) ?: continue
225225
nodes[parentIndex].prepend(nodes[i])
226226
}
227+
root.updateDescendantSize()
227228
return CrdtTree(root, InitialTimeTicket).root
228229
}
229230

@@ -315,7 +316,7 @@ internal fun RgaTreeList.toPBRgaNodes(): List<PBRgaNode> {
315316
internal fun CrdtTreeNode.toPBTreeNodes(): List<PBTreeNode> {
316317
val treeNode = this
317318
return buildList {
318-
traverse(treeNode) { node, nodeDepth ->
319+
traverseAll(treeNode) { node, nodeDepth ->
319320
val pbTreeNode = treeNode {
320321
id = node.id.toPBTreeNodeID()
321322
type = node.type

yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtTree.kt

+7-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.yorkie.document.crdt
22

3+
import androidx.annotation.VisibleForTesting
34
import dev.yorkie.document.CrdtTreeNodeIDStruct
45
import dev.yorkie.document.CrdtTreePosStruct
56
import dev.yorkie.document.JsonSerializable
@@ -15,6 +16,7 @@ import dev.yorkie.util.IndexTreeNodeList
1516
import dev.yorkie.util.TokenType
1617
import dev.yorkie.util.TreePos
1718
import dev.yorkie.util.TreeToken
19+
import dev.yorkie.util.traverseAll
1820
import java.util.TreeMap
1921

2022
public typealias TreePosRange = Pair<CrdtTreePos, CrdtTreePos>
@@ -46,14 +48,18 @@ internal data class CrdtTree(
4648
get() = indexTree.root.toTreeNode()
4749

4850
init {
49-
indexTree.traverse { node, _ ->
51+
indexTree.traverseAll { node, _ ->
5052
nodeMapByID[node.id] = node
5153
}
5254
}
5355

5456
val size: Int
5557
get() = indexTree.size
5658

59+
@VisibleForTesting
60+
val nodeSize: Int
61+
get() = nodeMapByID.size
62+
5763
/**
5864
* Applies the given [attributes] of the given [range].
5965
*/
@@ -444,17 +450,6 @@ internal data class CrdtTree(
444450
return TreeOperationResult(changes, gcPairs)
445451
}
446452

447-
private fun traverseAll(
448-
node: CrdtTreeNode,
449-
depth: Int = 0,
450-
action: ((CrdtTreeNode, Int) -> Unit),
451-
) {
452-
node.allChildren.forEach { child ->
453-
traverseAll(child, depth + 1, action)
454-
}
455-
action.invoke(node, depth)
456-
}
457-
458453
private fun traverseInPosRange(
459454
fromParent: CrdtTreeNode,
460455
fromLeft: CrdtTreeNode,

yorkie/src/main/kotlin/dev/yorkie/util/IndexTree.kt

+24-6
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ internal class IndexTree<T : IndexTreeNode<T>>(val root: T) {
138138
traverse(root, 0, action)
139139
}
140140

141+
/**
142+
* Traverses the whole tree (include tombstones) with postorder traversal.
143+
*/
144+
fun traverseAll(action: ((T, Int) -> Unit)) {
145+
traverseAll(root, 0, action)
146+
}
147+
141148
/**
142149
* Finds the position of the given [index] in the tree.
143150
*/
@@ -370,7 +377,7 @@ internal data class TreePos<T : IndexTreeNode<T>>(
370377
* document of text-based editors.
371378
*/
372379
@Suppress("UNCHECKED_CAST")
373-
internal abstract class IndexTreeNode<T : IndexTreeNode<T>>() {
380+
internal abstract class IndexTreeNode<T : IndexTreeNode<T>> {
374381
abstract val type: String
375382

376383
protected abstract val childNodes: IndexTreeNodeList<T>
@@ -422,7 +429,8 @@ internal abstract class IndexTreeNode<T : IndexTreeNode<T>>() {
422429
var onRemovedListener: OnRemovedListener<T>? = null
423430

424431
/**
425-
* Updates the size of the ancestors.
432+
* Updates the size of the ancestors. It is used when
433+
* the size of the node is changed.
426434
*/
427435
fun updateAncestorSize() {
428436
var parent = parent
@@ -434,6 +442,20 @@ internal abstract class IndexTreeNode<T : IndexTreeNode<T>>() {
434442
}
435443
}
436444

445+
/**
446+
* Updates the size of the descendants. It is used when
447+
* the tree is newly created and the size of the descendants is not calculated.
448+
*/
449+
fun updateDescendantSize(): Int {
450+
if (isRemoved) {
451+
size = 0
452+
return 0
453+
}
454+
455+
size += children.sumOf { it.updateDescendantSize() }
456+
return paddedSize
457+
}
458+
437459
fun findOffset(node: T): Int {
438460
check(!isText) {
439461
"Text node cannot have children"
@@ -474,10 +496,6 @@ internal abstract class IndexTreeNode<T : IndexTreeNode<T>>() {
474496

475497
childNodes.add(0, node)
476498
node.parent = this as T
477-
478-
if (!node.isRemoved) {
479-
node.updateAncestorSize()
480-
}
481499
}
482500

483501
/**

yorkie/src/main/kotlin/dev/yorkie/util/IndexTreeExtensions.kt

+11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ internal fun <T : IndexTreeNode<T>> traverse(
1111
action.invoke(node, depth)
1212
}
1313

14+
internal fun <T : IndexTreeNode<T>> traverseAll(
15+
node: T,
16+
depth: Int = 0,
17+
action: ((T, Int) -> Unit),
18+
) {
19+
node.allChildren.forEach { child ->
20+
traverseAll(child, depth + 1, action)
21+
}
22+
action.invoke(node, depth)
23+
}
24+
1425
internal fun <T : IndexTreeNode<T>> findCommonAncestor(node1: T, node2: T): T? {
1526
if (node1 == node2) {
1627
return node1

yorkie/src/test/kotlin/dev/yorkie/api/ConverterTest.kt

+30
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.google.protobuf.kotlin.toByteStringUtf8
44
import dev.yorkie.api.v1.jSONElement
55
import dev.yorkie.api.v1.operation
66
import dev.yorkie.core.PresenceChange
7+
import dev.yorkie.document.Document
78
import dev.yorkie.document.change.Change
89
import dev.yorkie.document.change.ChangeID
910
import dev.yorkie.document.change.ChangePack
@@ -25,6 +26,9 @@ import dev.yorkie.document.crdt.RgaTreeList
2526
import dev.yorkie.document.crdt.RgaTreeSplit
2627
import dev.yorkie.document.crdt.RgaTreeSplitNodeID
2728
import dev.yorkie.document.crdt.RgaTreeSplitPos
29+
import dev.yorkie.document.json.JsonObject
30+
import dev.yorkie.document.json.JsonTree
31+
import dev.yorkie.document.json.TreeBuilder.element
2832
import dev.yorkie.document.operation.AddOperation
2933
import dev.yorkie.document.operation.EditOperation
3034
import dev.yorkie.document.operation.IncreaseOperation
@@ -44,6 +48,7 @@ import dev.yorkie.document.time.TimeTicket.Companion.MaxTimeTicket
4448
import dev.yorkie.util.IndexTreeNode.Companion.DEFAULT_ROOT_TYPE
4549
import java.util.Date
4650
import kotlin.test.assertEquals
51+
import kotlinx.coroutines.test.runTest
4752
import org.junit.Assert.assertThrows
4853
import org.junit.Test
4954

@@ -413,6 +418,31 @@ class ConverterTest {
413418
}
414419
}
415420

421+
@Test
422+
fun `should encode and decode tree properly`() = runTest {
423+
val document = Document(Document.Key(""))
424+
document.updateAsync { root, _ ->
425+
root.setNewTree(
426+
"t",
427+
element("r") {
428+
element("p") { text { "12" } }
429+
element("p") { text { "34" } }
430+
},
431+
).editByPath(listOf(0, 1), listOf(1, 1))
432+
}.await()
433+
434+
fun JsonObject.tree() = getAs<JsonTree>("t")
435+
436+
assertEquals("<r><p>14</p></r>", document.getRoot().tree().toXml())
437+
assertEquals(4, document.getRoot().tree().size)
438+
439+
val tree = document.getRoot().target["t"]
440+
val bytes = tree.toByteString()
441+
val obj = bytes.toCrdtTree()
442+
assertEquals(document.getRoot().tree().target.nodeSize, obj.nodeSize)
443+
assertEquals(document.getRoot().tree().size, obj.size)
444+
}
445+
416446
private class TestOperation(
417447
override val parentCreatedAt: TimeTicket,
418448
override var executedAt: TimeTicket,

0 commit comments

Comments
 (0)