Skip to content

[pull] main from loro-dev:main - #208

Merged
pull[bot] merged 4 commits into
Superoldman96:mainfrom
loro-dev:main
Sep 30, 2026
Merged

pull[bot] merged 4 commits into
Superoldman96:mainfrom
loro-dev:main

Conversation

@pull

@pull pull Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

zxch3n and others added 4 commits September 30, 2026 14:57
, #1151; #1149 documented) (#1171)

* fix: reject imports that reuse local op ids with different content (#1118)

Importing history from a client that shared a peer id trimmed the "known"
part by version vector and applied the tail on top of different local
history: it panicked in state apply (poisoning the doc, aborting on drop)
or silently scrambled text. Compare the overlap with local history before
anything is mutated and return UsedOpID; also reject changes that skip
counters of their peer instead of tripping a DAG assertion.

Rationale: context/import-peer-id-reuse.md

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

* fix: drop subscriber callbacks outside the subscriber set lock (#1162)

Dropping a subscriber's callback runs arbitrary code; when it owns another
Subscription or an UndoManager on the same doc, their drop re-entered the
set's mutex on the same thread and deadlocked. The unsubscribe closure,
retain's dropped-subscriber sweep, and remove() now move the removed
subscribers out and drop them after releasing the lock.

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

* fix: export several non-contiguous spans of one peer in updates_in_range (#1155)

export_blocks_in_range inserted every selected change into one temporary
change store, which requires each peer's counters to be contiguous, so two
non-contiguous spans of one peer panicked under the op log lock. Merge the
ranges per peer and put the k-th range of every peer into the k-th store;
the blocks of all stores are written one after another.

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

* fix: accept a style start without its end in snapshots (#1165)

A version addressed with op-level frontiers can hold a mark's StyleStart but
not its StyleEnd. checkout keeps the lone start anchor, but the fast state
decoder rejected it, and the first read of a fork or snapshot panicked under
the doc locks. Decode it the way checkout keeps it (no style range until the
end arrives), relax the debug-only pair checks accordingly, leave it alone
in dead-style redaction, and only advance a shallow root past a StyleStart
when the StyleEnd exists in the op log.

Rationale: context/shallow-snapshot-style-redaction.md

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

* docs: migration advice for legacy text delete start_ids (#1149)

Deletes written by WASM loro-crdt <= 1.16.3 can name the wrong characters in
start_id. The ops are immutable and the affected readers (shallow-snapshot
placeholders, cursors on deleted text) have nothing but start_id, so the
history is not repaired in code. Document which paths stay correct and how
to avoid the wrong ones, and pin that advice with a test on a forged legacy
history (plus the still-divergent shallow root before the delete).

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

* fix: show empty root containers touched by imported history (#1156)

A root is visible when the state store has it. The writing peer creates it
with its first local op, but an import only did so when the root's net diff
was non-empty, so text inserted then deleted showed {"t": ""} live and {}
in a replay. After an attached import, and after a checkout to the latest
version, create the state of every non-mergeable root the diff calculator
touched.

Getting a root handle also makes it visible, by design; that half of the
issue's random workload is an API decision and is documented, not changed.

Rationale: context/root-container-visibility.md

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

* fix: keep arena indices when a failed import is rolled back (#1164)

A failed import's arena rollback freed the indices of containers registered
during the import, while DocState store entries, the dead-container cache and
handlers on other threads still referred to them. The next container
registered at a freed index inherited the old state. The rollback now keeps
every index and drops only what the failed import may have told the arena
(parent links of new normal containers, cached depths), plus values and text;
every rollback path also clears the dead-container cache.

Rationale: context/failed-import-arena-indices.md.

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

* fix: emit tree events for revived subtrees on import (#1157)

In the Linear and ImportGreaterUpdates diff modes the tree diff is the raw
ops, and TreeState::apply_diff_and_convert labeled a move that brought a
node out of a deleted subtree as a single Create. The nodes below it got
no Create, so a tree rebuilt from events lost them. In Linear mode a move
of an alive node under a deleted one also emitted no Delete, and in
ImportGreaterUpdates mode Create ops skipped the cycle check that
apply_diff applies.

Raw Create/Move ops are now classified by whether the target is alive
before and after the op, and a revived node is followed by a Create for
each node of its subtree. Checkout-mode diffs are unchanged. Rationale and
the local-event gap: context/tree-events.md.

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

* fix: make apply_diff/revert_to all-or-nothing (#1154)

A batch that fails is rolled back: its ops are taken out of the
transaction (uncommitted edits before it stay), undone from the state by
a checkout through a temporary change under an import rollback scope,
and removed from the DAG. No event is emitted. A stale tree diff naming
a missing parent now returns an error instead of panicking.

Rationale, reused op ids and the multi-thread fallback:
context/apply-diff-atomicity.md.

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

* fix(loro-crdt): keep the known keys of a map event with an unknown child (#1151)

A map event that set a key to a container of an unknown type (created by a
newer loro-crdt) failed to convert as a whole, so edits to known keys in the
same event never reached subscribers. Leave out only the unknown child's
entry. List events holding an unknown child are still left out whole, and
delivering the unknown child itself needs a public representation (an API
decision); see context/unknown-containers.md.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The two "major" changesets (rich-text anchors, text interop) bumped
loro.js from 0.2.1 to 1.0.0. Keep it on 0.x: mark them minor and refer
to the release as 0.3 in the changesets and the README upgrade guide.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t stored data (#1172)

* fix: export an unprunable shallow root verbatim

Re-exporting a shallow doc at its own root filtered the cached root state
and returned an error when the state was inconsistent. The error was not
cached, so every export failed again and only fork() worked. Fall back to
the verbatim root, as exporters before #1123 did: nothing referenced is
dropped. See context/internal-encoding.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: report an unparsable change block instead of panicking in the creator resolver

The resolver runs under the state lock from queries that cannot return an
error. It now records the block and answers CreatorOp::Corrupt; import,
export and checkout then return a DecodeError. See
context/arena-parent-links.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: check an unparsable change block only where an error can be returned

The check sat in _checkout_without_emitting, so once a block was recorded,
undo, checkout_to_latest and a detached fork panicked on the new error
(they unwrap it). Check it in checkout, diff, revert_to, import and
export instead; merge propagates the export error.

Every reader of the change store now records a block it cannot parse, not
only the creator resolver, and export checks again when it is done, so it
cannot return updates that silently miss the block.

See context/arena-parent-links.md, "A block that cannot be parsed".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@pull pull Bot locked and limited conversation to collaborators Sep 30, 2026
@pull pull Bot added the ⤵️ pull label Sep 30, 2026
@pull
pull Bot merged commit 45708d0 into Superoldman96:main Sep 30, 2026
1 of 2 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant