Releases: loro-dev/loro
Release list
loro.js@0.1.0
Minor Changes
- 68587bc: Add a pure TypeScript implementation of the current Loro binary format and a
loro-crdt-compatible CRDT runtime. - 68587bc: Improve pure TypeScript runtime performance for merged changes, concurrent
sequence insertion, large state snapshots, and bulk list edits. Snapshot
SSTables now use interoperable LZ4 compression and defer non-frontier history
decoding until a history-dependent API needs it.
Patch Changes
- 4e663a1: Keep large latest-state snapshots encoded and hydrate containers on demand.
Local edits and later updates now use a small history overlay, while snapshot
export rewrites only dirty SSTable blocks and avoids redundant output buffers.
loro-crdt@1.13.8
Patch Changes
-
ac1feb6: Reduce peak memory across large snapshot import,
toJSON, update import, and
snapshot export by preserving lazy state, bounding the decompressed SSTable
block cache by bytes, and preallocating the snapshot output exactly. Snapshot
import now validates every embedded SSTable block checksum before accepting
external bytes.Full snapshot export no longer walks the alive-container graph: store entries
for referenced containers are created when the creating op or imported diff is
applied, so a cold export after a large import is a flush plus KV export
instead of re-reading every container from compressed blocks. As part of this,
full export round-trips imported state bytes faithfully (inconsistent
alive-container parent metadata is now rejected by shallow export, which still
walks, rather than by full export), and an ensured-but-empty mergeable child is
no longer materialized into the KV store by a full export — importers resolve
it from its parent map marker. FirsttoJSONafter import also avoids decoding
each lazy container twice.
loro-crdt-map@1.13.8
Patch Changes
- 033f43a: Keep the source-map package changelog aligned with the matching
loro-crdtrelease.
loro-crdt@1.13.7
Patch Changes
-
3986805: Speed up snapshot import. When decoding a Loro snapshot, the redundant per-block SSTable validation (eager block-metadata decode and per-block checksums) is now skipped, because the whole snapshot body is already protected by the document-level checksum verified during decoding. This removes a second hash pass over the data (roughly halving B4 snapshot import time) while preserving integrity guarantees.
This fast path is internal to Loro's snapshot decoding. The public
MemKvStore::import_allstill verifies every block's checksum; a separateimport_all_uncheckedopts into the unchecked path and is only used where an outer checksum already guarantees integrity. -
9b6330d: Speed up local text editing (~35% faster on the B4 editing trace). Three hot-path
changes: the lock-order debug instrumentation is now compiled out of release
builds (it ran on every per-op lock acquisition); the visible-op count is bumped
incrementally for local ops instead of recomputing it from the version vectors
(which also allocated) on every op; and a couple of per-op allocations on the
text insert/delete path were removed (lazy error-context formatting and inline
storage for entity ranges). -
c37e76a: Fix a checkout hang after snapshot import. Change-store blocks decoded from a snapshot recorded a wrong end lamport (
lamport_range.1was set to the start lamport of the block's last change instead of its end). When a change was split across multiple blocks and a lamport-based lookup engaged the binary search path inChangeStore::get_change_by_lamport_lte— e.g. the movable-list diff calculator resolving historical positions duringcheckout— the degenerate range made the search loop forever. The binary search now also caps its steps and falls back to a scan if it ever fails to converge; the fallback considers both unflushed in-memory blocks and the underlying kv store, so lamport lookups (e.g.getChangeAtLamport) stay correct for local changes that have not been flushed yet.
loro-crdt-map@1.13.7
loro-crdt-map
1.13.6
Patch Changes
- a4e6174: Fix undo/redo restoring mergeable map children as regular non-mergeable containers.
- 78f6c8a: Fix a potential deadlock when lazy snapshot loading resolves mergeable container parent depths.
1.13.5
Patch Changes
-
1727258: Improve text insert and snapshot import performance by avoiding duplicate text boundary validation and skipping eager imported change block parsing.
-
52d8168: Recover two per-operation editing slowdowns regressed since 1.11.
Both are constant-factor regressions on the per-op (auto-commit) editing path
introduced by the lazy-snapshot work in #985, measured against the 1.11.1
release.- Every
MapHandler/ListHandler/MovableListHandlerinsert validated its
value withensure_no_regular_container_value, which heap-allocated aVec
on each call even for scalar values (the common case). A scalar fast-path now
skips the allocation and traversal entirely.map create 10^4 key:
~19.4ms -> ~10.7ms. - The per-op text bounds check (
TextHandler::len/len_unicode/len_utf16)
took twoDocStatelocks — one to check whether the container state was
decoded, then another to query the length. These are now consolidated into a
singleDocState::get_text_lenthat takes one lock and one container-store
lookup. The lazy-snapshot memory behavior is preserved: a still-lazy
container reads its cached length metadata without materializing the full
richtext state.bench_text B4 apply(per-op text editing): ~389ms -> ~352ms.
- Every
1.13.4
Patch Changes
-
4d577ad: Fix two O(n^2) editing slowdowns.
- Editing with UTF-16 / UTF-8 (byte) positions (the default in the JS binding)
validated each position by materializing the entire[0, pos)prefix string,
making everyinsert/delete/splice/markO(n) and a run of edits O(n^2)
(regression since 1.12.0). The boundary check now reads the rope's prefix
caches via the cursor (O(log n)). Unicode-indexed editing was unaffected. - When a subscriber is attached and many edits land on the same container within
one event batch (e.g. random-position inserts, or many distinct map-key
writes), building the event cloned the growing accumulated diff on every
compose — O(n^2) in the number of fragments. The diffs are now composed in
place. This affected text, map and list events. - Converting a UTF-16 / UTF-8 position within a text chunk to a unicode offset
scanned the chunk char-by-char, so editing/slicing a large contiguous chunk
(a big insert, a loaded document, or a long run of typed text that merges into
one chunk) was O(chunk length) per op. Chunks that contain no astral-plane
characters (UTF-16) or are pure ASCII (UTF-8) now convert in O(1), covering
essentially all real-world text (ASCII/Latin/CJK).
- Editing with UTF-16 / UTF-8 (byte) positions (the default in the JS binding)
1.13.0
Minor Changes
- fa888d8: Add mergeable child containers: child containers created under a map key that converge across peers on concurrent first-write instead of forking. Exposed as
ensureMergeable{Counter,Map,List,MovableList,Text,Tree}onLoroMap. A mergeable child lives at a deterministicContainerIDderived from(parent, key, kind), and its visibility is driven by a binary ref the parent map stores at the key, so deletes and kind conflicts resolve through the map's regular LWW.getOrCreateContaineris deprecated for lazy map-child creation; migrate those call sites toensureMergeable*.setContainerandinsertContainerregular semantics are unchanged.
1.12.4
Patch Changes
- a6e23b6: Optimize snapshot export for shallow documents by reusing cached shallow-root state instead of checking out to the shallow root and back to latest.
1.12.2
Patch Changes
-
cc587ed: Add a browser package remapping so Vite/Rolldown production builds load WASM without top-level await or circular wasm wrapper chunks.
Also make the base64 entry easier to bundle with plain esbuild, Rollup, and Next.js Webpack by avoiding static Node builtin
require()calls and top-level await in browser bundles. -
8f57f4c: Reduce memory usage for read-only access to snapshot-imported documents by avoiding unnecessary lazy container state initialization.
1.12.0
Minor Changes
-
7dfda87: Make update imports atomic across oplog and document state application.
importandimport_json_updatesnow roll back imported oplog changes when state application fails, so malformed updates do not leave the document with oplog/state divergence.- Pending changes that are activated during import are included in the rollback boundary when they can affect state application.
- Import rollback uses conditional guards to avoid adding fixed overhead to successful detached or no-op imports.
Patch Changes
-
64aa97c: Harden encoding, snapshot, and import paths against malformed input
- JSON schema import (
import_json_updates): out-of-range compressed peer indices now returnDecodeErrorinstead of being silently accepted as raw peer IDs; mismatchedJsonOpContentvs container type returnsDecodeErrorinstead of panicking. - Outdated binary encoding decoder (
decode_op): malformed op streams (missing delete iterators, type mismatches) now returnDecodeDataCorruptionErrorinstead of panicking. - Fast snapshot decoder (
decode_snapshot_blob_meta): truncated or oversized section lengths now returnDecodeDataCorruptionErrorinstead of panicking on slice indexing. - Change store KV import (
import_all): corruptedVersionVector/Frontiersmetadata now returnsDecodeDataCorruptionErrorinstead of panicking. - Value encoding (
LoroValueKind::from_u8,read_str): invalid byte values and invalid UTF-8 now returnDecodeDataCorruptionErrorinstead of panicking. LoroDoc::diff(): checkout failures during diff calculation are now propagated asLoroErrorinstead of panicking; state restore usesunwrap()to fail-fast on internal errors.try_get_text/list/map/tree/movable_list/counter: now returnNonefor wrong root container types instead of panicking.- Detached list insert out-of-bounds: returns
LoroError::OutOfBoundinstead of panicking. - Tree
mov_after/mov_beforeon deleted node: returnsTreeNodeDeletedOrNotExistinstead of panicking. JsonChange::op_len: empty ops array returns0instead of panicking.renew_peer_id: avoids theoretical collision withPeerID::MAX.
- JSON schema import (
-
0977ad1: Fix lock-order panics when JavaScript callbacks re-enter Loro APIs.
opCount()no longer reacquires the OpLog lock while the current thread already holds a higher-order lock.LoroText.iter()snapshots text chunks before invoking the user callback, so callback code can safely read or mutate the document.
-
ef100e6: Reduce memory spikes when exporting snapshots from shallow documents.
When a shallow document is re-exported from its existing shallow root with only a small tail of updates, Loro now reuses the stored shallow-root state instead of decoding all containers just to re-encode the same state.
-
17dc6c0: Fix several edge-case contract violations in document, text, and JSONPath APIs.
- JSONPath
value(...)comparisons now handle boolean values consistently with other scalar comparisons. - Rich text mark expansion now follows
ExpandType::BeforeandExpandType::Bothat documented insertion boundaries. - Text delta slicing now validates invalid ranges and UTF-8/UTF-16 boundaries before slicing, and public deltas omit removed-style tombstones after unmarking.
- Detached list and movable-list out-of-bounds operations now return
LoroError::OutOfBoundinstead of panicking.
- JSONPath
1.11.1
Patch Changes
- 6f5b7a9: Fix production panic regressions
1.10.8
Patch Changes
- 9f68a57: Return errors instead of panicking when diff or checkout targets frontiers before a shallow snapshot root.
1.10.6
Patch Changes
1.10.5
Patch Changes
1.10.4
Patch Changes
- 864b5ca: perf(loro-internal): remove quadratic slow paths in text import/checkout #895
- a34134d: perf: rm the event calling wrapper for ops that will not trigger events
1.10.3
Patch Changes
1.10.2
Patch Changes
1.10.1
Patch Changes
1.10.0
Minor Changes
-
ce16b52: feat: add sliceDelta method to slice a span of richtext #862
Use
text.sliceDelta(start, end)when you need a Quill-style delta for only part of a rich text field (for example, to copy a styled snippet). The method takes UTF-16 indices; usesliceDeltaUtf8if you want to slice by UTF-8 byte offsets instead.import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.configTextStyle({ bold: { expand: "after" }, comment: { expand: "none" }, }); const text = doc.getText("text"); text.insert(0, "Hello World!"); text.mark({ start: 0, end: 5 }, "bold", true); text.mark({ start: 6, end: 11 }, "comment", "greeting"); const snippet = text.sliceDelta(1, 8); console.log(snippet); // [ // { insert: "ello", attributes: { bold: true } }, // { insert: " " }, // { insert: "Wo", attributes: { comment: "greeting" } }, // ]
Patch Changes
- a78d70f: fix: avoid convert panic #858
- ee94ee4: fix: EphemeralStore apply should ignore timeout entries #865
- 9e0a613: fix: Reject symbol-keyed map objects in wasm conversion #855
1.9.0
Minor Changes
loro-crdt@1.13.6
loro-crdt@1.13.5
Patch Changes
-
1727258: Improve text insert and snapshot import performance by avoiding duplicate text boundary validation and skipping eager imported change block parsing.
-
52d8168: Recover two per-operation editing slowdowns regressed since 1.11.
Both are constant-factor regressions on the per-op (auto-commit) editing path
introduced by the lazy-snapshot work in #985, measured against the 1.11.1
release.- Every
MapHandler/ListHandler/MovableListHandlerinsert validated its
value withensure_no_regular_container_value, which heap-allocated aVec
on each call even for scalar values (the common case). A scalar fast-path now
skips the allocation and traversal entirely.map create 10^4 key:
~19.4ms -> ~10.7ms. - The per-op text bounds check (
TextHandler::len/len_unicode/len_utf16)
took twoDocStatelocks — one to check whether the container state was
decoded, then another to query the length. These are now consolidated into a
singleDocState::get_text_lenthat takes one lock and one container-store
lookup. The lazy-snapshot memory behavior is preserved: a still-lazy
container reads its cached length metadata without materializing the full
richtext state.bench_text B4 apply(per-op text editing): ~389ms -> ~352ms.
- Every
loro-crdt-map@1.13.6
loro-crdt-map@1.13.5
Patch Changes
-
1727258: Improve text insert and snapshot import performance by avoiding duplicate text boundary validation and skipping eager imported change block parsing.
-
52d8168: Recover two per-operation editing slowdowns regressed since 1.11.
Both are constant-factor regressions on the per-op (auto-commit) editing path
introduced by the lazy-snapshot work in #985, measured against the 1.11.1
release.- Every
MapHandler/ListHandler/MovableListHandlerinsert validated its
value withensure_no_regular_container_value, which heap-allocated aVec
on each call even for scalar values (the common case). A scalar fast-path now
skips the allocation and traversal entirely.map create 10^4 key:
~19.4ms -> ~10.7ms. - The per-op text bounds check (
TextHandler::len/len_unicode/len_utf16)
took twoDocStatelocks — one to check whether the container state was
decoded, then another to query the length. These are now consolidated into a
singleDocState::get_text_lenthat takes one lock and one container-store
lookup. The lazy-snapshot memory behavior is preserved: a still-lazy
container reads its cached length metadata without materializing the full
richtext state.bench_text B4 apply(per-op text editing): ~389ms -> ~352ms.
- Every
loro-crdt@1.13.4
Patch Changes
-
4d577ad: Fix two O(n^2) editing slowdowns.
- Editing with UTF-16 / UTF-8 (byte) positions (the default in the JS binding)
validated each position by materializing the entire[0, pos)prefix string,
making everyinsert/delete/splice/markO(n) and a run of edits O(n^2)
(regression since 1.12.0). The boundary check now reads the rope's prefix
caches via the cursor (O(log n)). Unicode-indexed editing was unaffected. - When a subscriber is attached and many edits land on the same container within
one event batch (e.g. random-position inserts, or many distinct map-key
writes), building the event cloned the growing accumulated diff on every
compose — O(n^2) in the number of fragments. The diffs are now composed in
place. This affected text, map and list events. - Converting a UTF-16 / UTF-8 position within a text chunk to a unicode offset
scanned the chunk char-by-char, so editing/slicing a large contiguous chunk
(a big insert, a loaded document, or a long run of typed text that merges into
one chunk) was O(chunk length) per op. Chunks that contain no astral-plane
characters (UTF-16) or are pure ASCII (UTF-8) now convert in O(1), covering
essentially all real-world text (ASCII/Latin/CJK).
- Editing with UTF-16 / UTF-8 (byte) positions (the default in the JS binding)