Skip to content
Draft
204 changes: 190 additions & 14 deletions pkg/document/crdt/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@
return " " + sb.String()
}

// ClearChildren clears the children.
func (n *TreeNode) ClearChildren() error {
return n.Index.SetChildren(nil)
}

// Append appends the given node to the end of the children.
func (n *TreeNode) Append(newNodes ...*TreeNode) error {
indexNodes := make([]*index.Node[*TreeNode], len(newNodes))
Expand Down Expand Up @@ -594,6 +599,14 @@
return builder.String()
}

// Marshal returns the JSON encoding of this Tree.
func (n *TreeNode) Marshal() string {
builder := &strings.Builder{}
builder.WriteString(fmt.Sprintf(`%s:`, n.id.CreatedAt.ToTestString()))
marshal(builder, n)
return builder.String()
}

// Purge physically purges the given node.
func (t *Tree) Purge(child GCChild) error {
node := child.(*TreeNode)
Expand Down Expand Up @@ -673,7 +686,7 @@
builder.WriteString(`]`)

if node.Attrs != nil && node.Attrs.Len() > 0 {
builder.WriteString(fmt.Sprintf(`,"attributes":`))
builder.WriteString(`,"attributes":`)
builder.WriteString(node.Attrs.Marshal())
}

Expand Down Expand Up @@ -838,44 +851,50 @@
versionVector time.VersionVector,
) ([]GCPair, resource.DataSize, error) {
var diff resource.DataSize

// 01. find nodes from the given range and split nodes.
fromParent, fromLeft, diffFrom, err := t.FindTreeNodesWithSplitText(from, editedAt)
if err != nil {
return nil, diff, err
}

toParent, toLeft, diffTo, err := t.FindTreeNodesWithSplitText(to, editedAt)
if err != nil {
return nil, diff, err
}

diff.Add(diffFrom, diffTo)

toBeRemoveds, toBeMovedToFromParents, err := t.collectBetween(
fromParent, fromLeft, toParent, toLeft,
editedAt, versionVector,
)
removes, merges, err := t.collectDeleteAndMerge(fromParent, fromLeft, toParent, toLeft)
if err != nil {
return nil, resource.DataSize{}, err
}

// 02. Delete: delete the nodes that are marked as removed.
var pairs []GCPair
for _, node := range toBeRemoveds {
if node.remove(editedAt) {
for _, node := range removes {
removed, err := t.RemoveNode(node, editedAt, versionVector)
if err != nil {
return nil, resource.DataSize{}, err
}
if removed != nil {
pairs = append(pairs, GCPair{
Parent: t,
Child: node,
Child: removed,
})
}
}

// 03. Merge: move the nodes that are marked as moved.
for _, node := range toBeMovedToFromParents {
if node.removedAt == nil {
if err := fromParent.Append(node); err != nil {
return nil, resource.DataSize{}, err
}
for _, node := range merges {
removed, err := t.MergeNode(node, editedAt, versionVector)
if err != nil {
return nil, resource.DataSize{}, err
}
if removed != nil {
pairs = append(pairs, GCPair{
Parent: t,
Child: removed,
})
}
}

Expand Down Expand Up @@ -992,6 +1011,105 @@
return toBeRemoveds, toBeMovedToFromParents, nil
}

func (t *Tree) collectDeleteAndMerge(fromParent, fromLeft, toParent, toLeft *TreeNode) ([]*TreeNode, []*TreeNode, error) {

Check failure on line 1014 in pkg/document/crdt/tree.go

View workflow job for this annotation

GitHub Actions / build

The line is 122 characters long, which exceeds the maximum of 120 characters. (lll)
var removes []*TreeNode
var merges []*TreeNode
var stack []*TreeNodeID

if fromParent == nil || fromLeft == nil || toParent == nil || toLeft == nil {
return nil, nil, fmt.Errorf("invalid TreePos: cannot resolve nodes")
}

err := traverseInTreePos(fromParent, fromLeft, toParent, toLeft, func(parent, left *TreeNode) error {
if left.IsText() {
removes = append(removes, left)
return nil
}

if parent == left {
// push
stack = append(stack, left.ID())
} else {
// pop
if len(stack) > 0 && stack[len(stack)-1].Equals(left.ID()) {
stack = stack[:len(stack)-1]
removes = append(removes, left)
} else {
merges = append(merges, left)
}
}
return nil
})
if err != nil {
return nil, nil, err
}

slices.Reverse(merges)
return removes, merges, nil
}

func (t *Tree) RemoveNode(node *TreeNode, editedAt *time.Ticket, versionVector time.VersionVector) (*TreeNode, error) {
actorID := node.id.CreatedAt.ActorID()

if len(versionVector) == 0 {
if node.remove(editedAt) {
return node, nil
}
}
clientLamportAtChange, ok := versionVector.Get(actorID)
if !ok {
return nil, nil
}

if node.canDelete(editedAt, clientLamportAtChange) {
if node.remove(editedAt) {
return node, nil
}
}
return nil, nil
}

func (t *Tree) MergeNode(node *TreeNode, editedAt *time.Ticket, versionVector time.VersionVector) (*TreeNode, error) {
actorID := node.id.CreatedAt.ActorID()

var clientLamportAtChange int64
if len(versionVector) == 0 {
clientLamportAtChange = time.MaxLamport
} else {
var ok bool
clientLamportAtChange, ok = versionVector.Get(actorID)
if !ok {
return nil, nil
}
}

nodeIdx := node.Index
for nodeIdx, err := nodeIdx.NextSiblingExtended(); nodeIdx != nil; nodeIdx, err = nodeIdx.NextSiblingExtended() {
if err != nil {
return nil, err
}
right := nodeIdx.Value

if right.canDelete(editedAt, clientLamportAtChange) {
children := right.Children()
if len(children) > 0 {
if err := right.ClearChildren(); err != nil {
return nil, err
}
if err := node.Append(children...); err != nil {
return nil, err
}
}
if right.remove(editedAt) {
return right, nil
}
return nil, nil
}
}

return nil, nil
}

func (t *Tree) split(
fromParent *TreeNode,
fromLeft *TreeNode,
Expand Down Expand Up @@ -1042,6 +1160,64 @@
return t.IndexTree.TokensBetween(fromIdx, toIdx, callback)
}

func traverseInTreePos(fromParent, fromLeft, toParent, toLeft *TreeNode,
callback func(parent *TreeNode, left *TreeNode) error,
) error {
isEnd := func(p1, l1, p2, l2 *TreeNode) bool {
return p1.ID().Equals(p2.ID()) && l1.ID().Equals(l2.ID())
}

var ok bool
for {
if isEnd(fromParent, fromLeft, toParent, toLeft) {
break
}
if fromParent, fromLeft, ok = nextTreePos(fromParent, fromLeft); !ok {
break
}
if err := callback(fromParent, fromLeft); err != nil {
break
}
}
return nil
}

func nextTreePos(parent, left *TreeNode) (*TreeNode, *TreeNode, bool) {
// CASE 1, 2: parent == left
if parent == left {
children := parent.Index.Children(true)
if len(children) > 0 {
leftMostChild := children[0].Value
if leftMostChild.IsText() {
return parent, leftMostChild, true
} else {
return leftMostChild, leftMostChild, true
}
}
if parent.Index.Parent == nil {
return nil, nil, false
}
return parent.Index.Parent.Value, parent, true
}

// CASE 3, 4: parent != left
siblings := parent.Index.Children(true)
idx := parent.Index.OffsetOfChild(left.Index)
if idx+1 < len(siblings) {
next := siblings[idx+1]
if next.IsText() {
return parent, next.Value, true
} else {
return next.Value, next.Value, true
}
}

if parent.Index.Parent == nil {
return nil, nil, false
}
return parent.Index.Parent.Value, parent, true
}

// StyleByIndex applies the given attributes of the given range.
// This method uses indexes instead of a pair of TreePos for testing.
func (t *Tree) StyleByIndex(
Expand Down
36 changes: 33 additions & 3 deletions pkg/index/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ func (n *Node[V]) Children(includeRemovedNode ...bool) []*Node[V] {
// SetChildren sets the children of the given node.
// This method does not update the size of the ancestors.
func (n *Node[V]) SetChildren(children []*Node[V]) error {
if n.IsText() {
if n.IsText() && children != nil {
return ErrInvalidMethodCallForTextNode
}

Expand Down Expand Up @@ -438,20 +438,50 @@ func (n *Node[V]) nextSibling() (*Node[V], error) {
return nil, nil
}

// nextSibling returns the next sibling of the node, even if the sibling is
func (n *Node[V]) NextSiblingExtended() (*Node[V], error) {
parent := n.Parent
if parent == nil {
return nil, nil
}

offset, err := parent.FindOffset(n, true)
if err != nil {
return nil, err
}

if len(parent.children) > offset+1 {
sibling := n.Parent.children[offset+1]
return sibling, nil
}

for parent, err = parent.NextSiblingExtended(); parent != nil; parent, err = parent.NextSiblingExtended() {
if err != nil {
return nil, err
}
if len(parent.children) > 0 {
return parent.children[0], nil
}
}
return nil, err
}

// FindOffset returns the offset of the given node in the children.
func (n *Node[V]) FindOffset(node *Node[V]) (int, error) {
func (n *Node[V]) FindOffset(node *Node[V], includeRemovedNode ...bool) (int, error) {
if n.IsText() {
return 0, ErrInvalidMethodCallForTextNode
}

include := len(includeRemovedNode) > 0 && includeRemovedNode[0]

// If nodes are removed, the offset of the removed node is the number of
// nodes before the node excluding the removed nodes.
offset := 0
for _, child := range n.children {
if child == node {
return offset, nil
}
if !child.Value.IsRemoved() {
if include || !child.Value.IsRemoved() {
offset++
}
}
Expand Down
Loading