Skip to content

fix(loro.js): keep a checkout of the current latest version attached - #1143

Merged
zxch3n merged 3 commits into
mainfrom
fix/loro-js-checkout-current-attached
Sep 29, 2026
Merged

zxch3n merged 3 commits into
mainfrom
fix/loro-js-checkout-current-attached

Conversation

@zxch3n

@zxch3n zxch3n commented Sep 27, 2026

Copy link
Copy Markdown
Member

Summary

doc.checkout(doc.frontiers()) detached a loro.js document even when those frontiers were the latest version, so later imports stopped reaching its state. loro-crdt leaves the document attached. A loro.js peer and a Rust peer that ran the same calls then diverged, and the differential fuzz saw it as lost imports.

doc.checkout(doc.frontiers());
doc.import(remoteUpdate);
doc.getText("t").toString();  // main: old text; loro-crdt: includes the update

Rationale

Rust's _checkout_without_emitting_with_event returns early when the target equals the state frontiers, re-attaching when they are the latest frontiers. It also returns early (keeping the attach state) when the shrunk target equals them, e.g. a target with a dominated extra frontier. loro.js now does the same, clears #checkoutVersion when it re-attaches, and emits no event, like Rust. Checking out the latest version from an older one still leaves the document detached; only checkoutToLatest()/attach() re-attach from there, as in Rust.

The existing test "records imports in the oplog while the state is detached" relied on the old behavior to detach; it now calls detach().

Validation

  • New test in loro-js/tests/runtime.test.ts covering: attached checkout of the current version (imports still apply), a dominated extra frontier, a repeated checkout of an old version, old → latest staying detached, and detach() then checkout(frontiers) re-attaching and accepting local edits. Fails on main. Every assertion was checked against loro-crdt (WASM build of this main), which returns the same attach state in all cases.
  • pnpm --dir loro-js format:check, lint, typecheck, test (248 passed), build.
  • bench:complexity (1k–8k), 7 alternating rounds vs main, loadavg 25–34: 284 items, none both >1.5× and >0.3 ms slower. Total 1301 ms on main, 1258 ms here.

Found by the differential fuzz harness (#1141), and listed by #1132 as a non-MovableList divergence.

🤖 Generated with Claude Code

Rust's checkout returns early when the target equals the state's
frontiers, re-attaching when they are the latest frontiers, and also
when the shrunk target equals them. loro.js always detached, so
`doc.checkout(doc.frontiers())` stopped later imports from reaching the
state and made loro.js peers diverge from loro-crdt ones that ran the
same calls.

The old detached-import test relied on this; it now uses detach().

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

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

WASM Size Report

  • Original size: 3277.59 KB
  • Gzipped size: 1089.33 KB
  • Brotli size: 762.83 KB

…current-attached

# Conflicts:
#	loro-js/src/runtime/document.ts

zxch3n commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

Merged the latest main (c0ff1255). The only conflict was two helpers appended at the end of document.ts; both are kept. Main's checkoutToLatest attach step resets the same two fields (#checkoutVersion, #detached) as this early return, so the merged logic is consistent.

Still needed on main: doc.checkout(doc.frontiers()) detaches, so a later import does not reach the state (xy instead of xyB). loro-crdt and this branch give xyB and match loro-crdt on all 8 attach-state probes. pnpm -C loro-js check with LORO_REQUIRE_WASM_REFERENCE=1 passes (25 files, 415 tests).

@zxch3n
zxch3n merged commit 0ab8e38 into main Sep 29, 2026
2 checks passed
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