Skip to content

test(loro.js): add a multi-peer differential fuzz harness - #1141

Merged
zxch3n merged 6 commits into
mainfrom
test/loro-js-differential-fuzz
Sep 29, 2026
Merged

zxch3n merged 6 commits into
mainfrom
test/loro-js-differential-fuzz

Conversation

@zxch3n

@zxch3n zxch3n commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

Update (merged main at 238988f5, with #1131): a second fixed run adds 120 seeds with nested child containers and snapshot imports, all passing on main now; both runs take about 5.5 s locally. Seeds 297, 315, 344, 422, 452, and 559 of 0–599 with nested children still fail the event oracle on main; #1150 fixes all six.

Update (merged main at c0ff1255): #1126 and #1136 fix all seven seeds that were pending, so the default run now covers all 120 seeds, and the full-snapshot oracle is on as well (about 3 s locally; the timeout is 120 s). FUZZ_WASM=1 now uses the shared tests/support/rust-reference.ts loader from #1136, so LORO_WASM_NODEJS and the CI reference build apply. Lockstep with Rust is only meaningful once #1145 (absent-key map deletes, zero counter increments) and #1143 (checkout of the current version) land. On main most seeds diverge at the first such call; with #1145, 49 of 50 seeds match step by step. The context doc lists what is fixed, what is in PRs, and what is still open (tree cycles and tree event order).

Summary

Adds a randomized differential harness for loro.js and keeps a CI-sized fixed seed set in the test suite.

  • loro-js/tests/fuzz/differential.ts: 2–4 peers make random edits (Map, List, Text, Tree, Counter, MovableList, marks, child containers), exchange updates (update, full, per-change blobs in shuffled order via import or importBatch, optionally snapshots), and check out random versions.
  • Oracles: every peer equals a one-shot replay and a shuffled per-change replay; checkout(F) equals a fresh import of exactly the changes in F; a full snapshot and shallow snapshots equal the full-history document, including checkouts into the retained range; an event mirror keyed by container ID must reproduce toJSON() after every commit, import, and checkout. The mirror follows the Rust fuzzer's ContainerTracker rules.
  • Optional Rust reference (FUZZ_WASM=1): each peer gets a loro-crdt twin, every concrete edit/sync/checkout runs on both, and the first diverging action is reported. It also compares a Rust replay, Rust checkouts, both shallow-snapshot directions, and the chosen shallow root.
  • Failures are grouped by signature, minimized by delta debugging over the action list, and reported as a trace of loro.js API calls.
  • context/loro-js-differential-fuzz.md: how to run it, the oracle semantics, and the divergences found on main, linked from the root AGENTS.md.

Rationale

The runtime had no multi-peer oracle beyond hand-written cases. This harness found the bugs fixed in #1139, #1140, and the other fix/loro-js-* PRs from this session. It also found the still-open Tree, nested-container event, and shallow classes listed in the context doc. The default run needs no WASM build, so it can run with the rest of pnpm test. It overlaps little with #1132, which pairs every peer with Rust for MovableList only.

Validation

  • Default run: 120 seeds of Map/List/Text/Counter and 120 seeds with nested child containers and snapshot imports, with the replay, checkout, full-snapshot, and event oracles, about 5.5 s; all pass on main at 238988f5.
  • FUZZ_VALIDATE_HARNESS=1 with the WASM reference found no case where Rust's own events disagree with the mirror: 300 Map/List/Text/Counter seeds with child containers and 100 CORE_FEATURES seeds (which include trees). A tree-only validation run could not finish because loro.js loops forever on a tree cycle created by concurrent moves (listed in the context doc).
  • pnpm --dir loro-js format:check, lint, typecheck, test (249 passed, 1 skipped manual block), build.
  • No changeset: tests and docs only; the published package does not change.

🤖 Generated with Claude Code

Random multi-peer edits, update exchanges in several orders, and
checkouts, checked against replay, checkout-at-version, snapshot,
shallow-snapshot, and event-mirror oracles, and optionally step by step
against a loro-crdt WASM build. Failures are grouped by signature,
minimized, and printed as loro.js calls.

The default run is a fixed 120-seed Map/List/Text/Counter set that needs
no WASM build (~2 s). Seeds that fail on main are listed with the fix
that makes them pass. Usage and findings:
context/loro-js-differential-fuzz.md.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
zxch3n and others added 4 commits September 29, 2026 19:12
Main (#1126, #1136) fixes all seven pending seeds, so the CI run now covers
all 120 seeds and also the full-snapshot oracle. FUZZ_WASM=1 uses the shared
tests/support/rust-reference.ts loader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seeds 0-119 with nested children and snapshot imports pass on main now
that #1131 is in. See context/loro-js-differential-fuzz.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zxch3n
zxch3n merged commit 5365992 into main Sep 29, 2026
2 checks passed
zxch3n added a commit that referenced this pull request Sep 29, 2026
The Rust-model MovableList passes seeds 0-599 of the #1141 fuzz with all
oracles in lockstep with Rust; main failed 144 of seeds 0-299.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zxch3n added a commit that referenced this pull request Sep 29, 2026
#1166)

* test(loro.js): add a Rust differential suite for MovableList

Drive the Rust WASM build and loro.js with the same random multi-peer
edits, crossing engines on every sync, and require equal values, versions,
per-element metadata and events. Profiles that diverge on the current
MovableList model are recorded as known divergences. Design and findings:
context/loro-js-movable-list.md.

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

* test(loro.js): compare event effects and replicate unordered reverts

Event deltas that differ only in shape are checked by effect; reverts whose
op order Rust leaves to hash order are checked by value and replicated from
Rust; peers that both engines reject are retired; peer IDs are never reused;
undo stays unchained; the minimizer keeps those rules. See
context/loro-js-movable-list.md.

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

* test(loro.js): join shallow snapshots through Rust and keep replayed undo unchained

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

* test(loro.js): accept no-op sequence events and replicate reverts whose diff shape differs

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

* test(loro.js): skip shallow editors and start range exports at the shallow root

Both follow Rust issues listed in context/loro-js-movable-list.md.

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

* test(loro.js): measure revert container depths at the revert target

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

* test(loro.js): run the MovableList differential suite on the shared Rust reference

Load Rust through tests/support/rust-reference.ts like the other differential
suites, so it runs in `pnpm test` (and in CI's test-loro-js) and skips without
a WASM build. Drop the separate workflow and scripts. insert/delete now
converges on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): model MovableList like Rust (positions and LWW elements)

MovableList keeps a Fugue sequence of list items and elements whose
position and value are last-writer-wins registers, as Rust does. Moves of
concurrently deleted elements, deletes after moves, snapshots (list item,
element and last-set IDs, dead items), metadata getters, version
transitions, revertTo/applyDiff (Rust's apply_delta), undo and events now
match Rust. Snapshot-hydrated documents replay history before concurrent
imports, and a few shallow-snapshot incompatibilities are fixed. The
differential suite passes every profile. Design and remaining gaps:
context/loro-js-movable-list.md.

Rebuilt on current main: MovableList joins #1126's per-container snapshot
completion (its snapshot state names Rust IDs), applyDiff/revertTo/undo use
main's port of Rust #1138's apply_delta on the new model, and undo
transforms only over changes the UndoManager did not make.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): skip final syncs with a peer that left the run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): keep pending changes and follow Rust's shallow import rules

Decoding deferred snapshot history dropped pending changes. Shallow imports
now check the unknown part of each change like is_before_shallow_root, and an
import that only adds pending changes creates their root containers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): pin diff() of a MovableList child created in the range

main's diff() reports such a child by its ops, including map keys set and
deleted again, as Rust does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(loro.js): resolve concurrent MovableList op indices through a tracker

A causal view per concurrent op made importing concurrent moves O(n^2).
Like Rust's Tracker::checkout, keep a tracker version and a per-position
delta metric that moves incrementally between op versions; see
context/loro-js-movable-list.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(loro.js): describe the merged MovableList fixes in the changeset

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): never walk a container's fields when comparing event values

A MovableList replaced by a plain value made the comparison enumerate the
container and hit its guarded element sequence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): fuzz MovableList in the multi-peer differential CI run

The Rust-model MovableList passes seeds 0-599 of the #1141 fuzz with all
oracles in lockstep with Rust; main failed 144 of seeds 0-299.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): apply MovableList diffs deletes first, like Rust's apply_delta

Without moved children the delta was walked in order, which gave the ops
other IDs than Rust's and later deletes by ID missed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): skip map entries that only delete keys when comparing diff()

A child created in the range whose keys were all set and deleted again is
listed by Rust and omitted by loro.js, also on main and for any parent; see
context/loro-js-movable-list.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(loro.js): record snapshot completion on import and its cost

The performance context still described main's MovableList snapshot special
case and #1163 as open. Measurements are in context/loro-js-performance.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(loro.js): check MovableList transition inserts in one tree walk

canTransition walked the position tree once per insert op, which made
checkout, diff and revertTo O(insert ops * positions). Collect the runs and
check them in one containsIdRuns call, as the List branch does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): fix the suite's changeset and ignore empty diff() deltas

The changeset named a test:differential script that does not exist. Rust's
diff() can list a sequence with an empty delta where loro.js omits it
(context/loro-js-movable-list.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
zxch3n added a commit that referenced this pull request Sep 29, 2026
* test(loro.js): add a Rust differential suite for MovableList

Drive the Rust WASM build and loro.js with the same random multi-peer
edits, crossing engines on every sync, and require equal values, versions,
per-element metadata and events. Profiles that diverge on the current
MovableList model are recorded as known divergences. Design and findings:
context/loro-js-movable-list.md.

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

* test(loro.js): compare event effects and replicate unordered reverts

Event deltas that differ only in shape are checked by effect; reverts whose
op order Rust leaves to hash order are checked by value and replicated from
Rust; peers that both engines reject are retired; peer IDs are never reused;
undo stays unchained; the minimizer keeps those rules. See
context/loro-js-movable-list.md.

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

* test(loro.js): join shallow snapshots through Rust and keep replayed undo unchained

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

* test(loro.js): accept no-op sequence events and replicate reverts whose diff shape differs

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

* test(loro.js): skip shallow editors and start range exports at the shallow root

Both follow Rust issues listed in context/loro-js-movable-list.md.

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

* test(loro.js): measure revert container depths at the revert target

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

* test(loro.js): run the MovableList differential suite on the shared Rust reference

Load Rust through tests/support/rust-reference.ts like the other differential
suites, so it runs in `pnpm test` (and in CI's test-loro-js) and skips without
a WASM build. Drop the separate workflow and scripts. insert/delete now
converges on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): model MovableList like Rust (positions and LWW elements)

MovableList keeps a Fugue sequence of list items and elements whose
position and value are last-writer-wins registers, as Rust does. Moves of
concurrently deleted elements, deletes after moves, snapshots (list item,
element and last-set IDs, dead items), metadata getters, version
transitions, revertTo/applyDiff (Rust's apply_delta), undo and events now
match Rust. Snapshot-hydrated documents replay history before concurrent
imports, and a few shallow-snapshot incompatibilities are fixed. The
differential suite passes every profile. Design and remaining gaps:
context/loro-js-movable-list.md.

Rebuilt on current main: MovableList joins #1126's per-container snapshot
completion (its snapshot state names Rust IDs), applyDiff/revertTo/undo use
main's port of Rust #1138's apply_delta on the new model, and undo
transforms only over changes the UndoManager did not make.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): skip final syncs with a peer that left the run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): keep pending changes and follow Rust's shallow import rules

Decoding deferred snapshot history dropped pending changes. Shallow imports
now check the unknown part of each change like is_before_shallow_root, and an
import that only adds pending changes creates their root containers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): pin diff() of a MovableList child created in the range

main's diff() reports such a child by its ops, including map keys set and
deleted again, as Rust does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(loro.js): resolve concurrent MovableList op indices through a tracker

A causal view per concurrent op made importing concurrent moves O(n^2).
Like Rust's Tracker::checkout, keep a tracker version and a per-position
delta metric that moves incrementally between op versions; see
context/loro-js-movable-list.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(loro.js): describe the merged MovableList fixes in the changeset

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): make import and importBatch atomic

A failed import no longer leaves history, pending changes, version,
containers, state or events half applied. In-place history changes are
journaled and undone, state is rebuilt only when it had changed, events are
emitted after success.
Details: context/import-batch-atomicity.md ("loro.js").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): never walk a container's fields when comparing event values

A MovableList replaced by a plain value made the comparison enumerate the
container and hit its guarded element sequence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): fuzz MovableList in the multi-peer differential CI run

The Rust-model MovableList passes seeds 0-599 of the #1141 fuzz with all
oracles in lockstep with Rust; main failed 144 of seeds 0-299.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): apply MovableList diffs deletes first, like Rust's apply_delta

Without moved children the delta was walked in order, which gave the ops
other IDs than Rust's and later deletes by ID missed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): skip map entries that only delete keys when comparing diff()

A child created in the range whose keys were all set and deleted again is
listed by Rust and omitted by loro.js, also on main and for any parent; see
context/loro-js-movable-list.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(loro.js): record snapshot completion on import and its cost

The performance context still described main's MovableList snapshot special
case and #1163 as open. Measurements are in context/loro-js-performance.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(loro.js): check MovableList transition inserts in one tree walk

canTransition walked the position tree once per insert op, which made
checkout, diff and revertTo O(insert ops * positions). Collect the runs and
check them in one containsIdRuns call, as the List branch does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): fix the suite's changeset and ignore empty diff() deltas

The changeset named a test:differential script that does not exist. Rust's
diff() can list a sequence with an empty delta where loro.js omits it
(context/loro-js-movable-list.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(loro.js): journal snapshot-sequence writes instead of copying per import

Each import copied the whole snapshot-sequence table for rollback, so its cost
grew with the number of snapshot-hydrated containers. Record each write in the
import journal instead (context/import-batch-atomicity.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
zxch3n added a commit that referenced this pull request Sep 29, 2026
…) (#1168)

* test(loro.js): add a Rust differential suite for MovableList

Drive the Rust WASM build and loro.js with the same random multi-peer
edits, crossing engines on every sync, and require equal values, versions,
per-element metadata and events. Profiles that diverge on the current
MovableList model are recorded as known divergences. Design and findings:
context/loro-js-movable-list.md.

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

* test(loro.js): compare event effects and replicate unordered reverts

Event deltas that differ only in shape are checked by effect; reverts whose
op order Rust leaves to hash order are checked by value and replicated from
Rust; peers that both engines reject are retired; peer IDs are never reused;
undo stays unchained; the minimizer keeps those rules. See
context/loro-js-movable-list.md.

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

* test(loro.js): join shallow snapshots through Rust and keep replayed undo unchained

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

* test(loro.js): accept no-op sequence events and replicate reverts whose diff shape differs

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

* test(loro.js): skip shallow editors and start range exports at the shallow root

Both follow Rust issues listed in context/loro-js-movable-list.md.

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

* test(loro.js): measure revert container depths at the revert target

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

* test(loro.js): run the MovableList differential suite on the shared Rust reference

Load Rust through tests/support/rust-reference.ts like the other differential
suites, so it runs in `pnpm test` (and in CI's test-loro-js) and skips without
a WASM build. Drop the separate workflow and scripts. insert/delete now
converges on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): model MovableList like Rust (positions and LWW elements)

MovableList keeps a Fugue sequence of list items and elements whose
position and value are last-writer-wins registers, as Rust does. Moves of
concurrently deleted elements, deletes after moves, snapshots (list item,
element and last-set IDs, dead items), metadata getters, version
transitions, revertTo/applyDiff (Rust's apply_delta), undo and events now
match Rust. Snapshot-hydrated documents replay history before concurrent
imports, and a few shallow-snapshot incompatibilities are fixed. The
differential suite passes every profile. Design and remaining gaps:
context/loro-js-movable-list.md.

Rebuilt on current main: MovableList joins #1126's per-container snapshot
completion (its snapshot state names Rust IDs), applyDiff/revertTo/undo use
main's port of Rust #1138's apply_delta on the new model, and undo
transforms only over changes the UndoManager did not make.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): skip final syncs with a peer that left the run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): keep pending changes and follow Rust's shallow import rules

Decoding deferred snapshot history dropped pending changes. Shallow imports
now check the unknown part of each change like is_before_shallow_root, and an
import that only adds pending changes creates their root containers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): pin diff() of a MovableList child created in the range

main's diff() reports such a child by its ops, including map keys set and
deleted again, as Rust does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(loro.js): resolve concurrent MovableList op indices through a tracker

A causal view per concurrent op made importing concurrent moves O(n^2).
Like Rust's Tracker::checkout, keep a tracker version and a per-position
delta metric that moves incrementally between op versions; see
context/loro-js-movable-list.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(loro.js): describe the merged MovableList fixes in the changeset

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): make import and importBatch atomic

A failed import no longer leaves history, pending changes, version,
containers, state or events half applied. In-place history changes are
journaled and undone, state is rebuilt only when it had changed, events are
emitted after success.
Details: context/import-batch-atomicity.md ("loro.js").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): reject MovableList moves and sets of elements outside their history

Port of #1125; see context/loro-js-movable-list.md (Validation).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): never walk a container's fields when comparing event values

A MovableList replaced by a plain value made the comparison enumerate the
container and hit its guarded element sequence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): fuzz MovableList in the multi-peer differential CI run

The Rust-model MovableList passes seeds 0-599 of the #1141 fuzz with all
oracles in lockstep with Rust; main failed 144 of seeds 0-299.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(loro.js): apply MovableList diffs deletes first, like Rust's apply_delta

Without moved children the delta was walked in order, which gave the ops
other IDs than Rust's and later deletes by ID missed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): skip map entries that only delete keys when comparing diff()

A child created in the range whose keys were all set and deleted again is
listed by Rust and omitted by loro.js, also on main and for any parent; see
context/loro-js-movable-list.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(loro.js): record snapshot completion on import and its cost

The performance context still described main's MovableList snapshot special
case and #1163 as open. Measurements are in context/loro-js-performance.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(loro.js): check MovableList transition inserts in one tree walk

canTransition walked the position tree once per insert op, which made
checkout, diff and revertTo O(insert ops * positions). Collect the runs and
check them in one containsIdRuns call, as the List branch does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(loro.js): fix the suite's changeset and ignore empty diff() deltas

The changeset named a test:differential script that does not exist. Rust's
diff() can list a sequence with an empty delta where loro.js omits it
(context/loro-js-movable-list.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(loro.js): journal snapshot-sequence writes instead of copying per import

Each import copied the whole snapshot-sequence table for rollback, so its cost
grew with the number of snapshot-hydrated containers. Record each write in the
import journal instead (context/import-batch-atomicity.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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