Skip to content

chore: version packages - #2

Open
github-actions[bot] wants to merge 1 commit into
mainfrom
changeset-release/main
Open

github-actions[bot] wants to merge 1 commit into
mainfrom
changeset-release/main

Conversation

@github-actions

@github-actions github-actions Bot commented Mar 24, 2025 •

Copy link
Copy Markdown

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

loro.js@1.0.0

Major Changes

  • c0ff125: Rich text now uses Rust's position model. A mark's start and end anchors are elements of the text sequence, as in Rust (loro-crdt), so every insert, delete, and mark position loro.js writes or reads counts them.

    Breaking: loro.js reads rich-text history written by earlier versions with Rust's positions. Earlier versions wrote and read positions without the anchors, so every edit made after a mark in a document edited only with loro.js can now apply elsewhere, and styles can cover other text (for example, after marking ab in abcd, inserting X at 3 and deleting 1 gave acXd in earlier versions; those ops now read as bXcd, as Rust has always read them). Documents shared with loro-crdt peers had already diverged there (Rust read bXcd from loro.js's updates, loro.js read acdX from Rust's); they now agree with Rust. There is no version marker in the data: read "Upgrading from 0.2" in the README before upgrading.

    Behavior now follows Rust:

    • Text inserted where a mark starts or ends goes inside or outside the mark according to the mark's expand setting, as Rust decides it; concurrent edits at a mark boundary converge with Rust.
    • Breaking: mark and unmark throw when start >= end, and skip a mark that would change nothing, as Rust does.
    • Breaking: internal _-prefixed members of LoroText that were visible in the type declarations (_insertVisible, _applyMark, _styleRuns, _validateInsertPosition, _unicodePosition, _detachedStyleCounter) were removed.
    • applyDelta drops a style that inserted text would inherit when the insert does not list it, like Rust's apply_delta.
    • Undo and redo restore the attributes a mark replaced.
    • Snapshots write and read the anchors like Rust, so checking out an older version of a styled text gives the same result in both runtimes. Shallow snapshots null the values of styles that no text is left in, like Rust.
    • Inserting into a nested text that a snapshot loaded lazily no longer throws or drops the styles at the insert position, and checking out a version that has a mark's start but not its end no longer reports the whole text as inserted.

    Upgrading: a snapshot written by an earlier version keeps its current rich text, but its history reads like Rust, so a document loaded from it must not stay in use. Read the final state (text.toDelta()) with the old version and build a new document from it (reading it with 1.0 right after importing an old snapshot only works when no updates were stored after that snapshot: with a snapshot of bold ab in abcd and a later update that inserted X at 3 and deleted 1, the old version shows [a]cXd and 1.0 and Rust show [bX]cd), or import the old updates everywhere and accept Rust's reading. Exporting a shallow snapshot from a document loaded from an old snapshot switches that document itself to Rust's reading.

  • e74d64d: Text now follows Rust (loro-crdt) everywhere, including when reading documents written by earlier loro.js versions. This breaks documents edited only with loro.js that have concurrent text edits or stored cursors: read "Upgrading from 0.2" in the README before upgrading.

    • Breaking: concurrent text inserts are ordered like Rust (the fix is shared with fix(loro.js): stop a concurrent insert at the end of its origin's subtree loro-dev/loro#1139). Earlier versions could place an insert after a sibling's subtree behind later, unrelated concurrent text, so a history with such inserts can now read differently (for example béa9 instead of bé9a). Earlier loro.js replicas that merged such edits in different orders, and documents shared with loro-crdt peers, had already diverged; they now agree with Rust.
    • Breaking: text cursors follow Rust: a cursor reports its target's own offset, so a cursor encoded by an earlier version with side 1, or at the end of the text, resolves one character earlier (the end cursor of abc at 2 instead of 3). Other cursors resolve as before. The encoded origin is now the Unicode position, as in Rust. A cursor at the end or in empty text has no target, getCursorPos returns the target's own offset for every side, and a deleted target reports the length of the text before it with side -1. Get new cursors after upgrading.
    • An imported delete is applied by its position, as Rust does, instead of by its recorded start_id (Rust's WASM build up to 1.16.3 can record a wrong one around astral characters, Text deletes written by loro-crdt (WASM) ≤ 1.16.3 can record a wrong start_id around astral characters loro-dev/loro#1149).
    • checkout, diff, and revertTo no longer restore a character twice when two concurrent deletes removed it.
    • Consecutive inserts in one transaction are merged into one op, and a delete that spans several ID runs writes the last run first, as Rust does.

    Upgrading: updates and JSON updates written by an earlier version are read like Rust. A snapshot written by an earlier version keeps its current text, but its history reads like Rust, so a document loaded from it must not stay in use (exported updates, replicas loaded from updates, and checkouts can show other content). Exporting a shallow snapshot from such a document switches the document itself to Rust's reading. Migrate every replica the same way: read the final state with the old version and build a new document from it (reading it with 1.0 right after importing an old snapshot only works when that snapshot is the final version, with no updates stored after it), or import the old updates everywhere and accept Rust's reading. See "Upgrading from 0.2" in the README.

Minor Changes

  • 408d649: 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.js: an insert concurrent with a delete baked into an imported snapshot lands at the wrong position 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.

Patch Changes

  • 124b70b: 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.

  • 0ab8e38: checkout(frontiers) on a loro.js document now behaves like loro-crdt when frontiers is the current version: nothing changes, and the document stays (or becomes) attached when that version is the latest one. Before, doc.checkout(doc.frontiers()) detached the document, so later imports were not applied to its state. Checking out the latest version from an older one still leaves the document detached, as in Rust.

  • 4b04934: loro.js now treats a container as deleted when any ancestor is deleted, as loro-crdt does: isDeleted(), getPathToContainer(), and getCursorPos() account for deleted ancestors. Descendants of a deleted tree node are deleted: getNodes() omits them, and isNodeDeleted()/LoroTreeNode.isDeleted() return true. LoroTree.nodes() returns every node, including deleted ones, and getNodes() lists alive nodes breadth-first from the roots, matching Rust.

  • 106d9e5: export({ mode: "snapshot" }) on a detached loro.js document now encodes the latest state, version, and frontiers, as Rust and loro-crdt do. This includes a document imported lazily from a snapshot that received updates while detached. Before, the export wrote the checked-out state and version next to the full history, so an importer saw an old state as the latest version and a later checkout failed. The exporting document keeps its checkout.

  • 238988f: diff() and revertTo() in loro.js now restore containers that the range makes reachable again: a map key set back to an older child, a revived list element, or a revived tree node with its whole subtree and metadata, including a node moved out of a deleted ancestor. Their whole state is included, together with nested children, in parent-before-child order, as Rust does. Containers that stay reachable, such as a moved movable-list child, keep only their own changes. diff() leaves out containers that are unreachable at the target version.

    Mergeable children follow Rust main (fix: restore deleted mergeable children once on revert/apply_diff/undo loro-dev/loro#1134). diff() reports a re-activated mergeable child with its full state. applyDiff applies it incrementally by default, and applyDiff(diff, { fullState: true }) aligns the receiver's existing child with it: maps per key, counters by difference, text and lists by common prefix and suffix, and trees by node ID. revertTo and undo apply only the actual change, so the child keeps its identity. A local re-ensure emits only the parent marker. Import and checkout events now carry the full state of a re-activated child, as in loro-crdt. An event mirror (subscribe → applyDiff) should therefore pass { fullState: true } for import and checkout batches. With the default, the full state is applied on top of the mirror's hidden copy.

    Before writing anything, applyDiff checks every list delta against the list lengths the batch produces, including a hidden mergeable child's own length. It also dry-runs every tree item, so an out-of-range list delta or a tree item that cannot apply rejects the whole batch. Other malformed entries (text ranges, counter values, a diff whose type does not match its container) can still leave earlier entries applied, as before and as in Rust. Tree items apply in order, as in Rust: a create of a node that is alive moves it, a move of a missing or deleted node creates a new one, and a delete of a deleted node does nothing. Map children are always set as new containers. Tree diffs list moves first (parent first), then the topmost deletes, then revived subtrees, with each index counted in the tree the earlier items leave. A node that is hidden at the target version is reported as deleted. On a movable list, applyDiff moves an existing child container instead of creating an empty copy. Diffs, imports and checkouts of deep trees take linear time.

    Undo can revert movable-list moves, including several moves of the same element. The element returns next to its old physical neighbour when that neighbour has not moved since. The undo manager keeps only the tracked ranges its stacks can still use, at a commit cost independent of the stack length. Undoing a move writes a move op, and loro.js still places a remote move by its index rather than its causal position, so a move concurrent with an insert can diverge from Rust (the MovableList model rework, test(loro.js): add a Rust differential suite for MovableList loro-dev/loro#1132).

  • e74d64d: Fix a loro.js Text/List convergence bug with concurrent inserts. When an insert was concurrent with a run typed after the same character and with another concurrent element whose origin was further left, loro.js put the insert after that element instead of at the end of the run. Replicas could disagree with each other (depending on the order in which they received the updates) and with loro-crdt. loro.js now places these inserts where Rust does.

  • b365807: exportJsonUpdates now writes movable-list move and set element IDs as L{lamport}@{peer}, the format Rust and loro-crdt use, and importJsonUpdates accepts it. Before, loro.js wrote {lamport}@{peer}, which Rust rejects, and failed on Rust-produced JSON with counter is out of range: NaN. The old form is still accepted on import. The elem_id field type is now JsonIdLp.

  • 18924fa: exportJsonUpdates in loro.js now writes JSON that Rust and loro-crdt accept, in their format:

    • Counter ops carry Rust's value_type tag. As in Rust, whose counter is an f64, every counter op is written as "f64", and an "i64" value on import is read as an f64. Any finite value round-trips, including the i64 endpoints.
    • Binary values are written as plain number arrays, including the marker bytes of mergeable containers.

    The JSON schema has no binary type, so these arrays come back from importJsonUpdates as lists of numbers, in Rust as well. A byte array or a mergeable child therefore does not survive a JSON round trip as binary. Before, loro.js passed a Uint8Array through, but Rust rejected such JSON.

  • 99e7d84: loro.js now records the same local ops as loro-crdt for these API calls, so a document edited through either package gets the same op IDs and the same concurrent outcomes:

    • LoroMap.delete(key) records a delete even when the key is absent. Like in Rust, that delete can win against a concurrent set of the same key.
    • LoroCounter.increment(0) records an increment, and attaching a detached counter records an increment by its value, even 0.
  • 6e3e3c0: Importing a MovableList move or set that names an unknown element, an element of another list, an element outside the op's causal history, or (on a shallow document) an element deleted before the shallow root now fails and rolls back the whole import, as in Rust (fix: reject malformed movable list move/set ops on import instead of aborting loro-dev/loro#1125). A move or set of a deleted element is applied like a concurrent one on every import path.

  • 3435bac: Fix a loro.js checkout event for a child container in a List when the checkout falls back to replaying history, for example after updates were imported while the document was detached. The List diff could delete and re-insert an unchanged child container without sending its content, so a listener that starts an inserted child from empty lost that child's content. The child's whole state is now sent, as for any other re-attached child.

  • c4fd87f: loro.js shallow snapshots now pick the same root as loro-crdt: the latest single-head critical version of the requested frontiers and the latest version (Shallow snapshot exported at frontiers with an odd number of heads (≥3) cannot be imported into an empty document loro-dev/loro#1095). Before, loro.js used the requested version (or the meet of its heads) even when a retained op was concurrent with it. Checking out retained versions of such a snapshot and returning to the latest version could reorder concurrent Text/List elements, and the snapshot disagreed with a Rust export of the same document.

  • 4cf1d4a: Shallow snapshots exported by loro.js no longer contain containers deleted before the shallow root, such as a map child removed before the cut. Before, every container ever created was written to both the root state and the latest state. Tree node metadata is still kept for all nodes, including deleted ones, because a later move can revive such a node, and every mergeable child is kept, because re-ensuring it after its marker was deleted or replaced shows its state again. Re-exporting an older blob at its own root also drops these containers.

  • 1c75b3f: Fix checkout, diff, and undo on a loro.js shallow document when a map value or tree node placement was written in the shallow root commit and changed afterwards. When the root commit held more than one op, retreating to the root dropped the map key or tree node instead of restoring its root-time value, and getLastEditor returned undefined. A tree node deleted by a retained delete whose placement was trimmed from the shallow history now stays deleted.

  • 5e409d1: Fix checkout and diff on a loro.js document imported from a snapshot. Checking out an earlier version could return the latest value of a child container that had not been read yet, restore nothing for elements deleted before the snapshot, skip a delete when moving forward again, or throw duplicate sequence id. A transition now rebuilds only the Text and List containers it touches from their own history, once. Rust-created rich text whose history loro.js cannot replay keeps its snapshot state instead, so its latest state and exports no longer change after a checkout; its earlier versions still cannot restore text deleted before the snapshot. Checkout events for containers that had not been read are now correct, and a snapshot exported after history was loaded no longer drops containers that had not been read. A character deleted concurrently by two peers is no longer deleted twice during checkout, and a checkout or diff that throws leaves the document unchanged.

  • 489088c: Fix loro.js local tree moves and creates to match loro-crdt:

    • Creating or moving a node between two siblings that share a fractional index (for example two roots created concurrently at index 0) threw fractional index bounds are not ordered. It now gives the following siblings with that index new positions in the same transaction, as Rust does.
    • Moving a node to the position it already has, including move(node, parent) without an index when it is already last, node.move(parent), and moveAfter/moveBefore its current neighbor, no longer records an op.
    • moveAfter and moveBefore within the same parent counted the moved node itself, so moving a node after a later sibling threw tree index … is out of range or landed one slot off.
    • Moving a node under one of its own deleted descendants threw cannot move a tree node below itself or its descendant. A deleted node's parent is the deleted root, as in Rust, so the move is allowed (and the node becomes deleted).

loro-crdt@1.16.4

Patch Changes

  • 29553d5: Fix panics and lost events involving containers of a type this version doesn't know (created by a newer loro-crdt):

    • applyDiff and revertTo no longer trap the WASM instance when they would have to create such a container, e.g. a diff that inserts "🦜:cid:0@1:Unknown(9)" into a List, MovableList, Map or tree node meta, or that recreates a deleted container holding one. They throw a readable error and leave the doc unchanged. Moves of unknown elements that are already in a MovableList keep working. With { fullState: true }, re-activating a mergeable child that still holds such a container keeps it.
    • UndoManager.undo()/redo() throw the same error, without changing the doc, for a step that would recreate such a container. That step (with every edit merged into it) is dropped and its edits stay; the next call undoes the step before it, rebased over the dropped step's edits like over a remote peer's (previously the step was applied partially, losing data, and reported success).
    • Importing or checking out updates that create an unknown container and edit it in the same range no longer traps.
    • subscribe callbacks no longer lose a whole event batch when it touches an unknown container; only the events that can't be represented are left out.
    • getCursorPos returns undefined for cursors on Map, Tree, Counter or unknown containers instead of trapping.
    • parent() of a container whose parent has an unknown type throws a readable error instead of trapping.
    • Undo/redo no longer traps when an onPush cursor points to a missing container.
  • 229c8eb: Fix containers that stayed deleted after being revived. After isDeleted() was called on the metadata of a tree node under a deleted ancestor, moving the subtree back (locally or through an import) left that metadata reported as deleted, and edits to it threw until the document was reloaded. The same happened to a movable-list child revived by importing a concurrent move. Cached deletions of tree metadata and movable-list children are now dropped whenever a tree move or a movable-list change is applied.

  • e75df25: Speed up the first import that is concurrent with a local edit to the same map on a shallow doc. The checkout index no longer decodes every map in the shallow root up front; each map's shallow-root entries are added the first time a diff needs that map. On a doc with 100k maps in the shallow root this import went from ~240 ms to ~3.5 ms natively, and from ~2.8 s to ~6 ms in the WASM build.

  • 1eb14b3: Fix revertTo and undo adding a mergeable child's content twice after its map key was deleted. For example, revertTo a version before map.delete("s") turned a mergeable text "hello" into "hellohello", a counter 7 into 14, and a list ["keep"] into ["keep", "keep"]. They now write only the real difference and keep the child's characters, list elements and tree node ids.

    applyDiff accepts a new option, applyDiff(diff, { fullState: true }). Use it when applying the result of doc.diff() to a document that may still keep a deleted mergeable child the diff restores, such as the same document or a fork: the child's full content is then aligned with that hidden state instead of appended to it. Without the option, applyDiff behaves as before: that is right for documents without such hidden state, and for events forwarded to a document that shares the source's hidden state. Event shapes are unchanged. To build a mirror from scratch, use diff() or a snapshot.

    UndoManager also keeps a peer's own edit undoable after a remote undo restores the child it edited.

  • cdbc239: applyDiff on a LoroMovableList now keeps every child container that the diff moves. Before, a diff that moved two or more children at once, such as [{ delete: 2 }, { insert: ["🦜:" + b.id, "🦜:" + a.id] }] to swap two LoroText children, could drop one of them or put elements in the wrong order. UndoManager.undo() and redo() go through the same code, and after some movable-list edits they could panic with an OutOfBound error.

  • 5c7552d: Importing a malformed movable-list move or set op no longer aborts the process. These ops used to panic inside the document locks, poisoning them, and the process then aborted during unwind. Now:

    • A move/set whose element id is not an element of the same list in the op's causal history, or a move whose index is past the end of the list, makes import, importJsonUpdates and importBatch return an error. Nothing from that import is applied and the document stays usable. This also holds when the op depends on another change in the same import, which used to skip validation.
    • Insert, delete and move positions of 1,073,741,822 (about 2^30) or more in a List, MovableList or Text op are rejected when the op is decoded, instead of panicking during import. This is a hard limit on sequence length: it also applies when a document reads its own stored history or a snapshot. No peer could import such a document before either.
    • A move/set that targets an element deleted earlier in its own history is applied the same way a concurrent move/set of a deleted element is: the move brings the element back, and the set is not visible. Every import path, and a replay of the full history, gives the same result.

    Also fixed:

    • getChangeAtLamport returned undefined or the wrong change for changes that were loaded from a snapshot and not yet read. The change store misread the lamport range of stored blocks, a leftover from the block format change in 1.0. On a snapshot-loaded peer with many blocks, 42 of 669 sampled lamports returned undefined and 625 returned the wrong change; all 669 are now correct.
    • A rolled-back import could leave a cached change block behind that made the next import of the same changes panic with "counter should be continuous".
    • Imports of List and Tree ops no longer copy the peer's last change block on every import to keep a rollback copy. Many small List or Tree imports are about 30% faster.

    Performance: movable-list imports now check every move/set against the document's history. In release benchmarks that makes them about 2–8% slower for many small imports and 0–4% slower for a 120k-op import where about 70% of the ops are moves and sets.

  • 27982a5: Fix three rich-text bugs that a randomized comparison with loro.js found:

    • Text inserted next to a style anchor could land on the wrong side of it, depending on an internal cursor cache (native Rust and WASM): for example, after an unmark that changed nothing, typing right after a mark that does not expand after put the new text inside the mark.
    • In the WASM build, a delete spanning text from separate inserts could record a start_id naming the wrong characters when an astral character (for example, an emoji) ended one of the inserts. Imports apply deletes by position, but shallow snapshot imports and cursors on deleted text use the recorded IDs, so they could disagree with a full import. This fixes new ops only; deletes already in a history keep their IDs (Text deletes written by loro-crdt (WASM) ≤ 1.16.3 can record a wrong start_id around astral characters loro-dev/loro#1149).
    • In the WASM build, committing a transaction that merged backspaces over astral characters panicked with "Op/hint length mismatch" when the document had a subscriber or an UndoManager.
  • 824bb69: Fix a map value disappearing after importing a shallow snapshot and checking out its root. It happened when a key was rewritten after the root with the value it had at the root (for example by revertTo) and the op that wrote the root value was trimmed. Checking out a version also now updates the last editor of map entries and movable-list items whose value is equal at both versions, so getLastEditor is correct after a checkout, and shallow and state-only snapshots record the right writer in their root state. Snapshots exported by older versions can still carry the wrong writer in their root state; export them again from a full-history document to fix them.

  • 8488567: Re-exporting a shallow document at its own shallow root (a shallow snapshot at the same root, or a full snapshot of a shallow doc) now drops map/list containers that were deleted before the root. Shallow snapshots from older versions could still carry such containers, and re-exporting them used to keep that deleted content; it is now filtered with the same rules as a fresh shallow export, while tree node metadata that can still be revived is kept.

    If a shallow document's stored root state is internally inconsistent (a container's recorded parent does not match the container that references it) and some container looks unreachable, re-exporting it now returns an error instead of silently dropping a container that is still referenced. fork() still copies such a document as-is.

  • 8ff700f: Shallow and state-only snapshots no longer keep map/list child containers that were deleted before the shallow root. When the export carried a latest-state overlay (more than 256 retained ops, or any state-only export), the check for "containers created after the root" compared creation ids to the root frontiers for exact equality, so almost every stored container was retained in the root state; those snapshots are now smaller.

    Tree node metadata is the exception: a tree node deleted before the root can be revived by a later op (for example, moving a child out of a deleted parent), so the meta maps of all tree nodes, including deleted ones, stay in the root state. Shallow snapshots with at most 256 retained ops can therefore be larger than before when a tree has many deleted nodes. This also fixes shallow snapshots where such a revived node lost its metadata, a checkout into the retained history lost the revived node's children, and importing such a snapshot panicked with "Parent is not registered".

  • 892ed30: Reject imported Text ops whose positions lie past the end of the text. An insert, delete or mark past the end used to panic inside the document (a RuntimeError: unreachable trap in WASM, leaving the doc unusable). import, importBatch and importJsonUpdates now throw a decode error and leave the document unchanged and usable, like out-of-bounds List and MovableList ops (Importing a Text insert past the end of the text panics (richtext_state.rs:1653) loro-dev/loro#1160).

  • 6e294c8: Fix tree checkouts that depended on the path taken. Checking out from a version to a concurrent one could keep a tree node the target version does not have, or, with a subscriber, panic with get_index_by_tree_id(..).unwrap() in tree_state.rs when the diff created a node's child without the node. The tree diff now retreats and replays from the smallest lamport that differs between the two versions instead of from a replay base, which could be a non-critical meet that skipped earlier concurrent ops. Tree checkouts also no longer search the DAG for a replay base.

  • ce6498b: Fix a crash in documents loaded from a snapshot or created by fork(): isDeleted(), edits, getContainerById, and events touching the metadata of a tree node that was created under an already deleted parent threw RuntimeError: unreachable ("Parent is not registered"). The parent of such a container is now found from the change that created it. Also, an import rejected by state validation could leave loaded history pointing at containers and values its rollback had removed, so a later export, checkout, or import crashed (Honest import panics after an import rejected by state validation was rolled back (arena values) loro-dev/loro#1161). A change block that is present but cannot be parsed now fails with an internal error when a query needs it, instead of the container reading as deleted.

loro-crdt-map@1.16.4

Patch Changes

  • 29553d5: Fix panics and lost events involving containers of a type this version doesn't know (created by a newer loro-crdt):

    • applyDiff and revertTo no longer trap the WASM instance when they would have to create such a container, e.g. a diff that inserts "🦜:cid:0@1:Unknown(9)" into a List, MovableList, Map or tree node meta, or that recreates a deleted container holding one. They throw a readable error and leave the doc unchanged. Moves of unknown elements that are already in a MovableList keep working. With { fullState: true }, re-activating a mergeable child that still holds such a container keeps it.
    • UndoManager.undo()/redo() throw the same error, without changing the doc, for a step that would recreate such a container. That step (with every edit merged into it) is dropped and its edits stay; the next call undoes the step before it, rebased over the dropped step's edits like over a remote peer's (previously the step was applied partially, losing data, and reported success).
    • Importing or checking out updates that create an unknown container and edit it in the same range no longer traps.
    • subscribe callbacks no longer lose a whole event batch when it touches an unknown container; only the events that can't be represented are left out.
    • getCursorPos returns undefined for cursors on Map, Tree, Counter or unknown containers instead of trapping.
    • parent() of a container whose parent has an unknown type throws a readable error instead of trapping.
    • Undo/redo no longer traps when an onPush cursor points to a missing container.
  • 229c8eb: Fix containers that stayed deleted after being revived. After isDeleted() was called on the metadata of a tree node under a deleted ancestor, moving the subtree back (locally or through an import) left that metadata reported as deleted, and edits to it threw until the document was reloaded. The same happened to a movable-list child revived by importing a concurrent move. Cached deletions of tree metadata and movable-list children are now dropped whenever a tree move or a movable-list change is applied.

  • e75df25: Speed up the first import that is concurrent with a local edit to the same map on a shallow doc. The checkout index no longer decodes every map in the shallow root up front; each map's shallow-root entries are added the first time a diff needs that map. On a doc with 100k maps in the shallow root this import went from ~240 ms to ~3.5 ms natively, and from ~2.8 s to ~6 ms in the WASM build.

  • 1eb14b3: Fix revertTo and undo adding a mergeable child's content twice after its map key was deleted. For example, revertTo a version before map.delete("s") turned a mergeable text "hello" into "hellohello", a counter 7 into 14, and a list ["keep"] into ["keep", "keep"]. They now write only the real difference and keep the child's characters, list elements and tree node ids.

    applyDiff accepts a new option, applyDiff(diff, { fullState: true }). Use it when applying the result of doc.diff() to a document that may still keep a deleted mergeable child the diff restores, such as the same document or a fork: the child's full content is then aligned with that hidden state instead of appended to it. Without the option, applyDiff behaves as before: that is right for documents without such hidden state, and for events forwarded to a document that shares the source's hidden state. Event shapes are unchanged. To build a mirror from scratch, use diff() or a snapshot.

    UndoManager also keeps a peer's own edit undoable after a remote undo restores the child it edited.

  • cdbc239: applyDiff on a LoroMovableList now keeps every child container that the diff moves. Before, a diff that moved two or more children at once, such as [{ delete: 2 }, { insert: ["🦜:" + b.id, "🦜:" + a.id] }] to swap two LoroText children, could drop one of them or put elements in the wrong order. UndoManager.undo() and redo() go through the same code, and after some movable-list edits they could panic with an OutOfBound error.

  • 5c7552d: Importing a malformed movable-list move or set op no longer aborts the process. These ops used to panic inside the document locks, poisoning them, and the process then aborted during unwind. Now:

    • A move/set whose element id is not an element of the same list in the op's causal history, or a move whose index is past the end of the list, makes import, importJsonUpdates and importBatch return an error. Nothing from that import is applied and the document stays usable. This also holds when the op depends on another change in the same import, which used to skip validation.
    • Insert, delete and move positions of 1,073,741,822 (about 2^30) or more in a List, MovableList or Text op are rejected when the op is decoded, instead of panicking during import. This is a hard limit on sequence length: it also applies when a document reads its own stored history or a snapshot. No peer could import such a document before either.
    • A move/set that targets an element deleted earlier in its own history is applied the same way a concurrent move/set of a deleted element is: the move brings the element back, and the set is not visible. Every import path, and a replay of the full history, gives the same result.

    Also fixed:

    • getChangeAtLamport returned undefined or the wrong change for changes that were loaded from a snapshot and not yet read. The change store misread the lamport range of stored blocks, a leftover from the block format change in 1.0. On a snapshot-loaded peer with many blocks, 42 of 669 sampled lamports returned undefined and 625 returned the wrong change; all 669 are now correct.
    • A rolled-back import could leave a cached change block behind that made the next import of the same changes panic with "counter should be continuous".
    • Imports of List and Tree ops no longer copy the peer's last change block on every import to keep a rollback copy. Many small List or Tree imports are about 30% faster.

    Performance: movable-list imports now check every move/set against the document's history. In release benchmarks that makes them about 2–8% slower for many small imports and 0–4% slower for a 120k-op import where about 70% of the ops are moves and sets.

  • 27982a5: Fix three rich-text bugs that a randomized comparison with loro.js found:

    • Text inserted next to a style anchor could land on the wrong side of it, depending on an internal cursor cache (native Rust and WASM): for example, after an unmark that changed nothing, typing right after a mark that does not expand after put the new text inside the mark.
    • In the WASM build, a delete spanning text from separate inserts could record a start_id naming the wrong characters when an astral character (for example, an emoji) ended one of the inserts. Imports apply deletes by position, but shallow snapshot imports and cursors on deleted text use the recorded IDs, so they could disagree with a full import. This fixes new ops only; deletes already in a history keep their IDs (Text deletes written by loro-crdt (WASM) ≤ 1.16.3 can record a wrong start_id around astral characters loro-dev/loro#1149).
    • In the WASM build, committing a transaction that merged backspaces over astral characters panicked with "Op/hint length mismatch" when the document had a subscriber or an UndoManager.
  • 824bb69: Fix a map value disappearing after importing a shallow snapshot and checking out its root. It happened when a key was rewritten after the root with the value it had at the root (for example by revertTo) and the op that wrote the root value was trimmed. Checking out a version also now updates the last editor of map entries and movable-list items whose value is equal at both versions, so getLastEditor is correct after a checkout, and shallow and state-only snapshots record the right writer in their root state. Snapshots exported by older versions can still carry the wrong writer in their root state; export them again from a full-history document to fix them.

  • 8488567: Re-exporting a shallow document at its own shallow root (a shallow snapshot at the same root, or a full snapshot of a shallow doc) now drops map/list containers that were deleted before the root. Shallow snapshots from older versions could still carry such containers, and re-exporting them used to keep that deleted content; it is now filtered with the same rules as a fresh shallow export, while tree node metadata that can still be revived is kept.

    If a shallow document's stored root state is internally inconsistent (a container's recorded parent does not match the container that references it) and some container looks unreachable, re-exporting it now returns an error instead of silently dropping a container that is still referenced. fork() still copies such a document as-is.

  • 8ff700f: Shallow and state-only snapshots no longer keep map/list child containers that were deleted before the shallow root. When the export carried a latest-state overlay (more than 256 retained ops, or any state-only export), the check for "containers created after the root" compared creation ids to the root frontiers for exact equality, so almost every stored container was retained in the root state; those snapshots are now smaller.

    Tree node metadata is the exception: a tree node deleted before the root can be revived by a later op (for example, moving a child out of a deleted parent), so the meta maps of all tree nodes, including deleted ones, stay in the root state. Shallow snapshots with at most 256 retained ops can therefore be larger than before when a tree has many deleted nodes. This also fixes shallow snapshots where such a revived node lost its metadata, a checkout into the retained history lost the revived node's children, and importing such a snapshot panicked with "Parent is not registered".

  • 892ed30: Reject imported Text ops whose positions lie past the end of the text. An insert, delete or mark past the end used to panic inside the document (a RuntimeError: unreachable trap in WASM, leaving the doc unusable). import, importBatch and importJsonUpdates now throw a decode error and leave the document unchanged and usable, like out-of-bounds List and MovableList ops (Importing a Text insert past the end of the text panics (richtext_state.rs:1653) loro-dev/loro#1160).

  • 6e294c8: Fix tree checkouts that depended on the path taken. Checking out from a version to a concurrent one could keep a tree node the target version does not have, or, with a subscriber, panic with get_index_by_tree_id(..).unwrap() in tree_state.rs when the diff created a node's child without the node. The tree diff now retreats and replays from the smallest lamport that differs between the two versions instead of from a replay base, which could be a non-critical meet that skipped earlier concurrent ops. Tree checkouts also no longer search the DAG for a replay base.

  • ce6498b: Fix a crash in documents loaded from a snapshot or created by fork(): isDeleted(), edits, getContainerById, and events touching the metadata of a tree node that was created under an already deleted parent threw RuntimeError: unreachable ("Parent is not registered"). The parent of such a container is now found from the change that created it. Also, an import rejected by state validation could leave loaded history pointing at containers and values its rollback had removed, so a later export, checkout, or import crashed (Honest import panics after an import rejected by state validation was rolled back (arena values) loro-dev/loro#1161). A change block that is present but cannot be parsed now fails with an internal error when a query needs it, instead of the container reading as deleted.

@github-actions
github-actions Bot force-pushed the changeset-release/main branch 3 times, most recently from 7bc6d82 to b4f06ce Compare April 2, 2025 06:59
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 3 times, most recently from e8168b9 to d35bcd8 Compare April 9, 2025 06:56
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 5 times, most recently from e7d9457 to c029cc2 Compare May 26, 2025 06:50
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 2 times, most recently from abad8aa to 57ea7cc Compare November 11, 2025 12:51
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 3 times, most recently from 3a29ba4 to 7dbfe59 Compare November 27, 2025 00:22
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from 7dbfe59 to 4eb96e5 Compare November 27, 2025 22:49
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from 4eb96e5 to aa7883e Compare January 8, 2026 14:00
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from aa7883e to 6d71b1b Compare January 22, 2026 20:47
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from 6d71b1b to 510ded7 Compare March 16, 2026 13:45
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 3 times, most recently from 8aa8ac5 to 342393f Compare March 31, 2026 19:40
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from 342393f to b505595 Compare April 9, 2026 15:58
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 6 times, most recently from 7e20a9c to 3ccbb5c Compare April 27, 2026 07:12
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 5 times, most recently from 67dbe4c to ed31ebc Compare June 21, 2026 12:53
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from ed31ebc to 0b06612 Compare June 22, 2026 16:51
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from 0b06612 to 1e9d7f0 Compare July 7, 2026 14:12
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 3 times, most recently from 0b84f21 to 713412b Compare July 21, 2026 15:00
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 4 times, most recently from bf6a707 to 10140a4 Compare August 11, 2026 13:01
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 5 times, most recently from 3053b83 to f08974f Compare September 9, 2026 23:56
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 4 times, most recently from 9516b45 to 9fe1ab1 Compare September 28, 2026 04:11
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 6 times, most recently from ed36068 to 22be639 Compare September 29, 2026 12:23
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.

0 participants