Repository navigation
fix(loro.js): match Rust's local tree create and move positions - #1148
Conversation
- Port generate_fi_at's rearrangement: when both neighbors share a fractional index, the right neighbor and following siblings with that index get new positions (generate_n_evenly) through extra move ops. loro.js threw "fractional index bounds are not ordered" instead. - Like mov_with_txn, a move to the node's current index, including an omitted index when it is already last, records nothing. - Like mov_after/mov_before, count the target index without the moved node when it shares the parent. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Rust's is_ancestor_of treats a deleted node's parent as the deleted root. loro.js kept walking a deleted node's old parent, so moving a node under its own deleted child threw. The walk is also bounded by the node count: a cycle left by concurrent moves made it loop forever. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
WASM Size Report
|
…l-positions # Conflicts: # loro-js/src/runtime/document.ts
|
Merged the latest Still needed on |
…l-positions # Conflicts: # loro-js/src/runtime/document.ts
… move The no-op check listed every sibling to count them, so each move within a parent was linear in the sibling count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged main (resolved the |
Summary
Four local Tree API differences from
loro-crdt, found by the differential fuzz in lockstep with Rust:Equal fractional indexes throw. Two roots created concurrently at index 0 both get position
80.tree.createNode(undefined, 1)or moving a node between them threwfractional index bounds are not ordered. Rust gives the new node817F80and moves the right neighbor to8180in the same transaction.No-op moves record ops.
tree.move(a, undefined, 0)for a node already at 0,tree.move(last, undefined)/last.move(undefined)for the last child, andb.moveAfter(a)for the node right afteraeach recorded a move. Rust records nothing. A redundant move can override a concurrent move of the same node, so this also changed concurrent outcomes.moveAfter/moveBeforein the same parent. The index counted the moved node itself, soa.moveAfter(c)in[a, b, c]threwtree index 3 is out of range, and other cases landed one slot off.Moving a node under its own deleted descendant threw. Rust's
is_ancestor_oftreats a deleted node's parent as the deleted root, sotree.move(parent, deletedChild)is allowed (the node becomes deleted). Theloro.jscycle check followed the deleted node's old parent and threwcannot move a tree node below itself or its descendant. The walk is now also bounded by the node count: with a cycle left by concurrent moves (a separate, still open convergence bug), it looped forever, which is how the fuzz harness first hung.Rationale
LoroTree._positionForis now a port of Rust'sNodeChildren::generate_fi_at: when the left and right neighbors share a position, it collects the right neighbor and the following siblings with that position, and spreadsn + 1positions between the left neighbor and the next different position (fractionalIndexesBetween, a port ofgenerate_n_evenly)._treeCreate/_treeMoveemit the node's op, then one move per rearranged sibling, in Rust's order._treeMovereturns early when the node already has that parent and index, with an omitted index meaning the last slot, as inTreeHandler::mov/mov_with_txn.LoroTreeNode.moveAfter/moveBeforeapply the same index adjustment asmov_after/mov_before._treeMovestops at a deleted node and at most visits every node once.Validation
loro-js/tests/tree-local-positions.test.ts; the first four tests fail onmain, and the fifth (move under a deleted child) throws there. The fractional indexes, op counts, and orders asserted are exactly whatloro-crdt(WASM build of thismain) produces for the same calls.pnpm --dir loro-js format:check,lint,typecheck,test(252 passed),build.bench:complexity(1k–8k), 7 alternating rounds vsmain, loadavg 10–13: 284 items, none both >1.5× and >0.3 ms slower. Total 953 ms onmain, 956 ms here.Concurrent tree moves that form a cycle, and tree event ordering, are separate issues and are not addressed here.
🤖 Generated with Claude Code