Repository navigation
fix(loro.js): drop containers deleted before the shallow root, keep tree node metadata - #1128
Conversation
…mport
Snapshot-hydrated state has no tombstones or op history, so incremental
transitions returned wrong historical state, and a replay could hydrate
lazy latest-state entries on top of replayed ops ("duplicate sequence id").
Rationale: context/loro-js-performance.md (snapshot-hydrated state).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ow retreat A shallow history trims the ops that wrote root-time values, so retreating a later write dropped the key or node. Fall back to the shallow root state, indexed lazily per container. Rationale: context/loro-js-performance.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The first checkout after a snapshot import replayed the whole document, up to three times for the first `diff`, and replaced correct snapshot state with loro.js replays. Replaying Rust-created rich text lost styles and moved text, because loro.js positions do not count Rust style anchors. A transition now hydrates and rebuilds only the Text/List containers it touches, once, and keeps a snapshot state that the replay does not reproduce for every version with the same operations on that container. Checkout events of unread nested containers are correct, and a full snapshot export keeps unread containers after history is loaded. See context/loro-js-performance.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…allow-root-map-winner Resolution: the shallow root fallbacks here supersede the base branch's rebuild when a shallow Map or Tree winner was trimmed, so that guard is dropped. The root-state lookup reuses the base branch's root entry index (`#shallowRootEntryIndex`) instead of formatting every root container id on the first fallback. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rimmed placement When a retained delete wins but its node's placement op was trimmed from a shallow history, the retreat left the node alive. It now takes the root placement from the shallow root state and stays deleted. This moves the fix from the next PR in the stack so this one is correct on its own. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hallow-tree-meta-retention The retained-delete tree fix from this branch now lives in the base branch; both sides made the same change, so it merged cleanly. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mergeable children use deterministic root-form IDs. When their marker is deleted or replaced by another kind before the shallow root, re-ensuring the same kind must show their state again, so Rust keeps every mergeable cid in its retention roots. The retention walk skipped mergeable roots and exported the hidden child as empty, for loro.js and Rust readers alike. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Shallow export rebuilds the root and latest states by full replay, which replaced snapshot state that the replay does not reproduce (Rust rich text) in both the export and the live document. The rebuilds now pass the version they leave, so snapshot states are stashed and reinstated, and unread lazy sequences are hydrated first. The import builds the root entry index only when it merges a latest state. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…allow-root-map-winner Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hallow-tree-meta-retention Resolution: the shallow export keeps this branch's retention sets and passes the base branch's previous versions to `#rebuildFromHistory`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The check for crossing snapshot operations ran before lazily encoded containers were hydrated, so an unread nested Text or List skipped its history rebuild and the checkout fell back to replaying the whole document. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…allow-root-map-winner Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hallow-tree-meta-retention Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…allow-root-map-winner Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hallow-tree-meta-retention Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Resolution: a detached snapshot export (#1130) builds its latest state with the same prepared transition as checkout (#prepareSnapshotTransition, #settleSnapshotSequences) and encodes it with the lazy-aware state encoder. Lazily imported documents therefore also use the O(delta) transition instead of a staging replay, and snapshot states that a replay does not reproduce stay exact in the export. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… checkout Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…allow-root-map-winner Resolution: `#canTransitionRecords` takes no target here, since the shallow root fallbacks replace the base branch's rebuild guard. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hallow-tree-meta-retention Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Follow-up on the review of
|
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
WASM Size Report
|
|
Round-2 follow-up:
|
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…allow-root-map-winner Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hallow-tree-meta-retention Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Round-3 update:
The other phases are within noise. The full table is in #1126's round-3 comment.
|
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…allow-root-map-winner Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hallow-tree-meta-retention Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- forkAt/fork keeps an unreplayable snapshot state only when the fork's version includes it; an older fork replays its own operations, as on main. - A MovableList is never unreplayable: its hydrated state carries Rust position ids, so its replay replaces the state, as on main. - A completion that throws reinstalls the snapshot state; #transitionTo prepares inside its try, and diff restores with a full rebuild when it or its move back throws. Rationale and limits: context/loro-js-performance.md (issue #1163 tracks the concurrent-delete placement bug that main already has). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…allow-root-map-winner Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hallow-tree-meta-retention Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replaying a hydrated MovableList to the current version and retreating from there carried loro.js's MovableList non-convergence at the latest version into older versions (random Rust histories: 581 mismatching versions vs 91 on main). A MovableList is no longer a snapshot sequence: its hydrated state has no move or value history, so a transition that crosses its snapshot operations replays to the target, as on main. See context/loro-js-performance.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…allow-root-map-winner Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hallow-tree-meta-retention Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…allow-root-map-winner Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hallow-tree-meta-retention Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Round-4 follow-up:
|
A full rebuild only checked Text whose snapshot state still had styles. When every marked character was deleted, the mark's anchors still shift later Rust positions, so the replay (hlXo -> hloX) replaced the snapshot state and was exported. The check now also covers Text with a mark operation in its history (#markedTexts) or styles in its shallow root state; plain text still skips it. See context/loro-js-performance.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…allow-root-map-winner Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hallow-tree-meta-retention Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Round-5 follow-up:
|
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-richtext-anchors Integrates #1126–#1128: `_swapState` also swaps the anchor registry and keeps `_styleIndex` replaceable with `TextStyleIndex<TextStyle>`; the shallow export passes #1128's retained-container set and this branch's style redaction to `#buildStateStore`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stacked on #1127, which is stacked on #1126. Merge #1126 → #1127 → #1128 back to back (see #1126 and #1127).
Summary
This ports Rust #1119 and #1123 to
loro.js.On
main,LoroDoc.#buildStateStorewrites every container ever created into both the shallow root state and the latest state. So a map child deleted before the cut, such as{"secret":"old"}, ships in every shallow snapshot. Re-exporting an older blob at its own root keeps it too.Fix
#retainedContainerKeys()is the RustAliveWalk::Retentionrule. It walks from the root containers through visible Map/List children, and follows every tree node's meta, including deleted nodes, because a retained move can revive a node with its old meta.retain_created_after_root.loro.jsalways rebuilds the root state by replay (it has no cached-root reuse branch), so re-exports of old blobs are pruned too.context/internal-encoding.mddocuments this in its retention-set section, with aloro-jsparagraph.Parity with Rust (#1119/#1123 scenarios)
Moveof a directly deleted nodelegacy_*from #1123) and of an oldloro.jsblobValidation
New
loro-js/tests/shallow-snapshot-deleted-containers.test.ts(17 tests) ports every test inshallow_snapshot_deleted_containers.rsexcept state-only, whichloro.jsdoes not implement. It also imports the Rust fixtures from fix: drop containers deleted before the shallow root from shallow and state-only snapshots #1119/fix: drop dead containers when re-exporting a shallow doc at its cached root #1123 and a new blob exported byorigin/mainloro.js(legacy-shallow-tree-revival-dead-map.ts.blob). On the base branch, 8 tests fail; the revival tests passed before too and now guard against over-pruning.Cross-runtime check: Rust
mainimports shallow blobs exported by this branch (tree revival + dead map, 0 and 300 filler ops). The deep value equals theloro.jsvalue,has_container(dead map)is false, and a Rust re-export at the same root keeps the same value.pnpm -C loro-js check(format, lint, typecheck, tests, build) passes with 311 tests.shallow-snapshot-deleted-containers.test.tsalso covers hidden mergeable children (switched kind and deleted marker, with 0 and 300 retained ops), and Rust reads them the same.Shallow export, 8,192 child Maps with half deleted before the root and 300 retained ops (median of 5 alternating runs, load1 5.8–6.0):
main: 138.0 ms, 558,402 B.c12ccc99: 110.5 ms, 352,852 B.64d12269: 112.0 ms, 352,852 B.The output is 37% smaller. The round-3 review measured a different setup: 387,772 → 267,366 B (−31%).
Combined with fix(loro.js): include reattached child containers' state in diff() and revertTo() #1131
ae55ff7a:pnpm -C loro-js checkpasses with 338 tests. On that combination, the round-2 tree revival repro (B3) gives Rust's exact tree.Integration notes
#buildStateStore(frontiers, { redacted, only }), while this PR passes a retention set (#buildStateStore(frontiers, retained)). The two need one combined signature, and the shallow export needs both. Whichever PR lands second does this.Rust: #1119 (merged), #1123 (open). If #1123's final semantics change, this PR should follow.
🤖 Generated with Claude Code