Skip to content

fix(loro.js): apply Text/List deletes imported while detached - #1146

Closed
zxch3n wants to merge 1 commit into
mainfrom
fix/loro-js-detached-import-delete
Closed

zxch3n wants to merge 1 commit into
mainfrom
fix/loro-js-detached-import-delete

Conversation

@zxch3n

@zxch3n zxch3n commented Sep 27, 2026

Copy link
Copy Markdown
Member

Summary

A Text/List delete imported while a loro.js document was detached was lost when the document moved to a version that includes it:

doc.checkout([{ peer: "2", counter: 1 }]);            // "he"
doc.import(sourceUpdateThatDeletesTheO);
doc.checkoutToLatest();   // main: "hello"; loro-crdt and a fresh import: "hell"

The same happens for checkout(frontiers) to any version that contains the delete, and for lists. Afterwards the replica disagrees with every other peer. This was one of the "peers do not converge with a replay of the same updates" classes found by fuzzing.

Rationale

Importing while detached only integrates the records into the history; the container state (including the sequence's deletion index) is updated by the next transition. #canTransitionRecords already refuses the incremental transition when an insert is not materialized in the sequence, which falls back to a replay. It accepted any delete, though. #applyVersionTransition then looked the delete up with idRunsDeletedBy, found nothing, and skipped it. The fix makes #canTransitionRecords require that the sequence has recorded every element a delete removes (idRunsDeletedBy covers the op's full length). Otherwise it falls back like the insert case. Deletes integrated normally keep the incremental path; the extra check is O(log n + runs) per delete op in a transition, the same lookup the transition itself does.

Map, Tree, and MovableList transitions read per-subject histories that are indexed on integration even while detached, so they were not affected.

Validation

  • New tests in loro-js/tests/runtime.test.ts: text and list, via checkoutToLatest() and via checkout(oplogFrontiers). All four fail on main, and the expected values equal loro-crdt (WASM build of this main) and a fresh import.
  • Differential fuzz (harness in test(loro.js): add a multi-peer differential fuzz harness #1141): with this change, the remaining peer-vs-replay and checkout-vs-replay failures in 500 Map/List/Text/Counter seeds (after fix(loro.js): stop a concurrent insert at the end of its origin's subtree #1139) disappear.
  • pnpm --dir loro-js format:check, lint, typecheck, test (251 passed), build.
  • bench:complexity (1k–8k), 7 alternating rounds vs main, loadavg 12–13: 284 items, none both >1.5× and >0.3 ms slower. Total 922 ms on main, 938 ms here. (An earlier run at loadavg 40 with a concurrent fuzz job flagged unrelated map-import items that do not reach this code; they did not reproduce.)

🤖 Generated with Claude Code

Importing while detached only adds records to the history. The next
checkout or checkoutToLatest found no runs "deleted by" such a delete in
the sequence's deletion index and skipped it, so the deleted content
stayed visible. Inserts imported while detached already fell back to a
replay because #canTransitionRecords checks them.

Check deletes the same way: fall back unless the sequence has recorded
every element the delete removes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

WASM Size Report

  • Original size: 3210.11 KB
  • Gzipped size: 1062.59 KB
  • Brotli size: 744.13 KB

zxch3n commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

Closing: covered by #1126 (5e409d1d), which added the same check to #canTransitionRecords (a delete op must have recorded every element it removes, otherwise the transition replays).

Checked on main at c0ff1255: text and list deletes imported while detached are applied by both checkoutToLatest() and checkout(oplogFrontiers) (hell / ["h","e","l","l"], same as loro-crdt). The 4 regression tests from this PR pass on main.

@zxch3n zxch3n closed this Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant