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

") // https://pkg.go.dev/encoding/xml#Unmarshal tree := crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "r", nil), helper.TimeT(ctx)) - _, err := tree.EditT(0, 0, []*crdt.TreeNode{ + err := tree.EditT(0, 0, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "p", nil), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{ + err = tree.EditT(1, 1, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "hello"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) @@ -56,7 +56,7 @@ func createHelloTree(t *testing.T, ctx *change.Context) *crdt.Tree { func TestTreeNode(t *testing.T) { t.Run("text node test", func(t *testing.T) { node := crdt.NewTreeNode(dummyTreeNodeID, "text", nil, "hello") - assert.Equal(t, dummyTreeNodeID, node.ID) + assert.Equal(t, dummyTreeNodeID, node.ID()) assert.Equal(t, "text", node.Type()) assert.Equal(t, "hello", node.Value) assert.Equal(t, 5, node.Len()) @@ -83,8 +83,8 @@ func TestTreeNode(t *testing.T) { assert.Equal(t, "hello", left.Value) assert.Equal(t, "yorkie", right.Value) - assert.Equal(t, &crdt.TreeNodeID{CreatedAt: time.InitialTicket, Offset: 0}, left.ID) - assert.Equal(t, &crdt.TreeNodeID{CreatedAt: time.InitialTicket, Offset: 5}, right.ID) + assert.Equal(t, &crdt.TreeNodeID{CreatedAt: time.InitialTicket, Offset: 0}, left.ID()) + assert.Equal(t, &crdt.TreeNodeID{CreatedAt: time.InitialTicket, Offset: 5}, right.ID()) split, err := para.SplitElement(1, func() *time.Ticket { return time.InitialTicket @@ -132,7 +132,7 @@ func TestTreeNode(t *testing.T) { tree := createHelloTree(t, ctx) // To make tree have a deletion to check length modification. - _, err := tree.EditT(4, 5, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err := tree.EditT(4, 5, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

helo

", tree.ToXML()) assert.Equal(t, 6, tree.Root().Len()) @@ -147,7 +147,7 @@ func TestTreeNode(t *testing.T) { tree := createHelloTree(t, ctx) // To make tree have split text nodes. - _, err := tree.EditT(3, 3, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err := tree.EditT(3, 3, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

hello

", tree.ToXML()) @@ -186,7 +186,7 @@ func TestTreeEdit(t *testing.T) { // 1 //

- _, err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper. + err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper. PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

", tree.ToXML()) @@ -194,7 +194,7 @@ func TestTreeEdit(t *testing.T) { // 1 //

h e l l o

- _, err = tree.EditT(1, 1, []*crdt.TreeNode{ + err = tree.EditT(1, 1, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "hello"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) @@ -206,14 +206,14 @@ func TestTreeEdit(t *testing.T) { p := crdt.NewTreeNode(helper.PosT(ctx), "p", nil) err = p.InsertAt(crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "world"), 0) assert.NoError(t, err) - _, err = tree.EditT(7, 7, []*crdt.TreeNode{p}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(7, 7, []*crdt.TreeNode{p}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

hello

world

", tree.ToXML()) assert.Equal(t, 14, tree.Root().Len()) // 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

- _, err = tree.EditT(6, 6, []*crdt.TreeNode{ + err = tree.EditT(6, 6, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "!"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) @@ -245,7 +245,7 @@ func TestTreeEdit(t *testing.T) { // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 //

h e l l o ~ !

w o r l d

- _, err = tree.EditT(6, 6, []*crdt.TreeNode{ + err = tree.EditT(6, 6, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "~"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) @@ -259,19 +259,19 @@ func TestTreeEdit(t *testing.T) { ctx := helper.TextChangeContext(helper.TestRoot()) tree := crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "root", nil), helper.TimeT(ctx)) - _, err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{ + err = tree.EditT(1, 1, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "ab"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(4, 4, []*crdt.TreeNode{ + err = tree.EditT(4, 4, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "p", nil), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(5, 5, []*crdt.TreeNode{ + err = tree.EditT(5, 5, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "cd"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) @@ -285,7 +285,7 @@ func TestTreeEdit(t *testing.T) { // 02. Delete b from the second paragraph. // 0 1 2 3 4 5 6 7 //

a

c d

- _, err = tree.EditT(2, 3, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(2, 3, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

a

cd

", tree.ToXML()) @@ -302,20 +302,20 @@ func TestTreeEdit(t *testing.T) { ctx := helper.TextChangeContext(helper.TestRoot()) tree := crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "root", nil), helper.TimeT(ctx)) - _, err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{ + err = tree.EditT(1, 1, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "ab"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(4, 4, []*crdt.TreeNode{ + err = tree.EditT(4, 4, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "p", nil), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(5, 5, []*crdt.TreeNode{ + err = tree.EditT(5, 5, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "cd"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) @@ -325,7 +325,7 @@ func TestTreeEdit(t *testing.T) { // 02. delete b, c and the second paragraph. // 0 1 2 3 4 //

a d

- _, err = tree.EditT(2, 6, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(2, 6, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

ad

", tree.ToXML()) @@ -336,7 +336,7 @@ func TestTreeEdit(t *testing.T) { assert.Equal(t, 1, node.Children[0].Children[1].Size) // 03. insert a new text node at the start of the first paragraph. - _, err = tree.EditT(1, 1, []*crdt.TreeNode{ + err = tree.EditT(1, 1, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "@"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) @@ -351,21 +351,21 @@ func TestTreeEdit(t *testing.T) { ctx := helper.TextChangeContext(helper.TestRoot()) tree := crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "root", nil), helper.TimeT(ctx)) - _, err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "b", nil)}, 0, + err = tree.EditT(1, 1, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "b", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(2, 2, []*crdt.TreeNode{ + err = tree.EditT(2, 2, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "ab"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(6, 6, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err = tree.EditT(6, 6, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(7, 7, []*crdt.TreeNode{ + err = tree.EditT(7, 7, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "cd"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) @@ -375,7 +375,7 @@ func TestTreeEdit(t *testing.T) { // 02. delete b, c and the second paragraph. // 0 1 2 3 4 5 //

a d - _, err = tree.EditT(3, 8, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(3, 8, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

ad

", tree.ToXML()) }) @@ -384,20 +384,20 @@ func TestTreeEdit(t *testing.T) { // 01. style attributes to an element node. ctx := helper.TextChangeContext(helper.TestRoot()) tree := crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "root", nil), helper.TimeT(ctx)) - _, err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{ + err = tree.EditT(1, 1, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "ab"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(4, 4, []*crdt.TreeNode{ + err = tree.EditT(4, 4, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "p", nil), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(5, 5, []*crdt.TreeNode{ + err = tree.EditT(5, 5, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "cd"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) @@ -446,20 +446,20 @@ func TestTreeEdit(t *testing.T) { pNode := crdt.NewTreeNode(helper.PosT(ctx), "p", nil) textNode := crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "ab") - _, err := tree.EditT(0, 0, []*crdt.TreeNode{pNode}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err := tree.EditT(0, 0, []*crdt.TreeNode{pNode}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{textNode}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(1, 1, []*crdt.TreeNode{textNode}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

ab

", tree.ToXML()) // Find the closest index.TreePos when leftSiblingNode in crdt.TreePos is removed. // 0 1 2 //

- _, err = tree.EditT(1, 3, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(1, 3, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

", tree.ToXML()) - treePos := crdt.NewTreePos(pNode.ID, textNode.ID) + treePos := crdt.NewTreePos(pNode.ID(), textNode.ID()) parent, leftSibling, err := tree.FindTreeNodesWithSplitText(treePos, helper.TimeT(ctx)) assert.NoError(t, err) @@ -470,11 +470,11 @@ func TestTreeEdit(t *testing.T) { // Find the closest index.TreePos when parentNode in crdt.TreePos is removed. // 0 // - _, err = tree.EditT(0, 2, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(0, 2, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "", tree.ToXML()) - treePos = crdt.NewTreePos(pNode.ID, textNode.ID) + treePos = crdt.NewTreePos(pNode.ID(), textNode.ID()) parent, leftSibling, err = tree.FindTreeNodesWithSplitText(treePos, helper.TimeT(ctx)) assert.NoError(t, err) idx, err = tree.ToIndex(parent, leftSibling) @@ -485,11 +485,11 @@ func TestTreeEdit(t *testing.T) { t.Run("marshal test", func(t *testing.T) { ctx := helper.TextChangeContext(helper.TestRoot()) tree := crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "root", nil), helper.TimeT(ctx)) - _, err := tree.EditT(0, 0, []*crdt.TreeNode{ + err := tree.EditT(0, 0, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "p", nil), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{ + err = tree.EditT(1, 1, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, `"Hello" \n i'm yorkie!`), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) @@ -519,10 +519,10 @@ func TestTreeSplit(t *testing.T) { } tree := crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "r", nil), helper.TimeT(ctx)) - _, err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{ + err = tree.EditT(1, 1, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "helloworld"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) @@ -531,17 +531,17 @@ func TestTreeSplit(t *testing.T) { assert.Equal(t, tree.ToTreeNodeForTest(), expectedInitial) // 01. Split left side of 'helloworld'. - _, err = tree.EditT(1, 1, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(1, 1, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, tree.ToTreeNodeForTest(), expectedInitial) // 02. Split right side of 'helloworld'. - _, err = tree.EditT(11, 11, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(11, 11, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, tree.ToTreeNodeForTest(), expectedInitial) // 03. Split 'helloworld' into 'hello' and 'world'. - _, err = tree.EditT(6, 6, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(6, 6, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, tree.ToTreeNodeForTest(), crdt.TreeNodeForTest{ Type: "r", @@ -566,50 +566,50 @@ func TestTreeSplit(t *testing.T) { // 01. Split position 1. tree := crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "r", nil), helper.TimeT(ctx)) - _, err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{ + err = tree.EditT(1, 1, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "ab"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

ab

", tree.ToXML()) assert.Equal(t, 4, tree.Root().Len()) - _, err = tree.EditT(1, 1, nil, 1, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(1, 1, nil, 1, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

ab

", tree.ToXML()) assert.Equal(t, 6, tree.Root().Len()) // 02. Split position 2. tree = crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "r", nil), helper.TimeT(ctx)) - _, err = tree.EditT(0, 0, []*crdt.TreeNode{ + err = tree.EditT(0, 0, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "p", nil), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{ + err = tree.EditT(1, 1, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "ab"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

ab

", tree.ToXML()) assert.Equal(t, 4, tree.Root().Len()) - _, err = tree.EditT(2, 2, nil, 1, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(2, 2, nil, 1, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

a

b

", tree.ToXML()) assert.Equal(t, 6, tree.Root().Len()) // 03. Split position 3. tree = crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "r", nil), helper.TimeT(ctx)) - _, err = tree.EditT(0, 0, []*crdt.TreeNode{ + err = tree.EditT(0, 0, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "p", nil), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{ + err = tree.EditT(1, 1, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "ab"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

ab

", tree.ToXML()) assert.Equal(t, 4, tree.Root().Len()) - _, err = tree.EditT(3, 3, nil, 1, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(3, 3, nil, 1, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

ab

", tree.ToXML()) assert.Equal(t, 6, tree.Root().Len()) @@ -622,38 +622,38 @@ func TestTreeSplit(t *testing.T) { // 01. Split nodes level 1. tree := crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "r", nil), helper.TimeT(ctx)) - _, err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "b", nil)}, 0, + err = tree.EditT(1, 1, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "b", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(2, 2, []*crdt.TreeNode{ + err = tree.EditT(2, 2, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "ab"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

ab

", tree.ToXML()) assert.Equal(t, 6, tree.Root().Len()) - _, err = tree.EditT(3, 3, nil, 1, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(3, 3, nil, 1, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

ab

", tree.ToXML()) assert.Equal(t, 8, tree.Root().Len()) // 02. Split nodes level 2. tree = crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "r", nil), helper.TimeT(ctx)) - _, err = tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err = tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "b", nil)}, 0, + err = tree.EditT(1, 1, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "b", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(2, 2, []*crdt.TreeNode{ + err = tree.EditT(2, 2, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "ab"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

ab

", tree.ToXML()) assert.Equal(t, 6, tree.Root().Len()) - _, err = tree.EditT(3, 3, nil, 2, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(3, 3, nil, 2, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

a

b

", tree.ToXML()) assert.Equal(t, 10, tree.Root().Len()) @@ -663,10 +663,10 @@ func TestTreeSplit(t *testing.T) { ctx := helper.TextChangeContext(helper.TestRoot()) tree := crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "r", nil), helper.TimeT(ctx)) - _, err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{ + err = tree.EditT(1, 1, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "abcd"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) @@ -674,12 +674,12 @@ func TestTreeSplit(t *testing.T) { // 0 1 2 3 4 5 6 7 8 //

a b

c d

- _, err = tree.EditT(3, 3, nil, 1, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(3, 3, nil, 1, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

ab

cd

", tree.ToXML()) assert.Equal(t, 8, tree.Root().Len()) - _, err = tree.EditT(3, 5, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) + err = tree.EditT(3, 5, nil, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

abcd

", tree.ToXML()) assert.Equal(t, 6, tree.Root().Len()) @@ -690,47 +690,47 @@ func TestTreeMerge(t *testing.T) { t.Run("delete nodes in a multi-level range test", func(t *testing.T) { ctx := helper.TextChangeContext(helper.TestRoot()) tree := crdt.NewTree(crdt.NewTreeNode(helper.PosT(ctx), "root", nil), helper.TimeT(ctx)) - _, err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err := tree.EditT(0, 0, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(1, 1, []*crdt.TreeNode{ + err = tree.EditT(1, 1, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "ab"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(3, 3, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err = tree.EditT(3, 3, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(4, 4, []*crdt.TreeNode{ + err = tree.EditT(4, 4, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "x"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(7, 7, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err = tree.EditT(7, 7, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(8, 8, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err = tree.EditT(8, 8, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(9, 9, []*crdt.TreeNode{ + err = tree.EditT(9, 9, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "cd"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(13, 13, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err = tree.EditT(13, 13, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(14, 14, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, + err = tree.EditT(14, 14, []*crdt.TreeNode{crdt.NewTreeNode(helper.PosT(ctx), "p", nil)}, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(15, 15, []*crdt.TreeNode{ + err = tree.EditT(15, 15, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "y"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) - _, err = tree.EditT(17, 17, []*crdt.TreeNode{ + err = tree.EditT(17, 17, []*crdt.TreeNode{ crdt.NewTreeNode(helper.PosT(ctx), "text", nil, "ef"), }, 0, helper.TimeT(ctx), issueTimeTicket(ctx)) assert.NoError(t, err) assert.Equal(t, "

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

", tree.ToXML()) }) diff --git a/pkg/document/document_test.go b/pkg/document/document_test.go index 4683b42fb..1046efd53 100644 --- a/pkg/document/document_test.go +++ b/pkg/document/document_test.go @@ -525,189 +525,3 @@ func TestDocument(t *testing.T) { assert.Equal(t, 0, doc.GarbageLen()) }) } - -func TestTreeNodeAndAttrGC(t *testing.T) { - type opCode int - const ( - NoOp opCode = iota - Style - RemoveStyle - DeleteNode - GC - ) - - type operation struct { - code opCode - key string - val string - } - - type step struct { - op operation - garbageLen int - expectXML string - } - - tests := []struct { - desc string - steps []step - }{ - { - desc: "style-style test", - steps: []step{ - {operation{Style, "b", "t"}, 0, `

`}, - {operation{Style, "b", "f"}, 0, `

`}, - }, - }, - { - desc: "style-remove test", - steps: []step{ - {operation{Style, "b", "t"}, 0, `

`}, - {operation{RemoveStyle, "b", ""}, 1, `

`}, - }, - }, - { - desc: "remove-style test", - steps: []step{ - {operation{RemoveStyle, "b", ""}, 1, `

`}, - {operation{Style, "b", "t"}, 0, `

`}, - }, - }, - { - desc: "remove-remove test", - steps: []step{ - {operation{RemoveStyle, "b", ""}, 1, `

`}, - {operation{RemoveStyle, "b", ""}, 1, `

`}, - }, - }, - { - desc: "style-delete test", - steps: []step{ - {operation{Style, "b", "t"}, 0, `

`}, - {operation{DeleteNode, "", ""}, 1, ``}, - }, - }, - { - desc: "remove-delete test", - steps: []step{ - {operation{RemoveStyle, "b", ""}, 1, `

`}, - {operation{DeleteNode, "b", "t"}, 2, ``}, - }, - }, - { - desc: "remove-gc-delete test", - steps: []step{ - {operation{RemoveStyle, "b", ""}, 1, `

`}, - {operation{GC, "", ""}, 0, `

`}, - {operation{DeleteNode, "b", "t"}, 1, ``}, - }, - }, - } - - for i, tc := range tests { - t.Run(fmt.Sprintf("%d. %s", i+1, tc.desc), func(t *testing.T) { - // 01. Initial:

- doc := document.New("doc") - err := doc.Update(func(root *json.Object, p *presence.Presence) error { - root.SetNewTree("t", &json.TreeNode{ - Type: "r", - Children: []json.TreeNode{{Type: "p"}}, - }) - return nil - }) - assert.NoError(t, err) - assert.Equal(t, "

", doc.Root().GetTree("t").ToXML()) - assert.Equal(t, 0, doc.GarbageLen()) - - // 02. Run test steps - for _, s := range tc.steps { - assert.NoError(t, doc.Update(func(root *json.Object, p *presence.Presence) error { - if s.op.code == RemoveStyle { - root.GetTree("t").RemoveStyle(0, 1, []string{s.op.key}) - } else if s.op.code == Style { - root.GetTree("t").Style(0, 1, map[string]string{s.op.key: s.op.val}) - } else if s.op.code == DeleteNode { - root.GetTree("t").Edit(0, 2, nil, 0) - } else if s.op.code == GC { - doc.GarbageCollect(time.MaxTicket) - } - return nil - })) - assert.Equal(t, s.expectXML, doc.Root().GetTree("t").ToXML()) - assert.Equal(t, s.garbageLen, doc.GarbageLen()) - } - - // 03. Garbage collect - doc.GarbageCollect(time.MaxTicket) - assert.Equal(t, 0, doc.GarbageLen()) - }) - } -} - -func TestTextNodeAndAttrGC(t *testing.T) { - type opCode int - const ( - NoOp opCode = iota - Style - DeleteNode - GC - ) - - type operation struct { - code opCode - key string - val string - } - - type step struct { - op operation - garbageLen int - expectXML string - } - - tests := []struct { - desc string - steps []step - }{ - { - desc: "style-style test", - steps: []step{ - {operation{Style, "b", "t"}, 0, `[{"attrs":{"b":"t"},"val":"AB"}]`}, - {operation{Style, "b", "f"}, 0, `[{"attrs":{"b":"f"},"val":"AB"}]`}, - }, - }, - } - - for i, tc := range tests { - t.Run(fmt.Sprintf("%d. %s", i+1, tc.desc), func(t *testing.T) { - doc := document.New("doc") - err := doc.Update(func(root *json.Object, p *presence.Presence) error { - root.SetNewText("t").Edit(0, 0, "AB") - return nil - }) - assert.NoError(t, err) - assert.Equal(t, `[{"val":"AB"}]`, doc.Root().GetText("t").Marshal()) - assert.Equal(t, 0, doc.GarbageLen()) - - // 02. Run test steps - for _, s := range tc.steps { - assert.NoError(t, doc.Update(func(root *json.Object, p *presence.Presence) error { - if s.op.code == Style { - root.GetText("t").Style(0, 2, map[string]string{s.op.key: s.op.val}) - } else if s.op.code == DeleteNode { - root.GetText("t").Edit(0, 2, "") - } else if s.op.code == GC { - doc.GarbageCollect(time.MaxTicket) - } - return nil - })) - assert.Equal(t, s.expectXML, doc.Root().GetText("t").Marshal()) - assert.Equal(t, s.garbageLen, doc.GarbageLen()) - } - - // 03. Garbage collect - doc.GarbageCollect(time.MaxTicket) - assert.Equal(t, 0, doc.GarbageLen()) - }) - } -} diff --git a/pkg/document/gc_test.go b/pkg/document/gc_test.go new file mode 100644 index 000000000..df63796c6 --- /dev/null +++ b/pkg/document/gc_test.go @@ -0,0 +1,222 @@ +/* + * Copyright 2020 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package document_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/yorkie-team/yorkie/pkg/document" + "github.com/yorkie-team/yorkie/pkg/document/json" + "github.com/yorkie-team/yorkie/pkg/document/presence" + "github.com/yorkie-team/yorkie/pkg/document/time" +) + +func TestTreeGC(t *testing.T) { + type opCode int + const ( + NoOp opCode = iota + Style + RemoveStyle + DeleteNode + GC + ) + + type operation struct { + code opCode + key string + val string + } + + type step struct { + op operation + garbageLen int + expectXML string + } + + tests := []struct { + desc string + steps []step + }{ + { + desc: "style-style test", + steps: []step{ + {operation{Style, "b", "t"}, 0, `

`}, + {operation{Style, "b", "f"}, 0, `

`}, + }, + }, + { + desc: "style-remove test", + steps: []step{ + {operation{Style, "b", "t"}, 0, `

`}, + {operation{RemoveStyle, "b", ""}, 1, `

`}, + }, + }, + { + desc: "remove-style test", + steps: []step{ + {operation{RemoveStyle, "b", ""}, 1, `

`}, + {operation{Style, "b", "t"}, 0, `

`}, + }, + }, + { + desc: "remove-remove test", + steps: []step{ + {operation{RemoveStyle, "b", ""}, 1, `

`}, + {operation{RemoveStyle, "b", ""}, 1, `

`}, + }, + }, + { + desc: "style-delete test", + steps: []step{ + {operation{Style, "b", "t"}, 0, `

`}, + {operation{DeleteNode, "", ""}, 1, ``}, + }, + }, + { + desc: "remove-delete test", + steps: []step{ + {operation{RemoveStyle, "b", ""}, 1, `

`}, + {operation{DeleteNode, "b", "t"}, 2, ``}, + }, + }, + { + desc: "remove-gc-delete test", + steps: []step{ + {operation{RemoveStyle, "b", ""}, 1, `

`}, + {operation{GC, "", ""}, 0, `

`}, + {operation{DeleteNode, "b", "t"}, 1, ``}, + }, + }, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("%d. %s", i+1, tc.desc), func(t *testing.T) { + // 01. Initial:

+ doc := document.New("doc") + err := doc.Update(func(root *json.Object, p *presence.Presence) error { + root.SetNewTree("t", &json.TreeNode{ + Type: "r", + Children: []json.TreeNode{{Type: "p"}}, + }) + return nil + }) + assert.NoError(t, err) + assert.Equal(t, "

", doc.Root().GetTree("t").ToXML()) + assert.Equal(t, 0, doc.GarbageLen()) + + // 02. Run test steps + for _, s := range tc.steps { + assert.NoError(t, doc.Update(func(root *json.Object, p *presence.Presence) error { + if s.op.code == RemoveStyle { + root.GetTree("t").RemoveStyle(0, 1, []string{s.op.key}) + } else if s.op.code == Style { + root.GetTree("t").Style(0, 1, map[string]string{s.op.key: s.op.val}) + } else if s.op.code == DeleteNode { + root.GetTree("t").Edit(0, 2, nil, 0) + } else if s.op.code == GC { + doc.GarbageCollect(time.MaxTicket) + } + return nil + })) + assert.Equal(t, s.expectXML, doc.Root().GetTree("t").ToXML()) + assert.Equal(t, s.garbageLen, doc.GarbageLen()) + } + + // 03. Garbage collect + doc.GarbageCollect(time.MaxTicket) + assert.Equal(t, 0, doc.GarbageLen()) + }) + } +} + +func TestTextGC(t *testing.T) { + type opCode int + const ( + NoOp opCode = iota + Style + DeleteNode + GC + ) + + type operation struct { + code opCode + key string + val string + } + + type step struct { + op operation + garbageLen int + expectXML string + } + + tests := []struct { + desc string + steps []step + }{ + { + desc: "style-style test", + steps: []step{ + {operation{Style, "b", "t"}, 0, `[{"attrs":{"b":"t"},"val":"AB"}]`}, + {operation{Style, "b", "f"}, 0, `[{"attrs":{"b":"f"},"val":"AB"}]`}, + }, + }, + { + desc: "style-delete test", + steps: []step{ + {operation{Style, "b", "t"}, 0, `[{"attrs":{"b":"t"},"val":"AB"}]`}, + {operation{DeleteNode, "", ""}, 1, `[]`}, + }, + }, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("%d. %s", i+1, tc.desc), func(t *testing.T) { + doc := document.New("doc") + err := doc.Update(func(root *json.Object, p *presence.Presence) error { + root.SetNewText("t").Edit(0, 0, "AB") + return nil + }) + assert.NoError(t, err) + assert.Equal(t, `[{"val":"AB"}]`, doc.Root().GetText("t").Marshal()) + assert.Equal(t, 0, doc.GarbageLen()) + + // 02. Run test steps + for _, s := range tc.steps { + assert.NoError(t, doc.Update(func(root *json.Object, p *presence.Presence) error { + if s.op.code == Style { + root.GetText("t").Style(0, 2, map[string]string{s.op.key: s.op.val}) + } else if s.op.code == DeleteNode { + root.GetText("t").Edit(0, 2, "") + } else if s.op.code == GC { + doc.GarbageCollect(time.MaxTicket) + } + return nil + })) + assert.Equal(t, s.expectXML, doc.Root().GetText("t").Marshal()) + assert.Equal(t, s.garbageLen, doc.GarbageLen()) + } + + // 03. Garbage collect + doc.GarbageCollect(time.MaxTicket) + assert.Equal(t, 0, doc.GarbageLen()) + }) + } +} diff --git a/pkg/document/json/text.go b/pkg/document/json/text.go index ae5ad46e4..2420275ea 100644 --- a/pkg/document/json/text.go +++ b/pkg/document/json/text.go @@ -51,7 +51,12 @@ func (p *Text) CreateRange(from, to int) (*crdt.RGATreeSplitNodePos, *crdt.RGATr } // Edit edits the given range with the given content and attributes. -func (p *Text) Edit(from, to int, content string, attributes ...map[string]string) *Text { +func (p *Text) Edit( + from, + to int, + content string, + attributes ...map[string]string, +) *Text { if from > to { panic("from should be less than or equal to to") } @@ -68,7 +73,7 @@ func (p *Text) Edit(from, to int, content string, attributes ...map[string]strin } ticket := p.context.IssueTimeTicket() - _, maxCreationMapByActor, err := p.Text.Edit( + _, maxCreationMapByActor, pairs, err := p.Text.Edit( fromPos, toPos, nil, @@ -80,6 +85,10 @@ func (p *Text) Edit(from, to int, content string, attributes ...map[string]strin panic(err) } + for _, pair := range pairs { + p.context.RegisterGCPair(pair) + } + p.context.Push(operations.NewEdit( p.CreatedAt(), fromPos, @@ -89,9 +98,6 @@ func (p *Text) Edit(from, to int, content string, attributes ...map[string]strin attrs, ticket, )) - if !fromPos.Equal(toPos) { - p.context.RegisterElementHasRemovedNodes(p) - } return p } diff --git a/pkg/document/json/tree.go b/pkg/document/json/tree.go index 76e1c1880..4f356d142 100644 --- a/pkg/document/json/tree.go +++ b/pkg/document/json/tree.go @@ -345,7 +345,7 @@ func (t *Tree) edit(fromPos, toPos *crdt.TreePos, contents []*TreeNode, splitLev } ticket = t.context.LastTimeTicket() - maxCreationMapByActor, err := t.Tree.Edit( + maxCreationMapByActor, pairs, err := t.Tree.Edit( fromPos, toPos, clones, @@ -358,6 +358,10 @@ func (t *Tree) edit(fromPos, toPos *crdt.TreePos, contents []*TreeNode, splitLev panic(err) } + for _, pair := range pairs { + t.context.RegisterGCPair(pair) + } + t.context.Push(operations.NewTreeEdit( t.CreatedAt(), fromPos, @@ -368,10 +372,6 @@ func (t *Tree) edit(fromPos, toPos *crdt.TreePos, contents []*TreeNode, splitLev ticket, )) - if !fromPos.Equals(toPos) { - t.context.RegisterElementHasRemovedNodes(t.Tree) - } - return true } diff --git a/pkg/document/operations/edit.go b/pkg/document/operations/edit.go index b892f3132..3f8f830f1 100644 --- a/pkg/document/operations/edit.go +++ b/pkg/document/operations/edit.go @@ -75,12 +75,13 @@ func (e *Edit) Execute(root *crdt.Root) error { switch obj := parent.(type) { case *crdt.Text: - _, _, err := obj.Edit(e.from, e.to, e.maxCreatedAtMapByActor, e.content, e.attributes, e.executedAt) + _, _, pairs, err := obj.Edit(e.from, e.to, e.maxCreatedAtMapByActor, e.content, e.attributes, e.executedAt) if err != nil { return err } - if !e.from.Equal(e.to) { - root.RegisterElementHasRemovedNodes(obj) + + for _, pair := range pairs { + root.RegisterGCPair(pair) } default: return ErrNotApplicableDataType diff --git a/pkg/document/operations/tree_edit.go b/pkg/document/operations/tree_edit.go index 629c7411e..c81b016cf 100644 --- a/pkg/document/operations/tree_edit.go +++ b/pkg/document/operations/tree_edit.go @@ -89,7 +89,7 @@ func (e *TreeEdit) Execute(root *crdt.Root) error { } } - if _, err = obj.Edit( + _, pairs, err := obj.Edit( e.from, e.to, contents, @@ -117,13 +117,16 @@ func (e *TreeEdit) Execute(root *crdt.Root) error { } }(), e.maxCreatedAtMapByActor, - ); err != nil { + ) + if err != nil { return err } - if !e.from.Equals(e.to) { - root.RegisterElementHasRemovedNodes(obj) + for _, pair := range pairs { + root.RegisterGCPair(pair) + } + default: return ErrNotApplicableDataType } diff --git a/server/packs/packs.go b/server/packs/packs.go index 6015dbf68..58d952191 100644 --- a/server/packs/packs.go +++ b/server/packs/packs.go @@ -245,7 +245,7 @@ func BuildDocumentForServerSeq( "after apply %d changes: elements: %d removeds: %d, %s", len(changes), doc.Root().ElementMapLen(), - doc.Root().RemovedElementLen(), + doc.Root().GarbageElementLen(), doc.RootObject().Marshal(), ) } diff --git a/test/helper/helper.go b/test/helper/helper.go index 92d46ec2e..8644cb9ca 100644 --- a/test/helper/helper.go +++ b/test/helper/helper.go @@ -208,7 +208,7 @@ func createTreeNodePairs(node *crdt.TreeNode, parentID *crdt.TreeNodeID) []treeN pairs = append(pairs, treeNodePair{node, parentID}) for _, child := range node.Index.Children(true) { - pairs = append(pairs, createTreeNodePairs(child.Value, node.ID)...) + pairs = append(pairs, createTreeNodePairs(child.Value, node.ID())...) } return pairs }