Repository navigation
fix(loro.js): treat containers under deleted ancestors as deleted - #1147
Conversation
isDeleted() only looked at a container's own parent binding, and a tree node only at its own deleted flag. A child of a removed map value, or a node under a deleted tree node, therefore counted as alive: it kept a path, getNodes() listed it, and local edits to it recorded ops on invisible state. Rust's DocState::is_deleted follows the whole chain and rejects such edits with ContainerDeleted. Walk the parent chain (and tree ancestors) for isDeleted, getPathToContainer, and a new guard in #appendAndApply that throws Rust's message. getContainerById keeps its one-level check, since Rust still resolves those IDs. nodes() now returns every node and getNodes() lists alive nodes breadth-first, as Rust does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
WASM Size Report
|
|
Merged the latest Still needed on Known limitation (unchanged, not a data issue): for a directly deleted node, Rust's |
…path Keep main's one-level _isContainerDeleted for getContainerById and the diff/event reachability walk, and use the ancestor walk only in isDeleted(), getPathToContainer() and getCursorPos(). Drop the guard on local edits to deleted containers: it threw on a redo that main allows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged main (with #1131) and reworked in 03dc8dc: the ancestor walk now backs only |
getNodes() spread each child list, and with withDeleted the deleted subtrees, into one push() call, which throws a RangeError past V8's argument limit (about 125k on Node's main thread). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged main and fixed the blocking issue in c783b88: |
Summary
loro.jsdecided whether a container was deleted from its own parent binding only, and a tree node from its own deleted flag, so anything below a deleted container or tree node counted as alive.loro-crdtfollows the whole chain. Differences onmain, checked against a WASM build ofmain(c0ff1255):mainloro-crdt/ this PRroot.delete("m")wheremholds a texttext.isDeleted()falsetruedoc.getPathToContainer(text.id)["root","m","t"]undefinedtree.delete(parent)tree.getNodes()tree.isNodeDeleted(child),child.isDeleted(),grandchild.data.isDeleted()falsetruetree.nodes()Rationale
Document._isContainerUnreachablewalks the parent chain, and for tree metadata the tree ancestors (LoroTree._isNodeHidden, bounded by the node count so a malformed cycle cannot loop). It backsisDeleted(),getPathToContainer(), andgetCursorPos()only.getNodes()now lists alive nodes breadth-first from the roots, then (withwithDeleted) the deleted subtrees, as Rust'sget_nodes_underdoes.Changed after #1131 merged:
_isContainerDeletedstays main's one-level check. It is used bygetContainerByIdand by #1131's#isReachable, which already walks ancestors; walking them again per level made deep trees quadratic (tree-diff-order.test.ts). The earlier guard that rejected local edits to deleted containers is dropped: it threw on a redo that main (and #1131'sdiff-attached-children.test.ts) accepts.Known limitations (unchanged from
main)loro-crdtrejects local edits to a deleted container (Transaction::apply_local_op);loro.jsstill records them.parent()returns its internal deleted-root node andindex()returnsundefined;loro.jsreturnsundefinedand-1.Validation
loro-js/tests/deleted-containers.test.ts(map descendants, list children, tree subtrees including revival); all three fail onmain.pnpm -C loro-js checkwithLORO_REQUIRE_WASM_REFERENCE=1and a Rust reference built frommain: 29 files, 482 passed, 1 skipped.main's code again;bench:complexityfor the unchangedgetNodes/nodeschange (7 alternating rounds vsmain, loadavg 10–14): 284 items, none both >1.5× and >0.3 ms slower.🤖 Generated with Claude Code