Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7ebbd0e
test(loro.js): add a Rust differential suite for MovableList
zxch3n Sep 27, 2026
dfe945d
test(loro.js): compare event effects and replicate unordered reverts
zxch3n Sep 27, 2026
2733ca6
Merge remote-tracking branch 'origin/main' into test/loro-js-movable-…
zxch3n Sep 29, 2026
47eb6d7
test(loro.js): join shallow snapshots through Rust and keep replayed …
zxch3n Sep 27, 2026
4ceb7c7
test(loro.js): accept no-op sequence events and replicate reverts who…
zxch3n Sep 27, 2026
17e8f2c
test(loro.js): skip shallow editors and start range exports at the sh…
zxch3n Sep 27, 2026
95006a5
test(loro.js): measure revert container depths at the revert target
zxch3n Sep 27, 2026
17366fd
test(loro.js): run the MovableList differential suite on the shared R…
zxch3n Sep 29, 2026
ca3f781
fix(loro.js): model MovableList like Rust (positions and LWW elements)
zxch3n Sep 27, 2026
f5dadab
test(loro.js): skip final syncs with a peer that left the run
zxch3n Sep 29, 2026
3c05592
Merge branch 'test/loro-js-movable-list-differential' into fix/loro-j…
zxch3n Sep 29, 2026
0af5b74
fix(loro.js): keep pending changes and follow Rust's shallow import r…
zxch3n Sep 27, 2026
a77fd0e
test(loro.js): pin diff() of a MovableList child created in the range
zxch3n Sep 27, 2026
b254f15
perf(loro.js): resolve concurrent MovableList op indices through a tr…
zxch3n Sep 27, 2026
7f56f79
chore(loro.js): describe the merged MovableList fixes in the changeset
zxch3n Sep 29, 2026
972f856
fix(loro.js): never walk a container's fields when comparing event va…
zxch3n Sep 29, 2026
699b5a5
test(loro.js): fuzz MovableList in the multi-peer differential CI run
zxch3n Sep 29, 2026
fe2e6dd
fix(loro.js): apply MovableList diffs deletes first, like Rust's appl…
zxch3n Sep 29, 2026
22d811f
test(loro.js): skip map entries that only delete keys when comparing …
zxch3n Sep 29, 2026
1c9e695
Merge branch 'test/loro-js-movable-list-differential' into fix/loro-j…
zxch3n Sep 29, 2026
224b0a4
docs(loro.js): record snapshot completion on import and its cost
zxch3n Sep 29, 2026
06a5fba
perf(loro.js): check MovableList transition inserts in one tree walk
zxch3n Sep 29, 2026
4e99ba6
test(loro.js): fix the suite's changeset and ignore empty diff() deltas
zxch3n Sep 29, 2026
418dfb2
Merge branch 'test/loro-js-movable-list-differential' into fix/loro-j…
zxch3n Sep 29, 2026
0ca7f38
Merge main into #1166 (after squash of the layer below)
zxch3n Sep 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/loro-js-movable-list-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"loro.js": minor
---

`LoroMovableList` now follows Rust's model: list items (positions) and elements are separate, and an element's position and value are last-writer-wins registers. This fixes convergence with Rust and `loro-crdt`:

- A move of an element that another peer deleted concurrently keeps the element (`["a","c","B"]`, as in Rust), and a delete after a move removes the moved element.
- Snapshots write and read Rust's MovableList layout, including list item IDs, element IDs, last-set IDs and dead list items. Before, loro.js wrote every position and last-set ID as the element ID.
- `getLastEditorAt` and `getLastMoverAt` return the last setter and mover instead of the creator.
- Checkout, `diff`, `revertTo`, `applyDiff` and undo select winners the way Rust does. Undoing a `move` or `set` reinserts the old value, and undo transforms its inverse only over changes the `UndoManager` did not make, as Rust's does.
- `set` records an op even when the value is unchanged, and `move(i, i)` is a no-op, as in Rust. `getIdAt` and `getCursor` of a MovableList anchor the list item, like Rust's cursor.
- Importing concurrent moves resolves their indices incrementally, like Rust's tracker, instead of one causal view per op.

Also fixed while making loro.js match Rust:

- A document loaded from a snapshot rebuilds each container that an import touches concurrently with the snapshot from that container's history first (as a checkout already does), so List, Text and MovableList place such changes where Rust does (loro-dev/loro#1163). A MovableList snapshot takes part in this completion like Text and List.
- Changes imported as pending while a snapshot's history was still undecoded were lost once the history was decoded (for example by a checkout or a history query); they now stay pending.
- Shallow documents follow Rust's rule for outdated imports: they accept changes rooted at the empty version when the shallow root is empty, skip changes already inside the shallow root, and reject a change whose part after the root depends on trimmed history. A shallow snapshot rooted at a peer's first op now writes the start version that Rust expects.
- An import that only adds pending changes creates the root containers they edit, as Rust does.
- Importing a shallow snapshot emits events for containers that exist only in the shallow root state.
16 changes: 11 additions & 5 deletions context/loro-js-differential-fuzz.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# loro.js Differential Fuzzing

Verified against code 2026-09-29.
Verified against code 2026-09-30.

`loro-js/tests/fuzz/differential.ts` drives several `loro.js` peers through
random edits, update exchanges, and checkouts, and checks them against
Expand Down Expand Up @@ -76,8 +76,9 @@ prints each action as it runs, which finds hangs. The report groups failures by
signature (oracle plus first differing root or normalized error) and stores the
minimized action list and a trace written as `loro.js` API calls.

CI runs 120 fixed seeds of Map/List/Text/Counter, and 120 more with nested
child containers and snapshot imports, with the replay, checkout,
CI runs 120 fixed seeds of Map/List/Text/Counter, 120 with MovableList and
nested child containers, and 120 more with nested child containers and snapshot
imports, with the replay, checkout,
full-snapshot, and event oracles. A seed that fails on `main` for a known
reason goes into `pending` with that reason until its fix lands; both lists are
empty on 2026-09-29 (`main` at #1131).
Expand All @@ -98,6 +99,13 @@ re-inserts (#1150). With nested children and snapshot imports, seeds 297,
315, 344, 422, 452, and 559 of 0–599 fail on `main` in the event oracle;
#1150 fixes all six.

MovableList, fixed by the Rust-model rewrite (loro-dev/loro#1132 and its
follow-ups, see [loro-js-movable-list.md](loro-js-movable-list.md)): with
`FUZZ_FEATURES=map,list,text,counter,movableList,nested`, all oracles and
lockstep with Rust, 144 of seeds 0–299 failed on `main` on 2026-09-30 (98
checkout-vs-replay, 29 lockstep); on the rewrite seeds 0–599 pass, and CI runs
120 of them without Rust.

Still open, so `CI_RUNS` does not enable them yet:

- Tree convergence: `loro.js` applies tree moves per node, last writer wins,
Expand All @@ -121,8 +129,6 @@ Still open, so `CI_RUNS` does not enable them yet:
(`DocState::get_path`); `loro.js` still sends them. Needs #1147's liveness.
- Shallow snapshots in the harness need #1144 (the root check fails first on
every seed without it).
- MovableList: `loro.js` skips a movable-list `set` to the current value,
which Rust records.

## Rust-side findings

Expand Down
Loading
Loading