diff --git a/pkg/document/crdt/root.go b/pkg/document/crdt/root.go index 73db7de88..9f338b804 100644 --- a/pkg/document/crdt/root.go +++ b/pkg/document/crdt/root.go @@ -157,7 +157,7 @@ func (r *Root) GarbageCollect(ticket *time.Ticket) (int, error) { return 0, err } - if purgedNodes > 0 { + if node.removedNodesLen() == 0 { delete(r.elementHasRemovedNodesSetByCreatedAt, node.CreatedAt().Key()) } count += purgedNodes diff --git a/test/integration/gc_test.go b/test/integration/gc_test.go index 0c972d5bf..9f5bae462 100644 --- a/test/integration/gc_test.go +++ b/test/integration/gc_test.go @@ -452,4 +452,154 @@ func TestGarbageCollection(t *testing.T) { assert.Equal(t, 5, d1.GarbageCollect(time.MaxTicket)) }) + t.Run("Should work properly when there are multiple nodes to be collected in text type", func(t *testing.T) { + ctx := context.Background() + d1 := document.New(helper.TestDocKey(t)) + err := c1.Attach(ctx, d1) + assert.NoError(t, err) + + d2 := document.New(helper.TestDocKey(t)) + err = c2.Attach(ctx, d2) + assert.NoError(t, err) + + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.SetNewText("text").Edit(0, 0, "z") + return nil + }) + assert.NoError(t, err) + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.GetText("text").Edit(0, 1, "a") + return nil + }) + assert.NoError(t, err) + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.GetText("text").Edit(1, 1, "b") + return nil + }) + assert.NoError(t, err) + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.GetText("text").Edit(2, 2, "d") + return nil + }) + assert.NoError(t, err) + err = c1.Sync(ctx) + assert.NoError(t, err) + err = c2.Sync(ctx) + assert.NoError(t, err) + + assert.Equal(t, `{"text":[{"val":"a"},{"val":"b"},{"val":"d"}]}`, d1.Marshal()) + assert.Equal(t, `{"text":[{"val":"a"},{"val":"b"},{"val":"d"}]}`, d2.Marshal()) + assert.Equal(t, 1, d1.GarbageLen()) // z + + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.GetText("text").Edit(2, 2, "c") + return nil + }) + assert.NoError(t, err) + err = c1.Sync(ctx) + assert.NoError(t, err) + err = c2.Sync(ctx) + assert.NoError(t, err) + err = c2.Sync(ctx) + assert.NoError(t, err) + assert.Equal(t, `{"text":[{"val":"a"},{"val":"b"},{"val":"c"},{"val":"d"}]}`, d1.Marshal()) + assert.Equal(t, `{"text":[{"val":"a"},{"val":"b"},{"val":"c"},{"val":"d"}]}`, d2.Marshal()) + + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.GetText("text").Edit(1, 3, "") + return nil + }) + assert.NoError(t, err) + err = c1.Sync(ctx) + assert.NoError(t, err) + assert.Equal(t, `{"text":[{"val":"a"},{"val":"d"}]}`, d1.Marshal()) + assert.Equal(t, 2, d1.GarbageLen()) // b,c + + err = c2.Sync(ctx) + assert.NoError(t, err) + err = c2.Sync(ctx) + assert.NoError(t, err) + err = c1.Sync(ctx) + assert.NoError(t, err) + assert.Equal(t, `{"text":[{"val":"a"},{"val":"d"}]}`, d2.Marshal()) + assert.Equal(t, 0, d1.GarbageLen()) + }) + + t.Run("Should work properly when there are multiple nodes to be collected in tree type", func(t *testing.T) { + ctx := context.Background() + d1 := document.New(helper.TestDocKey(t)) + err := c1.Attach(ctx, d1) + assert.NoError(t, err) + + d2 := document.New(helper.TestDocKey(t)) + err = c2.Attach(ctx, d2) + assert.NoError(t, err) + + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.SetNewTree("tree", &json.TreeNode{ + Type: "r", + Children: []json.TreeNode{{ + Type: "text", Value: "z", + }}, + }) + return nil + }) + assert.NoError(t, err) + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.GetTree("tree").EditByPath([]int{0}, []int{1}, &json.TreeNode{Type: "text", Value: "a"}, 0) + return nil + }) + assert.NoError(t, err) + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.GetTree("tree").EditByPath([]int{1}, []int{1}, &json.TreeNode{Type: "text", Value: "b"}, 0) + return nil + }) + assert.NoError(t, err) + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.GetTree("tree").EditByPath([]int{2}, []int{2}, &json.TreeNode{Type: "text", Value: "d"}, 0) + return nil + }) + assert.NoError(t, err) + err = c1.Sync(ctx) + assert.NoError(t, err) + err = c2.Sync(ctx) + assert.NoError(t, err) + + assert.Equal(t, `abd`, d1.Root().GetTree("tree").ToXML()) + assert.Equal(t, `abd`, d2.Root().GetTree("tree").ToXML()) + assert.Equal(t, 1, d1.GarbageLen()) // z + + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.GetTree("tree").EditByPath([]int{2}, []int{2}, &json.TreeNode{Type: "text", Value: "c"}, 0) + return nil + }) + assert.NoError(t, err) + err = c1.Sync(ctx) + assert.NoError(t, err) + err = c2.Sync(ctx) + assert.NoError(t, err) + err = c2.Sync(ctx) + assert.NoError(t, err) + assert.Equal(t, `abcd`, d1.Root().GetTree("tree").ToXML()) + assert.Equal(t, `abcd`, d2.Root().GetTree("tree").ToXML()) + + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.GetTree("tree").EditByPath([]int{1}, []int{3}, nil, 0) + return nil + }) + assert.NoError(t, err) + err = c1.Sync(ctx) + assert.NoError(t, err) + assert.Equal(t, `ad`, d1.Root().GetTree("tree").ToXML()) + assert.Equal(t, 2, d1.GarbageLen()) // b,c + + err = c2.Sync(ctx) + assert.NoError(t, err) + err = c2.Sync(ctx) + assert.NoError(t, err) + err = c1.Sync(ctx) + assert.NoError(t, err) + assert.Equal(t, `ad`, d2.Root().GetTree("tree").ToXML()) + assert.Equal(t, 0, d1.GarbageLen()) + }) }