Repository navigation
chore: version packages - #2
Open
github-actions[bot] wants to merge 1 commit into
Open
github-actions[bot] wants to merge 1 commit into
github-actions[bot] wants to merge 1 commit into
Conversation
github-actions
Bot
force-pushed
the
changeset-release/main
branch
3 times, most recently
from
April 2, 2025 06:59
7bc6d82 to
b4f06ce
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
3 times, most recently
from
April 9, 2025 06:56
e8168b9 to
d35bcd8
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
5 times, most recently
from
May 26, 2025 06:50
e7d9457 to
c029cc2
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
2 times, most recently
from
November 11, 2025 12:51
abad8aa to
57ea7cc
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
3 times, most recently
from
November 27, 2025 00:22
3a29ba4 to
7dbfe59
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
November 27, 2025 22:49
7dbfe59 to
4eb96e5
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
January 8, 2026 14:00
4eb96e5 to
aa7883e
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
January 22, 2026 20:47
aa7883e to
6d71b1b
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
March 16, 2026 13:45
6d71b1b to
510ded7
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
3 times, most recently
from
March 31, 2026 19:40
8aa8ac5 to
342393f
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
April 9, 2026 15:58
342393f to
b505595
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
6 times, most recently
from
April 27, 2026 07:12
7e20a9c to
3ccbb5c
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
5 times, most recently
from
June 21, 2026 12:53
67dbe4c to
ed31ebc
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
June 22, 2026 16:51
ed31ebc to
0b06612
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 7, 2026 14:12
0b06612 to
1e9d7f0
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
3 times, most recently
from
July 21, 2026 15:00
0b84f21 to
713412b
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
4 times, most recently
from
August 11, 2026 13:01
bf6a707 to
10140a4
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
5 times, most recently
from
September 9, 2026 23:56
3053b83 to
f08974f
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
4 times, most recently
from
September 28, 2026 04:11
9516b45 to
9fe1ab1
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
6 times, most recently
from
September 29, 2026 12:23
ed36068 to
22be639
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
abinabcd, insertingXat 3 and deleting 1 gaveacXdin earlier versions; those ops now read asbXcd, as Rust has always read them). Documents shared withloro-crdtpeers had already diverged there (Rust readbXcdfrom loro.js's updates, loro.js readacdXfrom 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:
markandunmarkthrow whenstart >= end, and skip a mark that would change nothing, as Rust does._-prefixed members ofLoroTextthat were visible in the type declarations (_insertVisible,_applyMark,_styleRuns,_validateInsertPosition,_unicodePosition,_detachedStyleCounter) were removed.applyDeltadrops a style that inserted text would inherit when the insert does not list it, like Rust'sapply_delta.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 boldabinabcdand a later update that insertedXat 3 and deleted 1, the old version shows[a]cXdand 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.béa9instead ofbé9a). Earlier loro.js replicas that merged such edits in different orders, and documents shared withloro-crdtpeers, had already diverged; they now agree with Rust.abcat 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,getCursorPosreturns 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.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, andrevertTono longer restore a character twice when two concurrent deletes removed it.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:
LoroMovableListnow 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 andloro-crdt:["a","c","B"], as in Rust), and a delete after a move removes the moved element.getLastEditorAtandgetLastMoverAtreturn the last setter and mover instead of the creator.diff,revertTo,applyDiffand undo select winners the way Rust does. Undoing amoveorsetreinserts the old value, and undo transforms its inverse only over changes theUndoManagerdid not make, as Rust's does.setrecords an op even when the value is unchanged, andmove(i, i)is a no-op, as in Rust.getIdAtandgetCursorof a MovableList anchor the list item, like Rust's cursor.Also fixed while making loro.js match Rust:
Patch Changes
124b70b:
import,importBatchandimportJsonUpdatesare atomic: when an import fails, history, pending changes, version, containers and state are restored, and no event is emitted, so subscribers andUndoManagersee nothing of it. A multi-blobimportBatchimports every blob or none.0ab8e38:
checkout(frontiers)on aloro.jsdocument now behaves likeloro-crdtwhenfrontiersis 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.jsnow treats a container as deleted when any ancestor is deleted, asloro-crdtdoes:isDeleted(),getPathToContainer(), andgetCursorPos()account for deleted ancestors. Descendants of a deleted tree node are deleted:getNodes()omits them, andisNodeDeleted()/LoroTreeNode.isDeleted()returntrue.LoroTree.nodes()returns every node, including deleted ones, andgetNodes()lists alive nodes breadth-first from the roots, matching Rust.106d9e5:
export({ mode: "snapshot" })on a detachedloro.jsdocument now encodes the latest state, version, and frontiers, as Rust andloro-crdtdo. 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()andrevertTo()inloro.jsnow 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.applyDiffapplies it incrementally by default, andapplyDiff(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.revertToand 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 }forimportandcheckoutbatches. With the default, the full state is applied on top of the mirror's hidden copy.Before writing anything,
applyDiffchecks 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,applyDiffmoves 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.jsText/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.jsput 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 withloro-crdt.loro.jsnow places these inserts where Rust does.b365807:
exportJsonUpdatesnow writes movable-list move and set element IDs asL{lamport}@{peer}, the format Rust andloro-crdtuse, andimportJsonUpdatesaccepts it. Before,loro.jswrote{lamport}@{peer}, which Rust rejects, and failed on Rust-produced JSON withcounter is out of range: NaN. The old form is still accepted on import. Theelem_idfield type is nowJsonIdLp.18924fa:
exportJsonUpdatesinloro.jsnow writes JSON that Rust andloro-crdtaccept, in their format:value_typetag. 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.The JSON schema has no binary type, so these arrays come back from
importJsonUpdatesas 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.jspassed aUint8Arraythrough, but Rust rejected such JSON.99e7d84:
loro.jsnow records the same local ops asloro-crdtfor 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 concurrentsetof 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.jscheckout 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.jsshallow snapshots now pick the same root asloro-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.jsused 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.jsno 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 aloro.jsshallow 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, andgetLastEditorreturnedundefined. A tree node deleted by a retained delete whose placement was trimmed from the shallow history now stays deleted.5e409d1: Fix checkout and
diffon aloro.jsdocument 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 throwduplicate sequence id. A transition now rebuilds only the Text and List containers it touches from their own history, once. Rust-created rich text whose historyloro.jscannot 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 ordiffthat throws leaves the document unchanged.489088c: Fix
loro.jslocal tree moves and creates to matchloro-crdt:fractional index bounds are not ordered. It now gives the following siblings with that index new positions in the same transaction, as Rust does.move(node, parent)without an index when it is already last,node.move(parent), andmoveAfter/moveBeforeits current neighbor, no longer records an op.moveAfterandmoveBeforewithin the same parent counted the moved node itself, so moving a node after a later sibling threwtree index … is out of rangeor landed one slot off.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):
applyDiffandrevertTono 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).subscribecallbacks no longer lose a whole event batch when it touches an unknown container; only the events that can't be represented are left out.getCursorPosreturnsundefinedfor 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.onPushcursor 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
revertToand undo adding a mergeable child's content twice after its map key was deleted. For example,revertToa version beforemap.delete("s")turned a mergeable text"hello"into"hellohello", a counter7into14, 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.applyDiffaccepts a new option,applyDiff(diff, { fullState: true }). Use it when applying the result ofdoc.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,applyDiffbehaves 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, usediff()or a snapshot.UndoManageralso keeps a peer's own edit undoable after a remote undo restores the child it edited.cdbc239:
applyDiffon aLoroMovableListnow 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 twoLoroTextchildren, could drop one of them or put elements in the wrong order.UndoManager.undo()andredo()go through the same code, and after some movable-list edits they could panic with anOutOfBounderror.5c7552d: Importing a malformed movable-list
moveorsetop no longer aborts the process. These ops used to panic inside the document locks, poisoning them, and the process then aborted during unwind. Now:move/setwhose element id is not an element of the same list in the op's causal history, or amovewhose index is past the end of the list, makesimport,importJsonUpdatesandimportBatchreturn 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.move/setthat 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:
getChangeAtLamportreturnedundefinedor 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 returnedundefinedand 625 returned the wrong change; all 669 are now correct.Performance: movable-list imports now check every
move/setagainst 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:
unmarkthat changed nothing, typing right after a mark that does not expand after put the new text inside the mark.start_idnaming 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).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, sogetLastEditoris 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: unreachabletrap in WASM, leaving the doc unusable).import,importBatchandimportJsonUpdatesnow 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()intree_state.rswhen 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 threwRuntimeError: 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):
applyDiffandrevertTono 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).subscribecallbacks no longer lose a whole event batch when it touches an unknown container; only the events that can't be represented are left out.getCursorPosreturnsundefinedfor 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.onPushcursor 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
revertToand undo adding a mergeable child's content twice after its map key was deleted. For example,revertToa version beforemap.delete("s")turned a mergeable text"hello"into"hellohello", a counter7into14, 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.applyDiffaccepts a new option,applyDiff(diff, { fullState: true }). Use it when applying the result ofdoc.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,applyDiffbehaves 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, usediff()or a snapshot.UndoManageralso keeps a peer's own edit undoable after a remote undo restores the child it edited.cdbc239:
applyDiffon aLoroMovableListnow 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 twoLoroTextchildren, could drop one of them or put elements in the wrong order.UndoManager.undo()andredo()go through the same code, and after some movable-list edits they could panic with anOutOfBounderror.5c7552d: Importing a malformed movable-list
moveorsetop no longer aborts the process. These ops used to panic inside the document locks, poisoning them, and the process then aborted during unwind. Now:move/setwhose element id is not an element of the same list in the op's causal history, or amovewhose index is past the end of the list, makesimport,importJsonUpdatesandimportBatchreturn 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.move/setthat 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:
getChangeAtLamportreturnedundefinedor 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 returnedundefinedand 625 returned the wrong change; all 669 are now correct.Performance: movable-list imports now check every
move/setagainst 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:
unmarkthat changed nothing, typing right after a mark that does not expand after put the new text inside the mark.start_idnaming 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).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, sogetLastEditoris 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: unreachabletrap in WASM, leaving the doc unusable).import,importBatchandimportJsonUpdatesnow 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()intree_state.rswhen 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 threwRuntimeError: 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.