diff --git a/api/converter/from_bytes.go b/api/converter/from_bytes.go index 8e3e41abd..2ba4c24e7 100644 --- a/api/converter/from_bytes.go +++ b/api/converter/from_bytes.go @@ -334,7 +334,7 @@ func fromTextNode( if err != nil { return nil, err } - textNode.Remove(removedAt, time.MaxTicket) + textNode.Remove(removedAt, time.MaxTicket, time.MaxLamport) } return textNode, nil } diff --git a/pkg/document/change/change.go b/pkg/document/change/change.go index 0bdbf02ed..54cb162de 100644 --- a/pkg/document/change/change.go +++ b/pkg/document/change/change.go @@ -54,7 +54,7 @@ func New(id ID, message string, operations []operations.Operation, p *innerprese // Execute applies this change to the given JSON root. func (c *Change) Execute(root *crdt.Root, presences *innerpresence.Map) error { for _, op := range c.operations { - if err := op.Execute(root); err != nil { + if err := op.Execute(root, c.ID().versionVector); err != nil { return err } } diff --git a/pkg/document/crdt/rga_tree_split.go b/pkg/document/crdt/rga_tree_split.go index b2e9448b3..20726f71b 100644 --- a/pkg/document/crdt/rga_tree_split.go +++ b/pkg/document/crdt/rga_tree_split.go @@ -258,10 +258,18 @@ 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 { +func (s *RGATreeSplitNode[V]) Remove(removedAt *time.Ticket, + maxCreatedAt *time.Ticket, clientLamportAtChange int64) bool { justRemoved := s.removedAt == nil - if !s.createdAt().After(maxCreatedAt) && + var nodeExisted bool + if maxCreatedAt == nil { + nodeExisted = s.createdAt().Lamport() <= clientLamportAtChange + } else { + nodeExisted = !s.createdAt().After(maxCreatedAt) + } + + if nodeExisted && (s.removedAt == nil || removedAt.After(s.removedAt)) { s.removedAt = removedAt return justRemoved @@ -271,8 +279,16 @@ func (s *RGATreeSplitNode[V]) Remove(removedAt *time.Ticket, maxCreatedAt *time. } // canStyle checks if node is able to set style. -func (s *RGATreeSplitNode[V]) canStyle(editedAt *time.Ticket, maxCreatedAt *time.Ticket) bool { - return !s.createdAt().After(maxCreatedAt) && +func (s *RGATreeSplitNode[V]) canStyle(editedAt *time.Ticket, + maxCreatedAt *time.Ticket, clientLamportAtChange int64) bool { + var nodeExisted bool + if maxCreatedAt == nil { + nodeExisted = s.createdAt().Lamport() <= clientLamportAtChange + } else { + nodeExisted = !s.createdAt().After(maxCreatedAt) + } + + return nodeExisted && (s.removedAt == nil || editedAt.After(s.removedAt)) } @@ -451,6 +467,7 @@ func (s *RGATreeSplit[V]) edit( maxCreatedAtMapByActor map[string]*time.Ticket, content V, editedAt *time.Ticket, + versionVector time.VersionVector, ) (*RGATreeSplitNodePos, map[string]*time.Ticket, []GCPair, error) { // 01. Split nodes with from and to toLeft, toRight, err := s.findNodeWithSplit(to, editedAt) @@ -464,7 +481,7 @@ func (s *RGATreeSplit[V]) edit( // 02. delete between from and to nodesToDelete := s.findBetween(fromRight, toRight) - maxCreatedAtMap, removedNodes := s.deleteNodes(nodesToDelete, maxCreatedAtMapByActor, editedAt) + maxCreatedAtMap, removedNodes := s.deleteNodes(nodesToDelete, maxCreatedAtMapByActor, editedAt, versionVector) var caretID *RGATreeSplitNodeID if toRight == nil { @@ -506,6 +523,7 @@ func (s *RGATreeSplit[V]) deleteNodes( candidates []*RGATreeSplitNode[V], maxCreatedAtMapByActor map[string]*time.Ticket, editedAt *time.Ticket, + versionVector time.VersionVector, ) (map[string]*time.Ticket, map[string]*RGATreeSplitNode[V]) { createdAtMapByActor := make(map[string]*time.Ticket) removedNodeMap := make(map[string]*RGATreeSplitNode[V]) @@ -523,10 +541,20 @@ func (s *RGATreeSplit[V]) deleteNodes( for _, node := range candidates { actorIDHex := node.createdAt().ActorIDHex() + actorID := node.createdAt().ActorID() var maxCreatedAt *time.Ticket - if maxCreatedAtMapByActor == nil { - maxCreatedAt = time.MaxTicket + var clientLamportAtChange int64 + if versionVector == nil && maxCreatedAtMapByActor == nil { + // Local edit - use version vector comparison + clientLamportAtChange = time.MaxLamport + } else if versionVector != nil { + lamport, ok := versionVector.Get(actorID) + if ok { + clientLamportAtChange = lamport + } else { + clientLamportAtChange = 0 + } } else { createdAt, ok := maxCreatedAtMapByActor[actorIDHex] if ok { @@ -536,7 +564,9 @@ func (s *RGATreeSplit[V]) deleteNodes( } } - if node.Remove(editedAt, maxCreatedAt) { + // TODO(chacha912): maxCreatedAt can be removed after all legacy Changes + // (without version vector) are migrated to new Changes with version vector. + if node.Remove(editedAt, maxCreatedAt, clientLamportAtChange) { maxCreatedAt := createdAtMapByActor[actorIDHex] createdAt := node.id.createdAt if maxCreatedAt == nil || createdAt.After(maxCreatedAt) { diff --git a/pkg/document/crdt/root_test.go b/pkg/document/crdt/root_test.go index 18d499434..4311b9434 100644 --- a/pkg/document/crdt/root_test.go +++ b/pkg/document/crdt/root_test.go @@ -65,28 +65,28 @@ func TestRoot(t *testing.T) { text := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket()) fromPos, toPos, _ := text.CreateRange(0, 0) - _, _, pairs, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket()) + _, _, pairs, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket(), nil) assert.NoError(t, err) registerGCPairs(root, pairs) assert.Equal(t, "Hello World", text.String()) assert.Equal(t, 0, root.GarbageLen()) fromPos, toPos, _ = text.CreateRange(5, 10) - _, _, pairs, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket()) + _, _, pairs, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket(), nil) assert.NoError(t, err) registerGCPairs(root, pairs) assert.Equal(t, "HelloYorkied", text.String()) assert.Equal(t, 1, root.GarbageLen()) fromPos, toPos, _ = text.CreateRange(0, 5) - _, _, pairs, err = text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket()) + _, _, pairs, err = text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket(), nil) assert.NoError(t, err) registerGCPairs(root, pairs) assert.Equal(t, "Yorkied", text.String()) assert.Equal(t, 2, root.GarbageLen()) fromPos, toPos, _ = text.CreateRange(6, 7) - _, _, pairs, err = text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket()) + _, _, pairs, err = text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket(), nil) assert.NoError(t, err) registerGCPairs(root, pairs) assert.Equal(t, "Yorkie", text.String()) @@ -125,7 +125,7 @@ func TestRoot(t *testing.T) { for _, tc := range steps { fromPos, toPos, _ := text.CreateRange(tc.from, tc.to) - _, _, pairs, err := text.Edit(fromPos, toPos, nil, tc.content, nil, ctx.IssueTimeTicket()) + _, _, pairs, err := text.Edit(fromPos, toPos, nil, tc.content, nil, ctx.IssueTimeTicket(), nil) assert.NoError(t, err) registerGCPairs(root, pairs) assert.Equal(t, tc.want, text.String()) @@ -157,7 +157,7 @@ func TestRoot(t *testing.T) { for _, tc := range steps { fromPos, toPos, _ := text.CreateRange(tc.from, tc.to) - _, _, pairs, err := text.Edit(fromPos, toPos, nil, tc.content, nil, ctx.IssueTimeTicket()) + _, _, pairs, err := text.Edit(fromPos, toPos, nil, tc.content, nil, ctx.IssueTimeTicket(), nil) assert.NoError(t, err) registerGCPairs(root, pairs) assert.Equal(t, tc.want, text.String()) @@ -176,21 +176,21 @@ func TestRoot(t *testing.T) { text := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket()) fromPos, toPos, _ := text.CreateRange(0, 0) - _, _, pairs, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket()) + _, _, pairs, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket(), nil) assert.NoError(t, err) registerGCPairs(root, pairs) assert.Equal(t, `[{"val":"Hello World"}]`, text.Marshal()) assert.Equal(t, 0, root.GarbageLen()) fromPos, toPos, _ = text.CreateRange(6, 11) - _, _, pairs, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket()) + _, _, pairs, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket(), nil) assert.NoError(t, err) registerGCPairs(root, pairs) assert.Equal(t, `[{"val":"Hello "},{"val":"Yorkie"}]`, text.Marshal()) assert.Equal(t, 1, root.GarbageLen()) fromPos, toPos, _ = text.CreateRange(0, 6) - _, _, pairs, err = text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket()) + _, _, pairs, err = text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket(), nil) assert.NoError(t, err) registerGCPairs(root, pairs) assert.Equal(t, `[{"val":"Yorkie"}]`, text.Marshal()) diff --git a/pkg/document/crdt/text.go b/pkg/document/crdt/text.go index 09f3520f3..18653e80e 100644 --- a/pkg/document/crdt/text.go +++ b/pkg/document/crdt/text.go @@ -273,6 +273,7 @@ func (t *Text) Edit( content string, attributes map[string]string, executedAt *time.Ticket, + versionVector time.VersionVector, ) (*RGATreeSplitNodePos, map[string]*time.Ticket, []GCPair, error) { val := NewTextValue(content, NewRHT()) for key, value := range attributes { @@ -285,6 +286,7 @@ func (t *Text) Edit( maxCreatedAtMapByActor, val, executedAt, + versionVector, ) } @@ -295,6 +297,7 @@ func (t *Text) Style( maxCreatedAtMapByActor map[string]*time.Ticket, attributes map[string]string, executedAt *time.Ticket, + versionVector time.VersionVector, ) (map[string]*time.Ticket, []GCPair, error) { // 01. Split nodes with from and to _, toRight, err := t.rgaTreeSplit.findNodeWithSplit(to, executedAt) @@ -313,10 +316,20 @@ func (t *Text) Style( for _, node := range nodes { actorIDHex := node.id.createdAt.ActorIDHex() + actorID := node.id.createdAt.ActorID() var maxCreatedAt *time.Ticket - if len(maxCreatedAtMapByActor) == 0 { - maxCreatedAt = time.MaxTicket + var clientLamportAtChange int64 + if versionVector == nil && maxCreatedAtMapByActor == nil { + // Local edit - use version vector comparison + clientLamportAtChange = time.MaxLamport + } else if versionVector != nil { + lamport, ok := versionVector.Get(actorID) + if ok { + clientLamportAtChange = lamport + } else { + clientLamportAtChange = 0 + } } else { createdAt, ok := maxCreatedAtMapByActor[actorIDHex] if ok { @@ -326,8 +339,10 @@ func (t *Text) Style( } } - if node.canStyle(executedAt, maxCreatedAt) { - maxCreatedAt = createdAtMapByActor[actorIDHex] + // TODO(chacha912): maxCreatedAt can be removed after all legacy Changes + // (without version vector) are migrated to new Changes with version vector. + if node.canStyle(executedAt, maxCreatedAt, clientLamportAtChange) { + maxCreatedAt := createdAtMapByActor[actorIDHex] createdAt := node.id.createdAt if maxCreatedAt == nil || createdAt.After(maxCreatedAt) { createdAtMapByActor[actorIDHex] = createdAt diff --git a/pkg/document/crdt/text_test.go b/pkg/document/crdt/text_test.go index 474cf819b..c36f85285 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(), nil) 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(), nil) assert.NoError(t, err) assert.Equal(t, `[{"val":"Hello "},{"val":"Yorkie"}]`, text.Marshal()) }) @@ -70,17 +70,17 @@ 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(), nil) 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(), nil) assert.NoError(t, err) assert.Equal(t, `[{"val":"Hello "},{"val":"Yorkie"}]`, text.Marshal()) fromPos, toPos, _ = text.CreateRange(0, 1) - _, _, err = text.Style(fromPos, toPos, nil, map[string]string{"b": "1"}, ctx.IssueTimeTicket()) + _, _, err = text.Style(fromPos, toPos, nil, map[string]string{"b": "1"}, ctx.IssueTimeTicket(), nil) assert.NoError(t, err) assert.Equal( t, diff --git a/pkg/document/crdt/tree.go b/pkg/document/crdt/tree.go index 42454818b..3b84d82fa 100644 --- a/pkg/document/crdt/tree.go +++ b/pkg/document/crdt/tree.go @@ -381,20 +381,40 @@ func (n *TreeNode) remove(removedAt *time.Ticket) bool { return false } -func (n *TreeNode) canDelete(removedAt *time.Ticket, maxCreatedAt *time.Ticket) bool { - if !n.id.CreatedAt.After(maxCreatedAt) && +// TODO(chacha912): maxCreatedAt can be removed after all legacy Changes +// (without version vector) are migrated to new Changes with version vector. +func (n *TreeNode) canDelete(removedAt *time.Ticket, + maxCreatedAt *time.Ticket, clientLamportAtChange int64) bool { + var nodeExisted bool + if maxCreatedAt == nil { + nodeExisted = n.id.CreatedAt.Lamport() <= clientLamportAtChange + } else { + nodeExisted = !n.id.CreatedAt.After(maxCreatedAt) + } + + if nodeExisted && (n.removedAt == nil || n.removedAt.Compare(removedAt) > 0) { return true } return false } -func (n *TreeNode) canStyle(editedAt *time.Ticket, maxCreatedAt *time.Ticket) bool { +// TODO(chacha912): maxCreatedAt can be removed after all legacy Changes +// (without version vector) are migrated to new Changes with version vector. +func (n *TreeNode) canStyle(editedAt *time.Ticket, + maxCreatedAt *time.Ticket, clientLamportAtChange int64) bool { if n.IsText() { return false } - return !n.id.CreatedAt.After(maxCreatedAt) && + var nodeExisted bool + if maxCreatedAt == nil { + nodeExisted = n.id.CreatedAt.Lamport() <= clientLamportAtChange + } else { + nodeExisted = !n.id.CreatedAt.After(maxCreatedAt) + } + + return nodeExisted && (n.removedAt == nil || editedAt.After(n.removedAt)) } @@ -669,7 +689,7 @@ func (t *Tree) EditT( return err } - _, _, err = t.Edit(fromPos, toPos, contents, splitLevel, editedAt, issueTimeTicket, nil) + _, _, err = t.Edit(fromPos, toPos, contents, splitLevel, editedAt, issueTimeTicket, nil, nil) return err } @@ -717,6 +737,7 @@ func (t *Tree) Edit( editedAt *time.Ticket, issueTimeTicket func() *time.Ticket, maxCreatedAtMapByActor map[string]*time.Ticket, + versionVector time.VersionVector, ) (map[string]*time.Ticket, []GCPair, error) { // 01. find nodes from the given range and split nodes. fromParent, fromLeft, err := t.FindTreeNodesWithSplitText(from, editedAt) @@ -730,7 +751,7 @@ func (t *Tree) Edit( toBeRemoveds, toBeMovedToFromParents, maxCreatedAtMap, err := t.collectBetween( fromParent, fromLeft, toParent, toLeft, - maxCreatedAtMapByActor, editedAt, + maxCreatedAtMapByActor, editedAt, versionVector, ) if err != nil { return nil, nil, err @@ -816,6 +837,7 @@ func (t *Tree) collectBetween( fromParent *TreeNode, fromLeft *TreeNode, toParent *TreeNode, toLeft *TreeNode, maxCreatedAtMapByActor map[string]*time.Ticket, editedAt *time.Ticket, + versionVector time.VersionVector, ) ([]*TreeNode, []*TreeNode, map[string]*time.Ticket, error) { var toBeRemoveds []*TreeNode var toBeMovedToFromParents []*TreeNode @@ -843,10 +865,20 @@ func (t *Tree) collectBetween( } actorIDHex := node.id.CreatedAt.ActorIDHex() + actorID := node.id.CreatedAt.ActorID() var maxCreatedAt *time.Ticket - if maxCreatedAtMapByActor == nil { - maxCreatedAt = time.MaxTicket + var clientLamportAtChange int64 + if versionVector == nil && maxCreatedAtMapByActor == nil { + // Local edit - use version vector comparison + clientLamportAtChange = time.MaxLamport + } else if versionVector != nil { + lamport, ok := versionVector.Get(actorID) + if ok { + clientLamportAtChange = lamport + } else { + clientLamportAtChange = 0 + } } else { createdAt, ok := maxCreatedAtMapByActor[actorIDHex] if ok { @@ -858,8 +890,9 @@ func (t *Tree) collectBetween( // NOTE(sejongk): If the node is removable or its parent is going to // be removed, then this node should be removed. - if node.canDelete(editedAt, maxCreatedAt) || slices.Contains(toBeRemoveds, node.Index.Parent.Value) { - maxCreatedAt = createdAtMapByActor[actorIDHex] + if node.canDelete(editedAt, maxCreatedAt, clientLamportAtChange) || + slices.Contains(toBeRemoveds, node.Index.Parent.Value) { + maxCreatedAt := createdAtMapByActor[actorIDHex] createdAt := node.id.CreatedAt if maxCreatedAt == nil || createdAt.After(maxCreatedAt) { createdAtMapByActor[actorIDHex] = createdAt @@ -935,6 +968,7 @@ func (t *Tree) StyleByIndex( attributes map[string]string, editedAt *time.Ticket, maxCreatedAtMapByActor map[string]*time.Ticket, + versionVector time.VersionVector, ) (map[string]*time.Ticket, []GCPair, error) { fromPos, err := t.FindPos(start) if err != nil { @@ -946,7 +980,7 @@ func (t *Tree) StyleByIndex( return nil, nil, err } - return t.Style(fromPos, toPos, attributes, editedAt, maxCreatedAtMapByActor) + return t.Style(fromPos, toPos, attributes, editedAt, maxCreatedAtMapByActor, versionVector) } // Style applies the given attributes of the given range. @@ -955,6 +989,7 @@ func (t *Tree) Style( attrs map[string]string, editedAt *time.Ticket, maxCreatedAtMapByActor map[string]*time.Ticket, + versionVector time.VersionVector, ) (map[string]*time.Ticket, []GCPair, error) { fromParent, fromLeft, err := t.FindTreeNodesWithSplitText(from, editedAt) if err != nil { @@ -970,20 +1005,31 @@ func (t *Tree) Style( if err = t.traverseInPosRange(fromParent, fromLeft, toParent, toLeft, func(token index.TreeToken[*TreeNode], _ bool) { node := token.Node actorIDHex := node.id.CreatedAt.ActorIDHex() + actorID := node.id.CreatedAt.ActorID() var maxCreatedAt *time.Ticket - if maxCreatedAtMapByActor == nil { - maxCreatedAt = time.MaxTicket + var clientLamportAtChange int64 + if versionVector == nil && maxCreatedAtMapByActor == nil { + // Local edit - use version vector comparison + clientLamportAtChange = time.MaxLamport + } else if versionVector != nil { + lamport, ok := versionVector.Get(actorID) + if ok { + clientLamportAtChange = lamport + } else { + clientLamportAtChange = 0 + } } else { - if createdAt, ok := maxCreatedAtMapByActor[actorIDHex]; ok { + createdAt, ok := maxCreatedAtMapByActor[actorIDHex] + if ok { maxCreatedAt = createdAt } else { maxCreatedAt = time.InitialTicket } } - if node.canStyle(editedAt, maxCreatedAt) && len(attrs) > 0 { - maxCreatedAt = createdAtMapByActor[actorIDHex] + if node.canStyle(editedAt, maxCreatedAt, clientLamportAtChange) && len(attrs) > 0 { + maxCreatedAt := createdAtMapByActor[actorIDHex] createdAt := node.id.CreatedAt if maxCreatedAt == nil || createdAt.After(maxCreatedAt) { createdAtMapByActor[actorIDHex] = createdAt @@ -1012,6 +1058,7 @@ func (t *Tree) RemoveStyle( attrs []string, editedAt *time.Ticket, maxCreatedAtMapByActor map[string]*time.Ticket, + versionVector time.VersionVector, ) (map[string]*time.Ticket, []GCPair, error) { fromParent, fromLeft, err := t.FindTreeNodesWithSplitText(from, editedAt) if err != nil { @@ -1027,20 +1074,31 @@ func (t *Tree) RemoveStyle( if err = t.traverseInPosRange(fromParent, fromLeft, toParent, toLeft, func(token index.TreeToken[*TreeNode], _ bool) { node := token.Node actorIDHex := node.id.CreatedAt.ActorIDHex() + actorID := node.id.CreatedAt.ActorID() var maxCreatedAt *time.Ticket - if maxCreatedAtMapByActor == nil { - maxCreatedAt = time.MaxTicket + var clientLamportAtChange int64 + if versionVector == nil && maxCreatedAtMapByActor == nil { + // Local edit - use version vector comparison + clientLamportAtChange = time.MaxLamport + } else if versionVector != nil { + lamport, ok := versionVector.Get(actorID) + if ok { + clientLamportAtChange = lamport + } else { + clientLamportAtChange = 0 + } } else { - if createdAt, ok := maxCreatedAtMapByActor[actorIDHex]; ok { + createdAt, ok := maxCreatedAtMapByActor[actorIDHex] + if ok { maxCreatedAt = createdAt } else { maxCreatedAt = time.InitialTicket } } - if node.canStyle(editedAt, maxCreatedAt) && len(attrs) > 0 { - maxCreatedAt = createdAtMapByActor[actorIDHex] + if node.canStyle(editedAt, maxCreatedAt, clientLamportAtChange) && len(attrs) > 0 { + maxCreatedAt := createdAtMapByActor[actorIDHex] createdAt := node.id.CreatedAt if maxCreatedAt == nil || createdAt.After(maxCreatedAt) { createdAtMapByActor[actorIDHex] = createdAt diff --git a/pkg/document/crdt/tree_test.go b/pkg/document/crdt/tree_test.go index 3384fe727..56d4695a0 100644 --- a/pkg/document/crdt/tree_test.go +++ b/pkg/document/crdt/tree_test.go @@ -405,28 +405,28 @@ func TestTreeEdit(t *testing.T) { assert.Equal(t, "

ab

cd

", tree.ToXML()) // style attributes with opening tag - _, _, err = tree.StyleByIndex(0, 1, map[string]string{"weight": "bold"}, helper.TimeT(ctx), nil) + _, _, err = tree.StyleByIndex(0, 1, map[string]string{"weight": "bold"}, helper.TimeT(ctx), nil, nil) assert.NoError(t, err) assert.Equal(t, `

ab

cd

`, tree.ToXML()) // style attributes with closing tag - _, _, err = tree.StyleByIndex(3, 4, map[string]string{"color": "red"}, helper.TimeT(ctx), nil) + _, _, err = tree.StyleByIndex(3, 4, map[string]string{"color": "red"}, helper.TimeT(ctx), nil, nil) assert.NoError(t, err) assert.Equal(t, `

ab

cd

`, tree.ToXML()) // style attributes with the whole - _, _, err = tree.StyleByIndex(0, 4, map[string]string{"size": "small"}, helper.TimeT(ctx), nil) + _, _, err = tree.StyleByIndex(0, 4, map[string]string{"size": "small"}, helper.TimeT(ctx), nil, nil) assert.NoError(t, err) assert.Equal(t, `

ab

cd

`, tree.ToXML()) // 02. style attributes to elements. - _, _, err = tree.StyleByIndex(0, 5, map[string]string{"style": "italic"}, helper.TimeT(ctx), nil) + _, _, err = tree.StyleByIndex(0, 5, map[string]string{"style": "italic"}, helper.TimeT(ctx), nil, nil) assert.NoError(t, err) assert.Equal(t, `

ab

`+ `

cd

`, tree.ToXML()) // 03. Ignore styling attributes to text nodes. - _, _, err = tree.StyleByIndex(1, 3, map[string]string{"bold": "true"}, helper.TimeT(ctx), nil) + _, _, err = tree.StyleByIndex(1, 3, map[string]string{"bold": "true"}, helper.TimeT(ctx), nil, nil) assert.NoError(t, err) assert.Equal(t, `

ab

`+ `

cd

`, tree.ToXML()) diff --git a/pkg/document/json/text.go b/pkg/document/json/text.go index 2420275ea..3e51456e8 100644 --- a/pkg/document/json/text.go +++ b/pkg/document/json/text.go @@ -80,6 +80,7 @@ func (p *Text) Edit( content, attrs, ticket, + nil, ) if err != nil { panic(err) @@ -119,6 +120,7 @@ func (p *Text) Style(from, to int, attributes map[string]string) *Text { nil, attributes, ticket, + nil, ) if err != nil { panic(err) diff --git a/pkg/document/json/tree.go b/pkg/document/json/tree.go index faae3f807..65e0f147b 100644 --- a/pkg/document/json/tree.go +++ b/pkg/document/json/tree.go @@ -221,7 +221,7 @@ func (t *Tree) Style(fromIdx, toIdx int, attributes map[string]string) bool { } ticket := t.context.IssueTimeTicket() - maxCreationMapByActor, pairs, err := t.Tree.Style(fromPos, toPos, attributes, ticket, nil) + maxCreationMapByActor, pairs, err := t.Tree.Style(fromPos, toPos, attributes, ticket, nil, nil) if err != nil { panic(err) } @@ -262,7 +262,7 @@ func (t *Tree) RemoveStyle(fromIdx, toIdx int, attributesToRemove []string) bool } ticket := t.context.IssueTimeTicket() - maxCreationMapByActor, pairs, err := t.Tree.RemoveStyle(fromPos, toPos, attributesToRemove, ticket, nil) + maxCreationMapByActor, pairs, err := t.Tree.RemoveStyle(fromPos, toPos, attributesToRemove, ticket, nil, nil) if err != nil { panic(err) } @@ -354,6 +354,7 @@ func (t *Tree) edit(fromPos, toPos *crdt.TreePos, contents []*TreeNode, splitLev ticket, t.context.IssueTimeTicket, nil, + nil, ) if err != nil { panic(err) diff --git a/pkg/document/operations/add.go b/pkg/document/operations/add.go index 9ea18d5d8..de74c71bb 100644 --- a/pkg/document/operations/add.go +++ b/pkg/document/operations/add.go @@ -52,7 +52,7 @@ func NewAdd( } // Execute executes this operation on the given document(`root`). -func (o *Add) Execute(root *crdt.Root) error { +func (o *Add) Execute(root *crdt.Root, _ time.VersionVector) error { parent := root.FindByCreatedAt(o.parentCreatedAt) obj, ok := parent.(*crdt.Array) diff --git a/pkg/document/operations/array_set.go b/pkg/document/operations/array_set.go index 11842a435..aff880e9a 100644 --- a/pkg/document/operations/array_set.go +++ b/pkg/document/operations/array_set.go @@ -52,9 +52,8 @@ func NewArraySet( } // Execute executes this operation on the given document(`root`). -func (o *ArraySet) Execute(root *crdt.Root) error { +func (o *ArraySet) Execute(root *crdt.Root, _ time.VersionVector) error { parent := root.FindByCreatedAt(o.parentCreatedAt) - obj, ok := parent.(*crdt.Array) if !ok { return ErrNotApplicableDataType diff --git a/pkg/document/operations/edit.go b/pkg/document/operations/edit.go index 3f8f830f1..4b561b702 100644 --- a/pkg/document/operations/edit.go +++ b/pkg/document/operations/edit.go @@ -70,12 +70,13 @@ func NewEdit( } // Execute executes this operation on the given document(`root`). -func (e *Edit) Execute(root *crdt.Root) error { +func (e *Edit) Execute(root *crdt.Root, versionVector time.VersionVector) error { parent := root.FindByCreatedAt(e.parentCreatedAt) switch obj := parent.(type) { case *crdt.Text: - _, _, pairs, 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, versionVector) if err != nil { return err } diff --git a/pkg/document/operations/increase.go b/pkg/document/operations/increase.go index 5b3d126a6..36c3c4b46 100644 --- a/pkg/document/operations/increase.go +++ b/pkg/document/operations/increase.go @@ -43,7 +43,7 @@ func NewIncrease( } // Execute executes this operation on the given document(`root`). -func (o *Increase) Execute(root *crdt.Root) error { +func (o *Increase) Execute(root *crdt.Root, _ time.VersionVector) error { parent := root.FindByCreatedAt(o.parentCreatedAt) cnt, ok := parent.(*crdt.Counter) if !ok { diff --git a/pkg/document/operations/move.go b/pkg/document/operations/move.go index 5207b8ecf..077065be2 100644 --- a/pkg/document/operations/move.go +++ b/pkg/document/operations/move.go @@ -52,7 +52,7 @@ func NewMove( } // Execute executes this operation on the given document(`root`). -func (o *Move) Execute(root *crdt.Root) error { +func (o *Move) Execute(root *crdt.Root, _ time.VersionVector) error { parent := root.FindByCreatedAt(o.parentCreatedAt) obj, ok := parent.(*crdt.Array) diff --git a/pkg/document/operations/operation.go b/pkg/document/operations/operation.go index 8047f04e7..774b79844 100644 --- a/pkg/document/operations/operation.go +++ b/pkg/document/operations/operation.go @@ -34,7 +34,7 @@ var ( // Operation represents an operation to be executed on a document. type Operation interface { // Execute executes this operation on the given document(`root`). - Execute(root *crdt.Root) error + Execute(root *crdt.Root, versionVector time.VersionVector) error // ExecutedAt returns execution time of this operation. ExecutedAt() *time.Ticket diff --git a/pkg/document/operations/remove.go b/pkg/document/operations/remove.go index 4abf3cdcc..1c5446160 100644 --- a/pkg/document/operations/remove.go +++ b/pkg/document/operations/remove.go @@ -48,7 +48,7 @@ func NewRemove( } // Execute executes this operation on the given document(`root`). -func (o *Remove) Execute(root *crdt.Root) error { +func (o *Remove) Execute(root *crdt.Root, _ time.VersionVector) error { parentElem := root.FindByCreatedAt(o.parentCreatedAt) switch parent := parentElem.(type) { diff --git a/pkg/document/operations/set.go b/pkg/document/operations/set.go index 2e2a66ce2..19f3c2ff0 100644 --- a/pkg/document/operations/set.go +++ b/pkg/document/operations/set.go @@ -53,7 +53,7 @@ func NewSet( } // Execute executes this operation on the given document(`root`). -func (o *Set) Execute(root *crdt.Root) error { +func (o *Set) Execute(root *crdt.Root, _ time.VersionVector) error { parent := root.FindByCreatedAt(o.parentCreatedAt) obj, ok := parent.(*crdt.Object) diff --git a/pkg/document/operations/style.go b/pkg/document/operations/style.go index 14e9564c8..b66edaddf 100644 --- a/pkg/document/operations/style.go +++ b/pkg/document/operations/style.go @@ -63,14 +63,14 @@ func NewStyle( } // Execute executes this operation on the given document(`root`). -func (e *Style) Execute(root *crdt.Root) error { +func (e *Style) Execute(root *crdt.Root, versionVector time.VersionVector) error { parent := root.FindByCreatedAt(e.parentCreatedAt) obj, ok := parent.(*crdt.Text) if !ok { return ErrNotApplicableDataType } - _, pairs, err := obj.Style(e.from, e.to, e.maxCreatedAtMapByActor, e.attributes, e.executedAt) + _, pairs, err := obj.Style(e.from, e.to, e.maxCreatedAtMapByActor, e.attributes, e.executedAt, versionVector) if err != nil { return err } diff --git a/pkg/document/operations/tree_edit.go b/pkg/document/operations/tree_edit.go index c81b016cf..108849660 100644 --- a/pkg/document/operations/tree_edit.go +++ b/pkg/document/operations/tree_edit.go @@ -69,7 +69,7 @@ func NewTreeEdit( } // Execute executes this operation on the given `CRDTRoot`. -func (e *TreeEdit) Execute(root *crdt.Root) error { +func (e *TreeEdit) Execute(root *crdt.Root, versionVector time.VersionVector) error { parent := root.FindByCreatedAt(e.parentCreatedAt) switch obj := parent.(type) { @@ -117,6 +117,7 @@ func (e *TreeEdit) Execute(root *crdt.Root) error { } }(), e.maxCreatedAtMapByActor, + versionVector, ) if err != nil { return err diff --git a/pkg/document/operations/tree_style.go b/pkg/document/operations/tree_style.go index d642b2171..7b5676442 100644 --- a/pkg/document/operations/tree_style.go +++ b/pkg/document/operations/tree_style.go @@ -87,7 +87,7 @@ func NewTreeStyleRemove( } // Execute executes this operation on the given `CRDTRoot`. -func (e *TreeStyle) Execute(root *crdt.Root) error { +func (e *TreeStyle) Execute(root *crdt.Root, versionVector time.VersionVector) error { parent := root.FindByCreatedAt(e.parentCreatedAt) obj, ok := parent.(*crdt.Tree) if !ok { @@ -97,12 +97,13 @@ func (e *TreeStyle) Execute(root *crdt.Root) error { var pairs []crdt.GCPair var err error if len(e.attributes) > 0 { - _, pairs, err = obj.Style(e.from, e.to, e.attributes, e.executedAt, e.maxCreatedAtMapByActor) + _, pairs, err = obj.Style(e.from, e.to, e.attributes, e.executedAt, e.maxCreatedAtMapByActor, versionVector) if err != nil { return err } } else { - _, pairs, err = obj.RemoveStyle(e.from, e.to, e.attributesToRemove, e.executedAt, e.maxCreatedAtMapByActor) + _, pairs, err = obj.RemoveStyle(e.from, e.to, e.attributesToRemove, e.executedAt, + e.maxCreatedAtMapByActor, versionVector) if err != nil { return err } diff --git a/pkg/document/time/version_vector.go b/pkg/document/time/version_vector.go index 75741a965..6b1f4e944 100644 --- a/pkg/document/time/version_vector.go +++ b/pkg/document/time/version_vector.go @@ -35,6 +35,13 @@ func NewVersionVector() VersionVector { return make(VersionVector) } +// Get gets the version of the given actor. +// Returns the version and whether the actor exists in the vector. +func (v VersionVector) Get(id *ActorID) (int64, bool) { + version, exists := v[id.bytes] + return version, exists +} + // Set sets the given actor's version to the given value. func (v VersionVector) Set(id *ActorID, i int64) { v[id.bytes] = i diff --git a/test/integration/gc_test.go b/test/integration/gc_test.go index 4fc6195e6..357dd1820 100644 --- a/test/integration/gc_test.go +++ b/test/integration/gc_test.go @@ -1303,4 +1303,148 @@ func TestGarbageCollection(t *testing.T) { assert.Equal(t, 0, d1.GarbageLen()) assert.Equal(t, 0, d2.GarbageLen()) }) + + t.Run("attach > pushpull > detach lifecycle version vector test (run gc at last client detaches document)", func(t *testing.T) { + clients := activeClients(t, 2) + c1, c2 := clients[0], clients[1] + defer deactivateAndCloseClients(t, clients) + + ctx := context.Background() + d1 := document.New(helper.TestDocKey(t)) + assert.NoError(t, c1.Attach(ctx, d1)) + // d2.vv =[c1:1], minvv =[c1:1], db.vv {c1: [c1:1]} + assert.Equal(t, true, checkVV(d1.VersionVector(), versionOf(d1.ActorID(), 1))) + + d2 := document.New(helper.TestDocKey(t)) + assert.NoError(t, c2.Attach(ctx, d2)) + // d2.vv =[c1:1, c2:2], minvv =[c1:0, c2:0], db.vv {c1: [c1:1], c2: [c2:1]} + assert.Equal(t, true, checkVV(d2.VersionVector(), versionOf(d1.ActorID(), 1), versionOf(d2.ActorID(), 2))) + + err := d1.Update(func(root *json.Object, p *presence.Presence) error { + root.SetNewText("text").Edit(0, 0, "a").Edit(1, 1, "b").Edit(2, 2, "c") + return nil + }, "sets text") + // d1/vv = [c1:2] + assert.Equal(t, true, checkVV(d1.VersionVector(), versionOf(d1.ActorID(), 2))) + assert.NoError(t, err) + + assert.NoError(t, c1.Sync(ctx)) + // d1.vv = [c1:3, c2:1], minvv = [c1:0, c2:0], db.vv {c1: [c1:2], c2: [c2:1]} + assert.Equal(t, true, checkVV(d1.VersionVector(), versionOf(d1.ActorID(), 3), versionOf(d2.ActorID(), 1))) + + assert.NoError(t, c2.Sync(ctx)) + // d2.vv = [c1:2, c2:3], minvv = [c1:1, c2:0], db.vv {c1: [c1:2], c2: [c1:1, c2:2]} + assert.Equal(t, true, checkVV(d2.VersionVector(), versionOf(d1.ActorID(), 2), versionOf(d2.ActorID(), 3))) + + err = d2.Update(func(root *json.Object, p *presence.Presence) error { + root.GetText("text").Edit(2, 2, "c") + return nil + }, "insert c") + //d2.vv = [c1:2, c2:4] + assert.Equal(t, true, checkVV(d2.VersionVector(), versionOf(d1.ActorID(), 2), versionOf(d2.ActorID(), 4))) + assert.NoError(t, err) + + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.GetText("text").Edit(1, 3, "") + return nil + }, "delete bc") + //d1.vv = [c1:4, c2:1] + assert.Equal(t, true, checkVV(d1.VersionVector(), versionOf(d1.ActorID(), 4), versionOf(d2.ActorID(), 1))) + assert.NoError(t, err) + assert.Equal(t, 2, d1.GarbageLen()) + assert.Equal(t, 0, d2.GarbageLen()) + + assert.NoError(t, c1.Sync(ctx)) + assert.Equal(t, true, checkVV(d1.VersionVector(), versionOf(d1.ActorID(), 4), versionOf(d2.ActorID(), 1))) + + assert.NoError(t, c2.Sync(ctx)) + assert.Equal(t, true, checkVV(d2.VersionVector(), versionOf(d1.ActorID(), 4), versionOf(d2.ActorID(), 5))) + + assert.Equal(t, 2, d1.GarbageLen()) + assert.Equal(t, 2, d2.GarbageLen()) + + err = d2.Update(func(root *json.Object, p *presence.Presence) error { + root.GetText("text").Edit(2, 2, "1") + return nil + }, "insert c") + assert.Equal(t, true, checkVV(d2.VersionVector(), versionOf(d1.ActorID(), 4), versionOf(d2.ActorID(), 6))) + assert.NoError(t, err) + + assert.NoError(t, c2.Sync(ctx)) + assert.Equal(t, true, checkVV(d2.VersionVector(), versionOf(d1.ActorID(), 4), versionOf(d2.ActorID(), 6))) + + assert.Equal(t, 2, d1.GarbageLen()) + assert.Equal(t, 0, d2.GarbageLen()) + + assert.NoError(t, c1.Sync(ctx)) + assert.Equal(t, true, checkVV(d1.VersionVector(), versionOf(d1.ActorID(), 7), versionOf(d2.ActorID(), 6))) + + // TODO(JOOHOJANG): we have to consider removing detached client's lamport from version vector + assert.NoError(t, c1.Detach(ctx, d1)) + + assert.NoError(t, c2.Sync(ctx)) + assert.Equal(t, true, checkVV(d2.VersionVector(), versionOf(d1.ActorID(), 8), versionOf(d2.ActorID(), 9))) + assert.Equal(t, `{"text":[{"val":"a"},{"val":"c"},{"val":"1"}]}`, d2.Marshal()) + + err = d2.Update(func(root *json.Object, p *presence.Presence) error { + root.GetText("text").Edit(0, 3, "") + return nil + }, "delete all") + assert.NoError(t, err) + assert.Equal(t, true, checkVV(d2.VersionVector(), versionOf(d1.ActorID(), 8), versionOf(d2.ActorID(), 10))) + assert.Equal(t, `{"text":[]}`, d2.Marshal()) + + assert.Equal(t, 3, d2.GarbageLen()) + assert.NoError(t, c2.Detach(ctx, d2)) + assert.Equal(t, 0, d2.GarbageLen()) + }) + + t.Run("detached client node deletion test", func(t *testing.T) { + clients := activeClients(t, 3) + c1, c2, c3 := clients[0], clients[1], clients[2] + defer deactivateAndCloseClients(t, clients) + ctx := context.Background() + d1 := document.New(helper.TestDocKey(t)) + d2 := document.New(helper.TestDocKey(t)) + d3 := document.New(helper.TestDocKey(t)) + + assert.NoError(t, c1.Attach(ctx, d1)) + assert.NoError(t, c2.Attach(ctx, d2)) + assert.NoError(t, c3.Attach(ctx, d3)) + + err := d1.Update(func(root *json.Object, p *presence.Presence) error { + root.SetNewText("text").Edit(0, 0, "a") // a + return nil + }, "insert abc") + assert.NoError(t, err) + + assert.NoError(t, c1.Sync(ctx)) + assert.NoError(t, c2.Sync(ctx)) + assert.NoError(t, c3.Sync(ctx)) + + err = d3.Update(func(root *json.Object, p *presence.Presence) error { + root.GetText("text").Edit(0, 0, "1") // 1a + return nil + }) + assert.NoError(t, err) + + assert.NoError(t, c3.Sync(ctx)) + assert.NoError(t, c1.Sync(ctx)) + assert.NoError(t, c2.Sync(ctx)) + + assert.NoError(t, c3.Detach(ctx, d3)) + assert.NoError(t, c1.Sync(ctx)) + assert.NoError(t, c2.Sync(ctx)) + + err = d2.Update(func(root *json.Object, p *presence.Presence) error { + root.GetText("text").Edit(0, 1, "x") // xa + return nil + }, "delete 123 and insert x") + assert.NoError(t, err) + + assert.NoError(t, c2.Sync(ctx)) + assert.NoError(t, c1.Sync(ctx)) + assert.Equal(t, `{"text":[{"val":"x"},{"val":"a"}]}`, d2.Marshal()) + assert.Equal(t, `{"text":[{"val":"x"},{"val":"a"}]}`, d1.Marshal()) + }) }