diff --git a/api/converter/from_pb.go b/api/converter/from_pb.go
index 7eae3ea3e..6cc53b6fe 100644
--- a/api/converter/from_pb.go
+++ b/api/converter/from_pb.go
@@ -658,10 +658,11 @@ func fromTreeNode(pbNode *api.TreeNode) (*crdt.TreeNode, error) {
}
}
- node.RemovedAt, err = fromTimeTicket(pbNode.RemovedAt)
+ removedAt, err := fromTimeTicket(pbNode.RemovedAt)
if err != nil {
return nil, err
}
+ node.SetRemovedAt(removedAt)
return node, nil
}
diff --git a/api/converter/to_bytes.go b/api/converter/to_bytes.go
index c8782afa0..e8abc381a 100644
--- a/api/converter/to_bytes.go
+++ b/api/converter/to_bytes.go
@@ -310,10 +310,10 @@ func toTreeNode(treeNode *crdt.TreeNode, depth int) *api.TreeNode {
}
pbNode := &api.TreeNode{
- Id: toTreeNodeID(treeNode.ID),
+ Id: toTreeNodeID(treeNode.ID()),
Type: treeNode.Type(),
Value: treeNode.Value,
- RemovedAt: ToTimeTicket(treeNode.RemovedAt),
+ RemovedAt: ToTimeTicket(treeNode.RemovedAt()),
Depth: int32(depth),
Attributes: attrs,
}
diff --git a/pkg/document/change/context.go b/pkg/document/change/context.go
index fca9589ea..90464c2b5 100644
--- a/pkg/document/change/context.go
+++ b/pkg/document/change/context.go
@@ -80,11 +80,6 @@ func (c *Context) RegisterRemovedElementPair(parent crdt.Container, deleted crdt
c.root.RegisterRemovedElementPair(parent, deleted)
}
-// RegisterElementHasRemovedNodes register the given text element with garbage to hash table.
-func (c *Context) RegisterElementHasRemovedNodes(element crdt.GCElement) {
- c.root.RegisterElementHasRemovedNodes(element)
-}
-
// RegisterGCPair registers the given GC pair to the root.
func (c *Context) RegisterGCPair(pair crdt.GCPair) {
c.root.RegisterGCPair(pair)
diff --git a/pkg/document/crdt/element.go b/pkg/document/crdt/element.go
index 5330ced41..2230b62cf 100644
--- a/pkg/document/crdt/element.go
+++ b/pkg/document/crdt/element.go
@@ -39,13 +39,6 @@ type Container interface {
DeleteByCreatedAt(createdAt *time.Ticket, deletedAt *time.Ticket) (Element, error)
}
-// GCElement represents Element which has GC.
-type GCElement interface {
- Element
- removedNodesLen() int
- purgeRemovedNodesBefore(ticket *time.Ticket) (int, error)
-}
-
// Element represents JSON element.
type Element interface {
// Marshal returns the JSON encoding of this element.
diff --git a/pkg/document/crdt/gc.go b/pkg/document/crdt/gc.go
index ba39fba7a..142eeb21b 100644
--- a/pkg/document/crdt/gc.go
+++ b/pkg/document/crdt/gc.go
@@ -32,6 +32,6 @@ type GCParent interface {
// GCChild is an interface for the child of the garbage collection target.
type GCChild interface {
- ID() string
+ IDString() string
RemovedAt() *time.Ticket
}
diff --git a/pkg/document/crdt/rga_tree_split.go b/pkg/document/crdt/rga_tree_split.go
index 70172e0e1..98a686ef6 100644
--- a/pkg/document/crdt/rga_tree_split.go
+++ b/pkg/document/crdt/rga_tree_split.go
@@ -173,6 +173,11 @@ func (s *RGATreeSplitNode[V]) ID() *RGATreeSplitNodeID {
return s.id
}
+// IDString returns the string representation of the ID.
+func (s *RGATreeSplitNode[V]) IDString() string {
+ return s.id.key()
+}
+
// InsPrevID returns previous node ID at the time of this node insertion.
func (s *RGATreeSplitNode[V]) InsPrevID() *RGATreeSplitNodeID {
if s.insPrev == nil {
@@ -254,11 +259,14 @@ func (s *RGATreeSplitNode[V]) toTestString() string {
// Remove removes this node if it created before the time of deletion are
// deleted. It only marks the deleted time (tombstone).
func (s *RGATreeSplitNode[V]) Remove(removedAt *time.Ticket, maxCreatedAt *time.Ticket) bool {
+ justRemoved := s.removedAt == nil
+
if !s.createdAt().After(maxCreatedAt) &&
(s.removedAt == nil || removedAt.After(s.removedAt)) {
s.removedAt = removedAt
- return true
+ return justRemoved
}
+
return false
}
@@ -281,10 +289,6 @@ type RGATreeSplit[V RGATreeSplitValue] struct {
initialHead *RGATreeSplitNode[V]
treeByIndex *splay.Tree[*RGATreeSplitNode[V]]
treeByID *llrb.Tree[*RGATreeSplitNodeID, *RGATreeSplitNode[V]]
-
- // removedNodeMap is a map to store removed nodes. It is used to
- // delete the node physically when the garbage collection is executed.
- removedNodeMap map[string]*RGATreeSplitNode[V]
}
// NewRGATreeSplit creates a new instance of RGATreeSplit.
@@ -294,10 +298,9 @@ func NewRGATreeSplit[V RGATreeSplitValue](initialHead *RGATreeSplitNode[V]) *RGA
treeByID.Put(initialHead.ID(), initialHead)
return &RGATreeSplit[V]{
- initialHead: initialHead,
- treeByIndex: treeByIndex,
- treeByID: treeByID,
- removedNodeMap: make(map[string]*RGATreeSplitNode[V]),
+ initialHead: initialHead,
+ treeByIndex: treeByIndex,
+ treeByID: treeByID,
}
}
@@ -448,20 +451,20 @@ func (s *RGATreeSplit[V]) edit(
maxCreatedAtMapByActor map[string]*time.Ticket,
content V,
editedAt *time.Ticket,
-) (*RGATreeSplitNodePos, map[string]*time.Ticket, error) {
+) (*RGATreeSplitNodePos, map[string]*time.Ticket, []GCPair, error) {
// 01. Split nodes with from and to
toLeft, toRight, err := s.findNodeWithSplit(to, editedAt)
if err != nil {
- return nil, nil, err
+ return nil, nil, nil, err
}
fromLeft, fromRight, err := s.findNodeWithSplit(from, editedAt)
if err != nil {
- return nil, nil, err
+ return nil, nil, nil, err
}
// 02. delete between from and to
nodesToDelete := s.findBetween(fromRight, toRight)
- maxCreatedAtMap, removedNodeMapByNodeKey := s.deleteNodes(nodesToDelete, maxCreatedAtMapByActor, editedAt)
+ maxCreatedAtMap, removedNodes := s.deleteNodes(nodesToDelete, maxCreatedAtMapByActor, editedAt)
var caretID *RGATreeSplitNodeID
if toRight == nil {
@@ -478,11 +481,15 @@ func (s *RGATreeSplit[V]) edit(
}
// 04. add removed node
- for key, removedNode := range removedNodeMapByNodeKey {
- s.removedNodeMap[key] = removedNode
+ var pairs []GCPair
+ for _, removedNode := range removedNodes {
+ pairs = append(pairs, GCPair{
+ Parent: s,
+ Child: removedNode,
+ })
}
- return caretPos, maxCreatedAtMap, nil
+ return caretPos, maxCreatedAtMap, pairs, nil
}
func (s *RGATreeSplit[V]) findBetween(from, to *RGATreeSplitNode[V]) []*RGATreeSplitNode[V] {
@@ -617,29 +624,10 @@ func (s *RGATreeSplit[V]) ToTestString() string {
return builder.String()
}
-// removedNodesLen returns length of removed nodes
-func (s *RGATreeSplit[V]) removedNodesLen() int {
- return len(s.removedNodeMap)
-}
-
-// purgeRemovedNodesBefore physically purges nodes that have been removed.
-func (s *RGATreeSplit[V]) purgeRemovedNodesBefore(ticket *time.Ticket) (int, error) {
- count := 0
- for _, node := range s.removedNodeMap {
- if node.removedAt != nil && ticket.Compare(node.removedAt) >= 0 {
- s.treeByIndex.Delete(node.indexNode)
- s.purge(node)
- s.treeByID.Remove(node.id)
- delete(s.removedNodeMap, node.id.key())
- count++
- }
- }
+// Purge physically purge the given node from RGATreeSplit.
+func (s *RGATreeSplit[V]) Purge(child GCChild) error {
+ node := child.(*RGATreeSplitNode[V])
- return count, nil
-}
-
-// purge physically purge the given node from RGATreeSplit.
-func (s *RGATreeSplit[V]) purge(node *RGATreeSplitNode[V]) {
node.prev.next = node.next
if node.next != nil {
node.next.prev = node.prev
@@ -653,4 +641,6 @@ func (s *RGATreeSplit[V]) purge(node *RGATreeSplitNode[V]) {
node.insNext.insPrev = node.insPrev
}
node.insPrev, node.insNext = nil, nil
+
+ return nil
}
diff --git a/pkg/document/crdt/rht.go b/pkg/document/crdt/rht.go
index 48b8ad348..94c85485a 100644
--- a/pkg/document/crdt/rht.go
+++ b/pkg/document/crdt/rht.go
@@ -41,8 +41,8 @@ func newRHTNode(key, val string, updatedAt *time.Ticket, isRemoved bool) *RHTNod
}
}
-// ID returns the ID of this node.
-func (n *RHTNode) ID() string {
+// IDString returns the string representation of this node.
+func (n *RHTNode) IDString() string {
return n.updatedAt.Key() + ":" + n.key
}
@@ -231,8 +231,8 @@ func (rht *RHT) Marshal() string {
// Purge purges the given child node.
func (rht *RHT) Purge(child *RHTNode) error {
- if node, ok := rht.nodeMapByKey[child.key]; !ok || node.ID() != child.ID() {
- //return ErrChildNotFound
+ if node, ok := rht.nodeMapByKey[child.key]; !ok || node.IDString() != child.IDString() {
+ // TODO(hackerwins): Should we return an error when the child is not found?
return nil
}
diff --git a/pkg/document/crdt/root.go b/pkg/document/crdt/root.go
index e1e5c0c50..488e1b91c 100644
--- a/pkg/document/crdt/root.go
+++ b/pkg/document/crdt/root.go
@@ -36,20 +36,18 @@ type ElementPair struct {
// Every element has a unique time ticket at creation, which allows us to find
// a particular element.
type Root struct {
- object *Object
- elementMapByCreatedAt map[string]Element
- removedElementPairMapByCreatedAt map[string]ElementPair
- elementHasRemovedNodesSetByCreatedAt map[string]GCElement
- gcPairMapByID map[string]GCPair
+ object *Object
+ elementMap map[string]Element
+ gcElementPairMap map[string]ElementPair
+ gcNodePairMap map[string]GCPair
}
// NewRoot creates a new instance of Root.
func NewRoot(root *Object) *Root {
r := &Root{
- elementMapByCreatedAt: make(map[string]Element),
- removedElementPairMapByCreatedAt: make(map[string]ElementPair),
- elementHasRemovedNodesSetByCreatedAt: make(map[string]GCElement),
- gcPairMapByID: make(map[string]GCPair),
+ elementMap: make(map[string]Element),
+ gcElementPairMap: make(map[string]ElementPair),
+ gcNodePairMap: make(map[string]GCPair),
}
r.object = root
@@ -59,7 +57,17 @@ func NewRoot(root *Object) *Root {
if elem.RemovedAt() != nil {
r.RegisterRemovedElementPair(parent, elem)
}
- // TODO(hackerwins): Register text elements with garbage
+
+ switch e := elem.(type) {
+ case *Text:
+ for _, pair := range e.GCPairs() {
+ r.RegisterGCPair(pair)
+ }
+ case *Tree:
+ for _, pair := range e.GCPairs() {
+ r.RegisterGCPair(pair)
+ }
+ }
return false
})
@@ -73,18 +81,18 @@ func (r *Root) Object() *Object {
// FindByCreatedAt returns the element of given creation time.
func (r *Root) FindByCreatedAt(createdAt *time.Ticket) Element {
- return r.elementMapByCreatedAt[createdAt.Key()]
+ return r.elementMap[createdAt.Key()]
}
// RegisterElement registers the given element to hash table.
func (r *Root) RegisterElement(element Element) {
- r.elementMapByCreatedAt[element.CreatedAt().Key()] = element
+ r.elementMap[element.CreatedAt().Key()] = element
switch element := element.(type) {
case Container:
{
element.Descendants(func(elem Element, parent Container) bool {
- r.elementMapByCreatedAt[elem.CreatedAt().Key()] = elem
+ r.elementMap[elem.CreatedAt().Key()] = elem
return false
})
}
@@ -97,8 +105,8 @@ func (r *Root) deregisterElement(element Element) int {
deregisterElementInternal := func(elem Element) {
createdAt := elem.CreatedAt().Key()
- delete(r.elementMapByCreatedAt, createdAt)
- delete(r.removedElementPairMapByCreatedAt, createdAt)
+ delete(r.elementMap, createdAt)
+ delete(r.gcElementPairMap, createdAt)
count++
}
@@ -119,17 +127,12 @@ func (r *Root) deregisterElement(element Element) int {
// RegisterRemovedElementPair register the given element pair to hash table.
func (r *Root) RegisterRemovedElementPair(parent Container, elem Element) {
- r.removedElementPairMapByCreatedAt[elem.CreatedAt().Key()] = ElementPair{
+ r.gcElementPairMap[elem.CreatedAt().Key()] = ElementPair{
parent,
elem,
}
}
-// RegisterElementHasRemovedNodes register the given element with garbage to hash table.
-func (r *Root) RegisterElementHasRemovedNodes(element GCElement) {
- r.elementHasRemovedNodesSetByCreatedAt[element.CreatedAt().Key()] = element
-}
-
// DeepCopy copies itself deeply.
func (r *Root) DeepCopy() (*Root, error) {
copiedObject, err := r.object.DeepCopy()
@@ -143,8 +146,8 @@ func (r *Root) DeepCopy() (*Root, error) {
func (r *Root) GarbageCollect(ticket *time.Ticket) (int, error) {
count := 0
- for _, pair := range r.removedElementPairMapByCreatedAt {
- if pair.elem.RemovedAt() != nil && ticket.Compare(pair.elem.RemovedAt()) >= 0 {
+ for _, pair := range r.gcElementPairMap {
+ if ticket.Compare(pair.elem.RemovedAt()) >= 0 {
if err := pair.parent.Purge(pair.elem); err != nil {
return 0, err
}
@@ -153,25 +156,13 @@ func (r *Root) GarbageCollect(ticket *time.Ticket) (int, error) {
}
}
- for _, node := range r.elementHasRemovedNodesSetByCreatedAt {
- purgedNodes, err := node.purgeRemovedNodesBefore(ticket)
- if err != nil {
- return 0, err
- }
-
- if node.removedNodesLen() == 0 {
- delete(r.elementHasRemovedNodesSetByCreatedAt, node.CreatedAt().Key())
- }
- count += purgedNodes
- }
-
- for _, pair := range r.gcPairMapByID {
+ for _, pair := range r.gcNodePairMap {
if ticket.Compare(pair.Child.RemovedAt()) >= 0 {
if err := pair.Parent.Purge(pair.Child); err != nil {
return 0, err
}
- delete(r.gcPairMapByID, pair.Child.ID())
+ delete(r.gcNodePairMap, pair.Child.IDString())
count++
}
}
@@ -181,20 +172,14 @@ func (r *Root) GarbageCollect(ticket *time.Ticket) (int, error) {
// ElementMapLen returns the size of element map.
func (r *Root) ElementMapLen() int {
- return len(r.elementMapByCreatedAt)
+ return len(r.elementMap)
}
-// RemovedElementLen returns the size of removed element map.
-func (r *Root) RemovedElementLen() int {
- return len(r.removedElementPairMapByCreatedAt)
-}
-
-// GarbageLen returns the count of removed elements.
-func (r *Root) GarbageLen() int {
- count := 0
+// GarbageElementLen return the count of removed elements.
+func (r *Root) GarbageElementLen() int {
seen := make(map[string]bool)
- for _, pair := range r.removedElementPairMapByCreatedAt {
+ for _, pair := range r.gcElementPairMap {
seen[pair.elem.CreatedAt().Key()] = true
switch elem := pair.elem.(type) {
@@ -206,23 +191,22 @@ func (r *Root) GarbageLen() int {
}
}
- count += len(seen)
-
- for _, element := range r.elementHasRemovedNodesSetByCreatedAt {
- count += element.removedNodesLen()
- }
-
- count += len(r.gcPairMapByID)
+ return len(seen)
+}
- return count
+// GarbageLen returns the count of removed elements and internal nodes.
+func (r *Root) GarbageLen() int {
+ return r.GarbageElementLen() + len(r.gcNodePairMap)
}
// RegisterGCPair registers the given pair to hash table.
func (r *Root) RegisterGCPair(pair GCPair) {
- if _, ok := r.gcPairMapByID[pair.Child.ID()]; ok {
- delete(r.gcPairMapByID, pair.Child.ID())
+ // NOTE(hackerwins): If the child is already registered, it means that the
+ // child should be removed from the cache.
+ if _, ok := r.gcNodePairMap[pair.Child.IDString()]; ok {
+ delete(r.gcNodePairMap, pair.Child.IDString())
return
}
- r.gcPairMapByID[pair.Child.ID()] = pair
+ r.gcNodePairMap[pair.Child.IDString()] = pair
}
diff --git a/pkg/document/crdt/root_test.go b/pkg/document/crdt/root_test.go
index e6c6a8b2e..138f4574c 100644
--- a/pkg/document/crdt/root_test.go
+++ b/pkg/document/crdt/root_test.go
@@ -26,9 +26,9 @@ import (
"github.com/yorkie-team/yorkie/test/helper"
)
-func registerElementHasRemovedNodes(fromPos, toPos *crdt.RGATreeSplitNodePos, root *crdt.Root, text crdt.GCElement) {
- if !fromPos.Equal(toPos) {
- root.RegisterElementHasRemovedNodes(text)
+func registerGCPairs(root *crdt.Root, pairs []crdt.GCPair) {
+ for _, pair := range pairs {
+ root.RegisterGCPair(pair)
}
}
@@ -61,76 +61,66 @@ func TestRoot(t *testing.T) {
})
t.Run("garbage collection for text test", func(t *testing.T) {
+ steps := []struct {
+ from int
+ to int
+ content string
+ want string
+ garbage int
+ }{
+ {0, 0, "Hi World", `[0:0:00:0 {} ""][0:2:00:0 {} "Hi World"]`, 0},
+ {2, 7, "Earth", `[0:0:00:0 {} ""][0:2:00:0 {} "Hi"][0:3:00:0 {} "Earth"]{0:2:00:2 {} " Worl"}[0:2:00:7 {} "d"]`, 1},
+ {0, 2, "", `[0:0:00:0 {} ""]{0:2:00:0 {} "Hi"}[0:3:00:0 {} "Earth"]{0:2:00:2 {} " Worl"}[0:2:00:7 {} "d"]`, 2},
+ {5, 6, "", `[0:0:00:0 {} ""]{0:2:00:0 {} "Hi"}[0:3:00:0 {} "Earth"]{0:2:00:2 {} " Worl"}{0:2:00:7 {} "d"}`, 3},
+ }
+
root := helper.TestRoot()
ctx := helper.TextChangeContext(root)
text := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket())
- fromPos, toPos, _ := text.CreateRange(0, 0)
- _, _, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
- assert.NoError(t, err)
- registerElementHasRemovedNodes(fromPos, toPos, root, text)
- assert.Equal(t, "Hello World", text.String())
- assert.Equal(t, 0, root.GarbageLen())
-
- fromPos, toPos, _ = text.CreateRange(5, 10)
- _, _, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
- assert.NoError(t, err)
- registerElementHasRemovedNodes(fromPos, toPos, root, text)
- assert.Equal(t, "HelloYorkied", text.String())
- assert.Equal(t, 1, root.GarbageLen())
-
- fromPos, toPos, _ = text.CreateRange(0, 5)
- _, _, err = text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket())
- assert.NoError(t, err)
- registerElementHasRemovedNodes(fromPos, toPos, root, text)
- assert.Equal(t, "Yorkied", text.String())
- assert.Equal(t, 2, root.GarbageLen())
-
- fromPos, toPos, _ = text.CreateRange(6, 7)
- _, _, err = text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket())
- assert.NoError(t, err)
- registerElementHasRemovedNodes(fromPos, toPos, root, text)
- assert.Equal(t, "Yorkie", text.String())
- assert.Equal(t, 3, root.GarbageLen())
+ for _, step := range steps {
+ fromPos, toPos, _ := text.CreateRange(step.from, step.to)
+ _, _, pairs, err := text.Edit(fromPos, toPos, nil, step.content, nil, ctx.IssueTimeTicket())
+ assert.NoError(t, err)
+ registerGCPairs(root, pairs)
+ assert.Equal(t, step.want, text.ToTestString())
+ assert.Equal(t, step.garbage, root.GarbageLen())
+ }
// It contains code marked tombstone.
// After calling the garbage collector, the node will be removed.
- nodeLen := len(text.Nodes())
- assert.Equal(t, 4, nodeLen)
+ assert.Equal(t, 4, len(text.Nodes()))
n, err := root.GarbageCollect(time.MaxTicket)
assert.NoError(t, err)
assert.Equal(t, 3, n)
assert.Equal(t, 0, root.GarbageLen())
- nodeLen = len(text.Nodes())
- assert.Equal(t, 1, nodeLen)
+ assert.Equal(t, 1, len(text.Nodes()))
})
t.Run("garbage collection for fragments of text", func(t *testing.T) {
- type test struct {
+ steps := []struct {
from int
to int
content string
want string
garbage int
- }
-
- root := helper.TestRoot()
- ctx := helper.TextChangeContext(root)
- text := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket())
-
- tests := []test{
+ }{
{from: 0, to: 0, content: "Yorkie", want: "Yorkie", garbage: 0},
{from: 4, to: 5, content: "", want: "Yorke", garbage: 1},
{from: 2, to: 3, content: "", want: "Yoke", garbage: 2},
{from: 0, to: 1, content: "", want: "oke", garbage: 3},
}
- for _, tc := range tests {
+ root := helper.TestRoot()
+ ctx := helper.TextChangeContext(root)
+ text := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket())
+
+ for _, tc := range steps {
fromPos, toPos, _ := text.CreateRange(tc.from, tc.to)
- _, _, err := text.Edit(fromPos, toPos, nil, tc.content, nil, ctx.IssueTimeTicket())
+ _, _, pairs, err := text.Edit(fromPos, toPos, nil, tc.content, nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
- registerElementHasRemovedNodes(fromPos, toPos, root, text)
+ registerGCPairs(root, pairs)
assert.Equal(t, tc.want, text.String())
assert.Equal(t, tc.garbage, root.GarbageLen())
}
@@ -147,23 +137,23 @@ func TestRoot(t *testing.T) {
text := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket())
fromPos, toPos, _ := text.CreateRange(0, 0)
- _, _, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
+ _, _, pairs, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
- registerElementHasRemovedNodes(fromPos, toPos, root, text)
+ registerGCPairs(root, pairs)
assert.Equal(t, `[{"val":"Hello World"}]`, text.Marshal())
assert.Equal(t, 0, root.GarbageLen())
fromPos, toPos, _ = text.CreateRange(6, 11)
- _, _, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
+ _, _, pairs, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
- registerElementHasRemovedNodes(fromPos, toPos, root, text)
+ registerGCPairs(root, pairs)
assert.Equal(t, `[{"val":"Hello "},{"val":"Yorkie"}]`, text.Marshal())
assert.Equal(t, 1, root.GarbageLen())
fromPos, toPos, _ = text.CreateRange(0, 6)
- _, _, err = text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket())
+ _, _, pairs, err = text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
- registerElementHasRemovedNodes(fromPos, toPos, root, text)
+ registerGCPairs(root, pairs)
assert.Equal(t, `[{"val":"Yorkie"}]`, text.Marshal())
assert.Equal(t, 2, root.GarbageLen())
diff --git a/pkg/document/crdt/text.go b/pkg/document/crdt/text.go
index 81ef1f70b..6cd09295f 100644
--- a/pkg/document/crdt/text.go
+++ b/pkg/document/crdt/text.go
@@ -110,6 +110,25 @@ func (t *TextValue) Purge(child GCChild) error {
return t.attrs.Purge(rhtNode)
}
+// GCPairs returns the pairs of GC.
+func (t *TextValue) GCPairs() []GCPair {
+ if t.attrs == nil {
+ return nil
+ }
+
+ var pairs []GCPair
+ for _, node := range t.attrs.Nodes() {
+ if node.isRemoved {
+ pairs = append(pairs, GCPair{
+ Parent: t,
+ Child: node,
+ })
+ }
+ }
+
+ return pairs
+}
+
// InitialTextNode creates an initial node of Text. The text is edited
// as this node is split into multiple nodes.
func InitialTextNode() *RGATreeSplitNode[*TextValue] {
@@ -185,6 +204,25 @@ func (t *Text) DeepCopy() (Element, error) {
return NewText(rgaTreeSplit, t.createdAt), nil
}
+// GCPairs returns the pairs of GC.
+func (t *Text) GCPairs() []GCPair {
+ var pairs []GCPair
+ for _, node := range t.Nodes() {
+ if node.removedAt != nil {
+ pairs = append(pairs, GCPair{
+ Parent: t.rgaTreeSplit,
+ Child: node,
+ })
+ }
+
+ for _, p := range node.Value().GCPairs() {
+ pairs = append(pairs, p)
+ }
+ }
+
+ return pairs
+}
+
// CreatedAt returns the creation time of this Text.
func (t *Text) CreatedAt() *time.Ticket {
return t.createdAt
@@ -233,7 +271,7 @@ func (t *Text) Edit(
content string,
attributes map[string]string,
executedAt *time.Ticket,
-) (*RGATreeSplitNodePos, map[string]*time.Ticket, error) {
+) (*RGATreeSplitNodePos, map[string]*time.Ticket, []GCPair, error) {
val := NewTextValue(content, NewRHT())
for key, value := range attributes {
val.attrs.Set(key, value, executedAt)
@@ -328,13 +366,3 @@ func (t *Text) ToTestString() string {
func (t *Text) CheckWeight() bool {
return t.rgaTreeSplit.CheckWeight()
}
-
-// removedNodesLen returns length of removed nodes
-func (t *Text) removedNodesLen() int {
- return t.rgaTreeSplit.removedNodesLen()
-}
-
-// purgeRemovedNodesBefore physically purges nodes that have been removed.
-func (t *Text) purgeRemovedNodesBefore(ticket *time.Ticket) (int, error) {
- return t.rgaTreeSplit.purgeRemovedNodesBefore(ticket)
-}
diff --git a/pkg/document/crdt/text_test.go b/pkg/document/crdt/text_test.go
index c2a816586..474cf819b 100644
--- a/pkg/document/crdt/text_test.go
+++ b/pkg/document/crdt/text_test.go
@@ -32,12 +32,12 @@ func TestText(t *testing.T) {
text := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket())
fromPos, toPos, _ := text.CreateRange(0, 0)
- _, _, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
+ _, _, _, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
assert.Equal(t, `[{"val":"Hello World"}]`, text.Marshal())
fromPos, toPos, _ = text.CreateRange(6, 11)
- _, _, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
+ _, _, _, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
assert.Equal(t, `[{"val":"Hello "},{"val":"Yorkie"}]`, text.Marshal())
})
@@ -70,12 +70,12 @@ func TestText(t *testing.T) {
text := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket())
fromPos, toPos, _ := text.CreateRange(0, 0)
- _, _, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
+ _, _, _, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
assert.Equal(t, `[{"val":"Hello World"}]`, text.Marshal())
fromPos, toPos, _ = text.CreateRange(6, 11)
- _, _, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
+ _, _, _, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
assert.Equal(t, `[{"val":"Hello "},{"val":"Yorkie"}]`, text.Marshal())
diff --git a/pkg/document/crdt/tree.go b/pkg/document/crdt/tree.go
index e04d5a709..ed79d4eed 100644
--- a/pkg/document/crdt/tree.go
+++ b/pkg/document/crdt/tree.go
@@ -92,7 +92,7 @@ func NewTreeNodeID(createdAt *time.Ticket, offset int) *TreeNodeID {
// NewTreeNode creates a new instance of TreeNode.
func NewTreeNode(id *TreeNodeID, nodeType string, attributes *RHT, value ...string) *TreeNode {
- node := &TreeNode{ID: id}
+ node := &TreeNode{id: id}
// NOTE(hackerwins): The value of TreeNode is optional. If the value is
// empty, it means that the node is an element node.
@@ -134,8 +134,8 @@ func (t *TreeNodeID) Equals(id *TreeNodeID) bool {
type TreeNode struct {
Index *index.Node[*TreeNode]
- ID *TreeNodeID
- RemovedAt *time.Ticket
+ id *TreeNodeID
+ removedAt *time.Ticket
InsPrevID *TreeNodeID
InsNextID *TreeNodeID
@@ -164,9 +164,29 @@ func (n *TreeNode) IsText() bool {
return n.Index.IsText()
}
+// ID returns the ID of this Node.
+func (n *TreeNode) ID() *TreeNodeID {
+ return n.id
+}
+
+// IDString returns the IDString of this Node.
+func (n *TreeNode) IDString() string {
+ return n.id.toIDString()
+}
+
+// RemovedAt returns the removal time of this Node.
+func (n *TreeNode) RemovedAt() *time.Ticket {
+ return n.removedAt
+}
+
+// SetRemovedAt sets the removal time of this node.
+func (n *TreeNode) SetRemovedAt(ticket *time.Ticket) {
+ n.removedAt = ticket
+}
+
// IsRemoved returns whether the Node is removed or not.
func (n *TreeNode) IsRemoved() bool {
- return n.RemovedAt != nil
+ return n.removedAt != nil
}
// Length returns the length of this node.
@@ -249,7 +269,7 @@ func (n *TreeNode) Split(tree *Tree, offset int, issueTimeTicket func() *time.Ti
var split *TreeNode
var err error
if n.IsText() {
- split, err = n.SplitText(offset, n.ID.Offset)
+ split, err = n.SplitText(offset, n.id.Offset)
if err != nil {
return err
}
@@ -261,14 +281,14 @@ func (n *TreeNode) Split(tree *Tree, offset int, issueTimeTicket func() *time.Ti
}
if split != nil {
- split.InsPrevID = n.ID
+ split.InsPrevID = n.id
if n.InsNextID != nil {
insNext := tree.findFloorNode(n.InsNextID)
- insNext.InsPrevID = split.ID
+ insNext.InsPrevID = split.id
split.InsNextID = n.InsNextID
}
- n.InsNextID = split.ID
- tree.NodeMapByID.Put(split.ID, split)
+ n.InsNextID = split.id
+ tree.NodeMapByID.Put(split.id, split)
}
return nil
@@ -292,10 +312,10 @@ func (n *TreeNode) SplitText(offset, absOffset int) (*TreeNode, error) {
n.Index.Length = len(leftRune)
rightNode := NewTreeNode(&TreeNodeID{
- CreatedAt: n.ID.CreatedAt,
+ CreatedAt: n.id.CreatedAt,
Offset: offset + absOffset,
}, n.Type(), nil, string(rightRune))
- rightNode.RemovedAt = n.RemovedAt
+ rightNode.removedAt = n.removedAt
if err := n.Index.Parent.InsertAfterInternal(
rightNode.Index,
@@ -309,14 +329,14 @@ func (n *TreeNode) SplitText(offset, absOffset int) (*TreeNode, error) {
// SplitElement splits the given element at the given offset.
func (n *TreeNode) SplitElement(offset int, issueTimeTicket func() *time.Ticket) (*TreeNode, error) {
- // TODO(hackerwins): Define ID of split node for concurrent editing.
+ // TODO(hackerwins): Define IDString of split node for concurrent editing.
// Text has fixed content and its split nodes could have limited offset
// range. But element node could have arbitrary children and its split
// nodes could have arbitrary offset range. So, id could be duplicated
// and its order could be broken when concurrent editing happens.
- // Currently, we use the similar ID of split element with the split text.
+ // Currently, we use the similar IDString of split element with the split text.
split := NewTreeNode(&TreeNodeID{CreatedAt: issueTimeTicket(), Offset: 0}, n.Type(), nil)
- split.RemovedAt = n.RemovedAt
+ split.removedAt = n.removedAt
if err := n.Index.Parent.InsertAfterInternal(split.Index, n.Index); err != nil {
return nil, err
}
@@ -348,33 +368,34 @@ func (n *TreeNode) SplitElement(offset int, issueTimeTicket func() *time.Ticket)
// remove marks the node as removed.
func (n *TreeNode) remove(removedAt *time.Ticket) bool {
- justRemoved := n.RemovedAt == nil
- if n.RemovedAt == nil || n.RemovedAt.Compare(removedAt) > 0 {
- n.RemovedAt = removedAt
+ justRemoved := n.removedAt == nil
+
+ if n.removedAt == nil || n.removedAt.Compare(removedAt) > 0 {
+ n.removedAt = removedAt
if justRemoved {
- if n.Index.Parent.Value.RemovedAt == nil {
+ if n.Index.Parent.Value.removedAt == nil {
n.Index.UpdateAncestorsSize()
} else {
n.Index.Parent.Length -= n.Index.PaddedLength()
}
}
- return true
+ return justRemoved
}
return false
}
func (n *TreeNode) canDelete(removedAt *time.Ticket, maxCreatedAt *time.Ticket) bool {
- if !n.ID.CreatedAt.After(maxCreatedAt) &&
- (n.RemovedAt == nil || n.RemovedAt.Compare(removedAt) > 0) {
+ if !n.id.CreatedAt.After(maxCreatedAt) &&
+ (n.removedAt == nil || n.removedAt.Compare(removedAt) > 0) {
return true
}
return false
}
func (n *TreeNode) canStyle(editedAt *time.Ticket, maxCreatedAt *time.Ticket) bool {
- return !n.ID.CreatedAt.After(maxCreatedAt) &&
- (n.RemovedAt == nil || editedAt.After(n.RemovedAt))
+ return !n.id.CreatedAt.After(maxCreatedAt) &&
+ (n.removedAt == nil || editedAt.After(n.removedAt))
}
// InsertAt inserts the given node at the given offset.
@@ -389,9 +410,9 @@ func (n *TreeNode) DeepCopy() (*TreeNode, error) {
attrs = n.Attrs.DeepCopy()
}
- clone := NewTreeNode(n.ID, n.Type(), attrs, n.Value)
+ clone := NewTreeNode(n.id, n.Type(), attrs, n.Value)
clone.Index.Length = n.Index.Length
- clone.RemovedAt = n.RemovedAt
+ clone.removedAt = n.removedAt
clone.InsPrevID = n.InsPrevID
clone.InsNextID = n.InsNextID
@@ -439,12 +460,30 @@ func (n *TreeNode) RemoveAttr(k string, ticket *time.Ticket) []*RHTNode {
return n.Attrs.Remove(k, ticket)
}
+// GCPairs returns the pairs of GC.
+func (n *TreeNode) GCPairs() []GCPair {
+ if n.Attrs == nil {
+ return nil
+ }
+
+ var pairs []GCPair
+ for _, node := range n.Attrs.Nodes() {
+ if node.isRemoved {
+ pairs = append(pairs, GCPair{
+ Parent: n,
+ Child: node,
+ })
+ }
+ }
+
+ return pairs
+}
+
// Tree represents the tree of CRDT. It has doubly linked list structure and
// index tree structure.
type Tree struct {
- IndexTree *index.Tree[*TreeNode]
- NodeMapByID *llrb.Tree[*TreeNodeID, *TreeNode]
- removedNodeMap map[string]*TreeNode
+ IndexTree *index.Tree[*TreeNode]
+ NodeMapByID *llrb.Tree[*TreeNodeID, *TreeNode]
createdAt *time.Ticket
movedAt *time.Ticket
@@ -454,14 +493,13 @@ type Tree struct {
// NewTree creates a new instance of Tree.
func NewTree(root *TreeNode, createdAt *time.Ticket) *Tree {
tree := &Tree{
- IndexTree: index.NewTree[*TreeNode](root.Index),
- NodeMapByID: llrb.NewTree[*TreeNodeID, *TreeNode](),
- removedNodeMap: make(map[string]*TreeNode),
- createdAt: createdAt,
+ IndexTree: index.NewTree[*TreeNode](root.Index),
+ NodeMapByID: llrb.NewTree[*TreeNodeID, *TreeNode](),
+ createdAt: createdAt,
}
index.Traverse(tree.IndexTree, func(node *index.Node[*TreeNode], depth int) {
- tree.NodeMapByID.Put(node.Value.ID, node.Value)
+ tree.NodeMapByID.Put(node.Value.id, node.Value)
})
return tree
@@ -474,38 +512,14 @@ func (t *Tree) Marshal() string {
return builder.String()
}
-// removedNodesLen returns the length of removed nodes.
-func (t *Tree) removedNodesLen() int {
- return len(t.removedNodeMap)
-}
-
-// purgeRemovedNodesBefore physically purges nodes that have been removed.
-func (t *Tree) purgeRemovedNodesBefore(ticket *time.Ticket) (int, error) {
- count := 0
- nodesToBeRemoved := make(map[*TreeNode]bool)
-
- for _, node := range t.removedNodeMap {
- if node.RemovedAt != nil && ticket.Compare(node.RemovedAt) >= 0 {
- count++
- nodesToBeRemoved[node] = true
- }
- }
-
- for node := range nodesToBeRemoved {
- if err := t.purgeNode(node); err != nil {
- return 0, err
- }
- }
-
- return count, nil
-}
+// Purge physically purges the given node.
+func (t *Tree) Purge(child GCChild) error {
+ node := child.(*TreeNode)
-// purgeNode physically purges the given node.
-func (t *Tree) purgeNode(node *TreeNode) error {
if err := node.Index.Parent.RemoveChild(node.Index); err != nil {
return err
}
- t.NodeMapByID.Remove(node.ID)
+ t.NodeMapByID.Remove(node.id)
insPrevID := node.InsPrevID
insNextID := node.InsNextID
@@ -520,7 +534,6 @@ func (t *Tree) purgeNode(node *TreeNode) error {
node.InsPrevID = nil
node.InsNextID = nil
- delete(t.removedNodeMap, node.ID.toIDString())
return nil
}
@@ -558,6 +571,26 @@ func (t *Tree) DeepCopy() (Element, error) {
return NewTree(node, t.createdAt), nil
}
+// GCPairs returns the pairs of GC.
+func (t *Tree) GCPairs() []GCPair {
+ var pairs []GCPair
+
+ for _, node := range t.Nodes() {
+ if node.removedAt != nil {
+ pairs = append(pairs, GCPair{
+ Parent: t,
+ Child: node,
+ })
+ }
+
+ for _, p := range node.GCPairs() {
+ pairs = append(pairs, p)
+ }
+ }
+
+ return pairs
+}
+
// CreatedAt returns the creation time of this Tree.
func (t *Tree) CreatedAt() *time.Ticket {
return t.createdAt
@@ -621,17 +654,18 @@ func (t *Tree) EditT(
splitLevel int,
editedAt *time.Ticket,
issueTimeTicket func() *time.Ticket,
-) (map[string]*time.Ticket, error) {
+) error {
fromPos, err := t.FindPos(start)
if err != nil {
- return nil, err
+ return err
}
toPos, err := t.FindPos(end)
if err != nil {
- return nil, err
+ return err
}
- return t.Edit(fromPos, toPos, contents, splitLevel, editedAt, issueTimeTicket, nil)
+ _, _, err = t.Edit(fromPos, toPos, contents, splitLevel, editedAt, issueTimeTicket, nil)
+ return err
}
// FindPos finds the position of the given index in the tree.
@@ -661,10 +695,10 @@ func (t *Tree) FindPos(offset int) (*TreePos, error) {
}
return &TreePos{
- ParentID: node.Value.ID,
+ ParentID: node.Value.id,
LeftSiblingID: &TreeNodeID{
- CreatedAt: leftNode.ID.CreatedAt,
- Offset: leftNode.ID.Offset + offset,
+ CreatedAt: leftNode.id.CreatedAt,
+ Offset: leftNode.id.Offset + offset,
},
}, nil
}
@@ -678,15 +712,15 @@ func (t *Tree) Edit(
editedAt *time.Ticket,
issueTimeTicket func() *time.Ticket,
maxCreatedAtMapByActor map[string]*time.Ticket,
-) (map[string]*time.Ticket, error) {
+) (map[string]*time.Ticket, []GCPair, error) {
// 01. find nodes from the given range and split nodes.
fromParent, fromLeft, err := t.FindTreeNodesWithSplitText(from, editedAt)
if err != nil {
- return nil, err
+ return nil, nil, err
}
toParent, toLeft, err := t.FindTreeNodesWithSplitText(to, editedAt)
if err != nil {
- return nil, err
+ return nil, nil, err
}
toBeRemoveds, toBeMovedToFromParents, maxCreatedAtMap, err := t.collectBetween(
@@ -694,28 +728,32 @@ func (t *Tree) Edit(
maxCreatedAtMapByActor, editedAt,
)
if err != nil {
- return nil, err
+ return nil, nil, err
}
// 02. Delete: delete the nodes that are marked as removed.
+ var pairs []GCPair
for _, node := range toBeRemoveds {
if node.remove(editedAt) {
- t.removedNodeMap[node.ID.toIDString()] = node
+ pairs = append(pairs, GCPair{
+ Parent: t,
+ Child: node,
+ })
}
}
// 03. Merge: move the nodes that are marked as moved.
for _, node := range toBeMovedToFromParents {
- if node.RemovedAt == nil {
+ if node.removedAt == nil {
if err := fromParent.Append(node); err != nil {
- return nil, err
+ return nil, nil, err
}
}
}
// 04. Split: split the element nodes for the given splitLevel.
if err := t.split(fromParent, fromLeft, splitLevel, issueTimeTicket); err != nil {
- return nil, err
+ return nil, nil, err
}
// 05. Insert: insert the given node at the given position.
@@ -728,13 +766,13 @@ func (t *Tree) Edit(
// 05-1-1. when there's no leftSibling, then insert content into very front of parent's children List
err := fromParent.InsertAt(content, 0)
if err != nil {
- return nil, err
+ return nil, nil, err
}
} else {
// 05-1-2. insert after leftSibling
err := fromParent.InsertAfter(content, leftInChildren)
if err != nil {
- return nil, err
+ return nil, nil, err
}
}
@@ -743,23 +781,27 @@ func (t *Tree) Edit(
// if insertion happens during concurrent editing and parent node has been removed,
// make new nodes as tombstone immediately
if fromParent.IsRemoved() {
- actorIDHex := node.Value.ID.CreatedAt.ActorIDHex()
+ actorIDHex := node.Value.id.CreatedAt.ActorIDHex()
if node.Value.remove(editedAt) {
maxCreatedAt := maxCreatedAtMap[actorIDHex]
- createdAt := node.Value.ID.CreatedAt
+ createdAt := node.Value.id.CreatedAt
if maxCreatedAt == nil || createdAt.After(maxCreatedAt) {
maxCreatedAtMap[actorIDHex] = createdAt
}
}
- t.removedNodeMap[node.Value.ID.toIDString()] = node.Value
+
+ pairs = append(pairs, GCPair{
+ Parent: t,
+ Child: node.Value,
+ })
}
- t.NodeMapByID.Put(node.Value.ID, node.Value)
+ t.NodeMapByID.Put(node.Value.id, node.Value)
})
}
}
- return maxCreatedAtMap, nil
+ return maxCreatedAtMap, pairs, nil
}
// collectBetween collects nodes that are marked as removed or moved. It also
@@ -795,7 +837,7 @@ func (t *Tree) collectBetween(
}
}
- actorIDHex := node.ID.CreatedAt.ActorIDHex()
+ actorIDHex := node.id.CreatedAt.ActorIDHex()
var maxCreatedAt *time.Ticket
if maxCreatedAtMapByActor == nil {
@@ -813,7 +855,7 @@ func (t *Tree) collectBetween(
// be removed, then this node should be removed.
if node.canDelete(editedAt, maxCreatedAt) || slices.Contains(toBeRemoveds, node.Index.Parent.Value) {
maxCreatedAt = createdAtMapByActor[actorIDHex]
- createdAt := node.ID.CreatedAt
+ createdAt := node.id.CreatedAt
if maxCreatedAt == nil || createdAt.After(maxCreatedAt) {
createdAtMapByActor[actorIDHex] = createdAt
}
@@ -922,7 +964,7 @@ func (t *Tree) Style(
createdAtMapByActor := make(map[string]*time.Ticket)
if err = t.traverseInPosRange(fromParent, fromLeft, toParent, toLeft, func(token index.TreeToken[*TreeNode], _ bool) {
node := token.Node
- actorIDHex := node.ID.CreatedAt.ActorIDHex()
+ actorIDHex := node.id.CreatedAt.ActorIDHex()
var maxCreatedAt *time.Ticket
if maxCreatedAtMapByActor == nil {
@@ -937,7 +979,7 @@ func (t *Tree) Style(
if node.canStyle(editedAt, maxCreatedAt) && !node.IsText() && len(attrs) > 0 {
maxCreatedAt = createdAtMapByActor[actorIDHex]
- createdAt := node.ID.CreatedAt
+ createdAt := node.id.CreatedAt
if maxCreatedAt == nil || createdAt.After(maxCreatedAt) {
createdAtMapByActor[actorIDHex] = createdAt
}
@@ -959,7 +1001,12 @@ func (t *Tree) Style(
}
// RemoveStyle removes the given attributes of the given range.
-func (t *Tree) RemoveStyle(from, to *TreePos, attrs []string, editedAt *time.Ticket) ([]GCPair, error) {
+func (t *Tree) RemoveStyle(
+ from *TreePos,
+ to *TreePos,
+ attrs []string,
+ editedAt *time.Ticket,
+) ([]GCPair, error) {
fromParent, fromLeft, err := t.FindTreeNodesWithSplitText(from, editedAt)
if err != nil {
return nil, err
@@ -1016,7 +1063,7 @@ func (t *Tree) FindTreeNodesWithSplitText(pos *TreePos, editedAt *time.Ticket) (
// 03. Split text node if the left node is text node.
if leftNode.IsText() {
- err := leftNode.Split(t, pos.LeftSiblingID.Offset-leftNode.ID.Offset, nil)
+ err := leftNode.Split(t, pos.LeftSiblingID.Offset-leftNode.id.Offset, nil)
if err != nil {
return nil, nil, err
}
@@ -1033,7 +1080,7 @@ func (t *Tree) FindTreeNodesWithSplitText(pos *TreePos, editedAt *time.Ticket) (
parentChildren := realParentNode.Index.Children(true)
for i := idx; i < len(parentChildren); i++ {
next := parentChildren[i].Value
- if !next.ID.CreatedAt.After(editedAt) {
+ if !next.id.CreatedAt.After(editedAt) {
break
}
leftNode = next
@@ -1153,9 +1200,9 @@ func (t *Tree) ToTreeNodes(pos *TreePos) (*TreeNode, *TreeNode) {
// NOTE(hackerwins): If the left node and the parent node are the same,
// it means that the position is the left-most of the parent node.
// We need to skip finding the left of the position.
- if !pos.LeftSiblingID.Equals(parentNode.ID) &&
+ if !pos.LeftSiblingID.Equals(parentNode.id) &&
pos.LeftSiblingID.Offset > 0 &&
- pos.LeftSiblingID.Offset == leftNode.ID.Offset &&
+ pos.LeftSiblingID.Offset == leftNode.id.Offset &&
leftNode.InsPrevID != nil {
return parentNode, t.findFloorNode(leftNode.InsPrevID)
}
diff --git a/pkg/document/crdt/tree_test.go b/pkg/document/crdt/tree_test.go
index 03ada2d08..3384fe727 100644
--- a/pkg/document/crdt/tree_test.go
+++ b/pkg/document/crdt/tree_test.go
@@ -38,12 +38,12 @@ func createHelloTree(t *testing.T, ctx *change.Context) *crdt.Tree {
// TODO(raararaara): This test should be generalized. e.g) createTree(ctx, " hello helo hello h e l l o hello world h e l l o ! w o r l d h e l l o ~ ! w o r l d a c d a cd a d ad a d ad ab ab ab ab a b ab ab ab ab ab a b a b c d ab cd abcd ab x
cd
y
ef", tree.ToXML()) - _, err = tree.EditT(2, 18, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(2, 18, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "af