From 28cbd37a96ca081759d9c8a7a9d30d3649e62bc5 Mon Sep 17 00:00:00 2001
From: Youngteac Hong
Date: Fri, 17 May 2024 20:56:43 +0900
Subject: [PATCH] Apply GCPair to TreeNode, TextNode (#866)
This commit is a follow-up task of #864, applying GCPair for TextNode
and TreeNode. Additionally, when generating a document from the root,
this commit registers GCPair in the cache.
---
api/converter/from_pb.go | 3 +-
api/converter/to_bytes.go | 4 +-
pkg/document/change/context.go | 5 -
pkg/document/crdt/element.go | 7 -
pkg/document/crdt/gc.go | 2 +-
pkg/document/crdt/rga_tree_split.go | 66 ++++----
pkg/document/crdt/rht.go | 8 +-
pkg/document/crdt/root.go | 100 +++++-------
pkg/document/crdt/root_test.go | 92 +++++------
pkg/document/crdt/text.go | 50 ++++--
pkg/document/crdt/text_test.go | 8 +-
pkg/document/crdt/tree.go | 233 ++++++++++++++++-----------
pkg/document/crdt/tree_test.go | 158 +++++++++---------
pkg/document/document_test.go | 186 ---------------------
pkg/document/gc_test.go | 222 +++++++++++++++++++++++++
pkg/document/json/text.go | 16 +-
pkg/document/json/tree.go | 10 +-
pkg/document/operations/edit.go | 7 +-
pkg/document/operations/tree_edit.go | 11 +-
server/packs/packs.go | 2 +-
test/helper/helper.go | 2 +-
21 files changed, 633 insertions(+), 559 deletions(-)
create mode 100644 pkg/document/gc_test.go
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
}