Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 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
b13cd31
fix(loro.js): make import and importBatch atomic
zxch3n Sep 27, 2026
972f856
fix(loro.js): never walk a container's fields when comparing event va…
zxch3n Sep 29, 2026
5481b27
Merge branch 'fix/loro-js-movable-list-model' into fix/loro-js-atomic…
zxch3n Sep 29, 2026
699b5a5
test(loro.js): fuzz MovableList in the multi-peer differential CI run
zxch3n Sep 29, 2026
754585e
Merge branch 'fix/loro-js-movable-list-model' into fix/loro-js-atomic…
zxch3n Sep 29, 2026
fe2e6dd
fix(loro.js): apply MovableList diffs deletes first, like Rust's appl…
zxch3n Sep 29, 2026
e701dc6
Merge branch 'fix/loro-js-movable-list-model' into fix/loro-js-atomic…
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
985ae4f
Merge branch 'fix/loro-js-movable-list-model' into fix/loro-js-atomic…
zxch3n Sep 29, 2026
224b0a4
docs(loro.js): record snapshot completion on import and its cost
zxch3n Sep 29, 2026
cc2db1b
Merge branch 'fix/loro-js-movable-list-model' into fix/loro-js-atomic…
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
686f1f7
Merge branch 'fix/loro-js-movable-list-model' into fix/loro-js-atomic…
zxch3n Sep 29, 2026
36a1232
perf(loro.js): journal snapshot-sequence writes instead of copying pe…
zxch3n Sep 29, 2026
1f7c74a
Merge main into #1167 (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
5 changes: 5 additions & 0 deletions .changeset/loro-js-atomic-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"loro.js": patch
---

`import`, `importBatch` and `importJsonUpdates` are atomic: when an import fails, history, pending changes, version, containers and state are restored, and no event is emitted, so subscribers and `UndoManager` see nothing of it. A multi-blob `importBatch` imports every blob or none.
34 changes: 34 additions & 0 deletions context/import-batch-atomicity.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,37 @@ regression on out-of-order batches, where every blob parks and is later unlocked
`import_batch_failure_leaves_doc_attached_and_unchanged`.
- `crates/loro-internal/src/oplog/pending_changes.rs`: the `import_batch_*` regressions
that assert the doc is attached after a batch.

## loro.js

Verified against code 2026-09-30. `loro-js/src/runtime/document.ts` gives
`import`, `importBatch` and `importJsonUpdates` the same all-or-nothing contract
through `LoroDoc.#atomically`:

- While an import runs, every in-place change to history structures (records,
per-peer arrays and ends, merged records, Map/Tree operation indexes,
containers and roots) and to the snapshot-sequence entries
(`#journalSnapshotSequence`) pushes an undo closure onto `#importUndo`. The
snapshot-sequence table can hold one entry per hydrated Text/List/MovableList,
so copying it made every import O(hydrated containers). Fields the import
replaces wholesale (frontiers, pending changes, deferred snapshot
history/state, shallow root and its entry index, counters) are saved by
reference or copied, since they are small.
- On failure the closures run in reverse and the saved fields are restored.
If the import had started to change container state (`#applyRecords`,
`#hydrateState` or `#rebuildFromHistory` ran), state is rebuilt from the
restored history at the previous version. The rebuild only happens on this
failure path. The per-container record index is invalidated. A snapshot
container that the import completed from its own history first
(`#prepareSnapshotImport`) keeps that replay, which equals its snapshot
state, and gets its snapshot entry back.
- Importing into a pristine document skips the journal: a failure empties it
again.
- `import` and `importBatch` emit their event only after the import succeeded,
so subscribers and `UndoManager` never see a partial import. A multi-blob
batch fails as a whole.
- `checkout`/`attach` that fail (for example on a forged op a detached import
recorded) restore the previous version and attachment instead of leaving the
document half switched (`#transitionTo`, from loro-dev/loro#1126).

Tests: `loro-js/tests/import-atomicity.test.ts`.
7 changes: 4 additions & 3 deletions context/loro-js-movable-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ reproduce, the loro.js model that implements them, and how the differential
suite checks the two against each other. Complexity rules still come from
[loro-js-performance.md](loro-js-performance.md).

Status: the model below is implemented. Import validation and atomic rollback
("Validation and import atomicity") land in follow-up changes.
Status: the model below and atomic imports are implemented. Import validation
("Validation and import atomicity") lands in a follow-up change.

## Rust semantics (the contract)

Expand Down Expand Up @@ -211,7 +211,8 @@ Rust's rule for `from_move` inserts that transforms leave behind

## Validation and import atomicity

Planned; not implemented yet. Import validation follows Rust #1125. A `Move`/`Set` whose element is unknown,
Atomicity is implemented ([import-batch-atomicity.md](import-batch-atomicity.md),
"loro.js"). Validation is planned. It follows Rust #1125. A `Move`/`Set` whose element is unknown,
lives in another container, is outside the op's causal history, or (on a
shallow doc) was deleted before the root is rejected with an error. So is a
`from`/`to` outside the op-index range of its causal view. A `Move`/`Set` of a
Expand Down
4 changes: 4 additions & 0 deletions context/loro-js-performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ JavaScript constant factor.
produce the final event.
- A pending transaction stores its accumulated operation length and causal
version incrementally. Never recover either by reducing all pending ops.
- An import journals its history changes (a few undo closures per imported
change) so a failure can roll back; importing into an empty document skips
the journal. Only a failure after state changed pays for a history replay
([import-batch-atomicity.md](import-batch-atomicity.md)).
- Plain Text/List elements allocate delete, value, and move metadata only when
an operation needs it; Text style metadata lives in the range index. A
multi-scalar Text insertion stores its string and UTF-16 boundaries once in a
Expand Down
Loading
Loading