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())
+ })
}