diff --git a/pages/blog/v1.0.mdx b/pages/blog/v1.0.mdx index e03aeeb..5849587 100644 --- a/pages/blog/v1.0.mdx +++ b/pages/blog/v1.0.mdx @@ -137,6 +137,10 @@ console.log(docA.getText("text").toString()); // Hello!Hi! console.log(docB.getText("text").toString()); // Hello!Hi! ``` +If a nested child container may be created lazily by multiple peers under the +same Map key, use [Mergeable Containers](/blog/mergeable-containers) instead of +regular `setContainer`. + diff --git a/pages/docs/advanced/cid.mdx b/pages/docs/advanced/cid.mdx index bc5ec93..6200335 100644 --- a/pages/docs/advanced/cid.mdx +++ b/pages/docs/advanced/cid.mdx @@ -44,6 +44,16 @@ export type ContainerID = containers - Contains the Operation ID of its creation within its Container ID +3. **Mergeable Containers** + - Created through `LoroMap.ensureMergeableText`, + `ensureMergeableMap`, `ensureMergeableList`, and the other + `ensureMergeable*` methods + - Use a special form of Root Container ID. The stable name inside that ID is + derived from the logical position `(parent Map, key, container type)`, not + from the operation that first created the child + - Useful when multiple peers may lazily initialize the same child container + under the same Map key + ## Container States and IDs The ContainerID is not a random UUID but is deterministically generated based on the container's context. To understand how ContainerIDs work, it's important to first understand container states. @@ -53,6 +63,7 @@ The ContainerID is not a random UUID but is deterministically generated based on Key points about ContainerID generation: - **Root containers**: Derive their ID from their name (e.g., "text" in `doc.getText("text")`) - **Normal containers**: Derive their ID from the operation (OpID) that created them +- **Mergeable containers**: Use a special Root Container ID whose stable name is derived from the parent Map, key, and container type - **Detached containers**: Have a default placeholder ID until they're inserted into a document ## Container Overwrites @@ -92,17 +103,31 @@ document content, overwriting it can lead to the unintended hiding/loss of critical information. For this reason, it is essential to implement careful and systematic container initialization practices to prevent such issues. +### Mergeable Containers + +The overwrite behavior occurs because parallel creation of regular child +containers results in different container IDs, preventing automatic merging of +their contents. If peers should address the same child by logical Map key, use a +mergeable child container: + +```ts no_run +const text = doc.getMap("map").ensureMergeableText("text"); +``` + +This uses a special Root Container ID for the child. Its stable name is derived +from the parent Map, key, and type, rather than from the operation that first +created it. + ### Best Practices -1. When containers might be initialized concurrently, prefer initializing them - at the root level rather than as nested containers +1. Use `ensureMergeable*` when a Map child container should be shared by logical + key even if peers initialize it concurrently. -2. When using map containers: +2. When the structure is fixed and known ahead of time: - If possible, initialize all child containers during the map container's initialization - - Avoid concurrent creation of child containers with the same key in the map - container to prevent overwrites -The overwrite behavior occurs because parallel creation of child containers -results in different container IDs, preventing automatic merging of their -contents. +3. Use root containers when the child naturally has a stable top-level name. + +4. Use regular child containers when each creation should have a distinct + operation-derived identity. diff --git a/pages/docs/api/js.mdx b/pages/docs/api/js.mdx index c22d368..a7f908a 100644 --- a/pages/docs/api/js.mdx +++ b/pages/docs/api/js.mdx @@ -360,17 +360,22 @@ doc.configDefaultTextStyle({ expand: "after" }); **πŸ“ Note:** Creating root containers (e.g., `doc.getText("...")`) does not record operations; nested container creation (e.g., `map.setContainer(...)`) does. -**⚠️ Pitfall:** Avoid concurrent creation of child containers with the same key in LoroMaps. Instead of: +**⚠️ Pitfall:** Avoid concurrent creation of regular child containers with the same key in LoroMaps. Instead of: ```ts no_run -// Dangerous - can cause overwrites +// Dangerous - concurrent peers can create different child container IDs doc.getMap("user").getOrCreateContainer(userId, new LoroMap()); ``` -Use: +Use a mergeable child container when the child should be identified by its logical position: + +```ts no_run +doc.getMap("user").ensureMergeableMap(userId); +``` + +Alternatively, use a unique root container if that better fits your model: ```ts no_run -// Safe - unique root container per user doc.getMap("user." + userId); ``` @@ -3603,7 +3608,7 @@ setContainer(key: string, container: T): T Sets a container as the value for a key. -**⚠️ Pitfall:** Concurrent child container creation at the same key on a LoroMap from multiple peers causes overwrites (different container IDs cannot auto-merge). See [Container initialization](/docs/tutorial/tips). +**⚠️ Pitfall:** Concurrent regular child container creation at the same key on a LoroMap from multiple peers causes overwrites (different container IDs cannot auto-merge). If the child should be shared by logical key, use the `ensureMergeable*` methods instead. See [Container initialization](/docs/tutorial/tips). **Parameters:** @@ -3625,6 +3630,125 @@ list.push("item1"); + +```typescript no_run +ensureMergeableText(key: string): LoroText +``` + + +Ensures that `key` points to a mergeable Text child container. Peers that call this method concurrently with the same parent Map and key will address the same logical Text container instead of creating competing child container IDs. + +Use this for lazily-created text fields that should behave like one shared document for all peers. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Text container + +**Example:** + +```ts no_run +const body = doc.getMap("note").ensureMergeableText("body"); +body.insert(0, "Hello"); +``` + + + + +```typescript no_run +ensureMergeableMap(key: string): LoroMap +``` + + +Ensures that `key` points to a mergeable Map child container. Use this when a nested record, profile, settings object, or migration-created child Map may be initialized by multiple peers. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Map container + +**Example:** + +```ts no_run +const record = doc.getMap("records").ensureMergeableMap(recordId); +record.set("updated", true); +``` + + + + +```typescript no_run +ensureMergeableList(key: string): LoroList +``` + + +Ensures that `key` points to a mergeable List child container. Use this for dynamic child lists, such as one list per day, where concurrent first creation should produce one shared List. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable List container + +**Example:** + +```ts no_run +const entries = doc.getMap("days").ensureMergeableList("2026-06-08"); +entries.insert(0, "meeting notes"); +``` + + + + +```typescript no_run +ensureMergeableMovableList(key: string): LoroMovableList +``` + + +Ensures that `key` points to a mergeable MovableList child container. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable MovableList container + + + + +```typescript no_run +ensureMergeableTree(key: string): LoroTree +``` + + +Ensures that `key` points to a mergeable Tree child container. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Tree container + + + + +```typescript no_run +ensureMergeableCounter(key: string): LoroCounter +``` + + +Ensures that `key` points to a mergeable Counter child container. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Counter container + + + ```typescript no_run get(key: string): Value | Container | undefined @@ -3653,13 +3777,18 @@ const name = map.get("name"); ```typescript no_run +/** + * @deprecated Prefer ensureMergeableText/Map/List/... for lazily-created Map child containers. + */ getOrCreateContainer(key: string, container: T): T ``` -Gets an existing container or creates a new one. +**Deprecated:** Prefer the `ensureMergeable*` methods for Map child containers that may be created lazily or concurrently by multiple peers. + +Gets an existing container or creates a new regular child container. -**⚠️ Pitfall:** Parallel container creation for the same key across peers causes overwrites. See [Container initialization](/docs/tutorial/tips). +**⚠️ Pitfall:** Parallel container creation for the same key across peers creates different child container IDs and can cause overwrites. This method is kept for compatibility and for cases where you explicitly want a regular child container. For shared dynamic children, use `ensureMergeableText`, `ensureMergeableMap`, `ensureMergeableList`, `ensureMergeableMovableList`, `ensureMergeableTree`, or `ensureMergeableCounter`. See [Container initialization](/docs/tutorial/tips). **Parameters:** diff --git a/pages/docs/concepts/choose_crdt_type.mdx b/pages/docs/concepts/choose_crdt_type.mdx index 3f981c6..67a8d7a 100644 --- a/pages/docs/concepts/choose_crdt_type.mdx +++ b/pages/docs/concepts/choose_crdt_type.mdx @@ -10,3 +10,19 @@ Choosing the right CRDT type means understanding their potential behavior in con For text, you can choose to represent it directly as a Value on a Map (where the Value can be a string type), or you can choose to use a Text CRDT. For the former, each operation completely overwrites the previous one, so if A and B make concurrent modifications, only one of their edits will remain in the end. For the latter, the CRDT will retain all concurrent insertions by both people, and concurrent deletions are combined to complete the deletion. For most text box edits, you might prefer the latter. But for something like editing a link, you might want to use the former. For Lists, concurrently removing the same element and inserting a single element creates a new element, differentiating from the semantics of Set on a Map (we may consider providing a list set method in the future). For representing coordinates, it's better to use a Map rather than a List. If you represent coordinates as [x, y], and the A client updates the y coordinate by deleting the y element and reinserting a new y_a, and the B client also deletes y and inserts y_b, then after merging, the array will become [x, y_a, y_b], which does not conform to the user's schema. Using a Map can prevent this problem. + +For nested data, also decide how the child container should get its identity: + +- Use a Map value, such as a string or plain object, when the field should have + Last-Write-Wins replacement semantics. +- Use a regular child container with `setContainer` or `insertContainer` when + each creation should produce a distinct child object. +- Use a mergeable child container with `ensureMergeableText`, + `ensureMergeableMap`, `ensureMergeableList`, and the other + `ensureMergeable*` methods when multiple peers may lazily initialize the same + child under the same Map key and should end up editing one shared child. + +This distinction matters for dynamic keys, schema migrations, date-keyed lists, +and per-entity subdocuments. In those cases, the child identity usually should +come from the logical position `(parent Map, key, type)` rather than from the +operation that happened to create it first. diff --git a/pages/docs/concepts/container.mdx b/pages/docs/concepts/container.mdx index d13af23..2d4565b 100644 --- a/pages/docs/concepts/container.mdx +++ b/pages/docs/concepts/container.mdx @@ -147,20 +147,40 @@ document content, overwriting it can lead to the unintended hiding/loss of critical information. For this reason, it is essential to implement careful and systematic container initialization practices to prevent such issues. +### Mergeable Containers + +The overwrite happens because `setContainer` creates a regular child container. +Its Container ID includes the operation that created it, so two peers that +create `map["text"]` concurrently create two different Text containers. + +When a child container should be identified by its logical position in a Map, +use a mergeable child container instead: + +```ts no_run +const text = doc.getMap("map").ensureMergeableText("text"); +text.insert(0, "A"); +``` + +Peers that call `ensureMergeableText("text")` on the same parent Map address the +same logical Text container. The same pattern is available for Map, List, +MovableList, Tree, and Counter children through the `ensureMergeable*` methods. + ### Best Practices -1. When containers might be initialized concurrently, prefer initializing them - at the root level rather than as nested containers +1. For dynamic Map keys where peers may lazily create the same child, use + `ensureMergeableText`, `ensureMergeableMap`, `ensureMergeableList`, and the + other `ensureMergeable*` methods. -2. When using map containers: +2. When the structure is fixed and known ahead of time: - If possible, initialize all child containers during the map container's initialization - - Avoid concurrent creation of child containers with the same key in the map - container to prevent overwrites -The overwrite behavior occurs because parallel creation of child containers -results in different container IDs, preventing automatic merging of their -contents. +3. Use regular `setContainer` / `insertContainer` when each creation should + produce a distinct child object, or when you are modeling replacement rather + than shared initialization. + +4. A unique root container name can still be a good fit when the child naturally + belongs at the root, for example `doc.getMap("user." + userId)`. ## Related Concepts diff --git a/pages/docs/index.mdx b/pages/docs/index.mdx index e0eac99..ff0bac9 100644 --- a/pages/docs/index.mdx +++ b/pages/docs/index.mdx @@ -125,6 +125,7 @@ CRDT libraries. | Time Travel | βœ… | βœ… | βœ…[1] | βœ… | | [Fugue](https://arxiv.org/abs/2305.00583) / Maximal non-interleaving | βœ… | βœ… | ❌ | ❌ | | JSON Types | βœ… | ❓ | βœ… | βœ… | +| [Mergeable Containers](/blog/mergeable-containers) | βœ… | ❌ | ❌ | ❌ | | Merging Elements in Memory by Run Length Encoding | βœ… | βœ… | βœ… Inventor | ❌ | | Byzantine-fault-tolerance | ❌ | ❌ | ❌ | βœ… | | Version Control | βœ… | ❌ | ❌ | βœ… | diff --git a/pages/docs/tutorial/composition.mdx b/pages/docs/tutorial/composition.mdx index 7942579..077fa95 100644 --- a/pages/docs/tutorial/composition.mdx +++ b/pages/docs/tutorial/composition.mdx @@ -51,3 +51,12 @@ expect(doc.toJSON()).toStrictEqual({ map: { list: [0, 1, "Hello, World!"] } }); expect(callTimes).toBe(2); } ``` + +`setContainer` creates a regular child container. This is fine for fixed +schemas, or when each creation should produce a distinct child object. If +multiple peers may lazily create the same child under the same Map key, use a +mergeable child container instead: + +```ts no_run +const sharedList = map.ensureMergeableList("list"); +``` diff --git a/pages/docs/tutorial/loro_doc.mdx b/pages/docs/tutorial/loro_doc.mdx index d69dfb1..9bff3b7 100644 --- a/pages/docs/tutorial/loro_doc.mdx +++ b/pages/docs/tutorial/loro_doc.mdx @@ -121,7 +121,7 @@ map.set("complex", { }); ``` -Note: When you need to store a CRDT container (like LoroText, LoroList, etc.) within another container, use `setContainer()` or `insertContainer()` methods instead of regular `set()` or `insert()`. This creates a proper sub-container relationship that maintains CRDT properties. +Note: When you need to store a CRDT container (like LoroText, LoroList, etc.) within another container, use `setContainer()` or `insertContainer()` methods instead of regular `set()` or `insert()`. This creates a proper sub-container relationship that maintains CRDT properties. If a child container under a Map may be created lazily by multiple peers at the same key, use the `ensureMergeable*` methods instead; see [Containers](/docs/concepts/container) and [Composing CRDTs](/docs/tutorial/composition). ## Container Types diff --git a/pages/docs/tutorial/map.mdx b/pages/docs/tutorial/map.mdx index 1a576da..e6a9a54 100644 --- a/pages/docs/tutorial/map.mdx +++ b/pages/docs/tutorial/map.mdx @@ -7,6 +7,16 @@ description: "how to use loro map crdt and show all APIs of loro map crdt." Loro's Map uses LWW (Last-Write-Wins) semantics. When concurrent edits conflict, it compares Lamport logic timestamps to determine the winner. +Map slot conflicts are still resolved by LWW. If the value at a key is a child +container, regular child containers have operation-derived identities, so two +peers that create a child at the same key can create two different children. If +the child should be shared by logical key, use a mergeable child container: + +```ts no_run +const body = doc.getMap("notes").ensureMergeableText(noteId); +body.insert(0, "Hello"); +``` + Here is how to use it: ```ts twoslash diff --git a/pages/docs/tutorial/sync.mdx b/pages/docs/tutorial/sync.mdx index f4ab35f..05bc174 100644 --- a/pages/docs/tutorial/sync.mdx +++ b/pages/docs/tutorial/sync.mdx @@ -44,6 +44,12 @@ expect(docA.toJSON()).toStrictEqual(docB.toJSON()); Due to CRDT properties, document consistency is guaranteed when peers receive the same updates, regardless of order or duplicates. +Consistency does not remove the need to choose the right child container +identity. If two offline peers create regular child containers under the same +Map key, sync will converge to one visible Map value and the other child can be +hidden by the Map conflict rule. For dynamic child containers that should be +shared by logical key, use `ensureMergeable*` when modeling the document. + ### Sync Strategies 1. **First Sync** (Initial synchronization between peers): diff --git a/pages/docs/tutorial/tips.mdx b/pages/docs/tutorial/tips.mdx index 19e5de6..a7ef540 100644 --- a/pages/docs/tutorial/tips.mdx +++ b/pages/docs/tutorial/tips.mdx @@ -50,12 +50,12 @@ This behavior contrasts with non-root containers. For example, when you execute --- -##### When initializing child containers of LoroMap in parallel, overwrites can occur instead of automatic merging. +##### When initializing child containers of LoroMap in parallel, use mergeable containers when you want one shared child.
Why this happens -This happens because parallel creation of child containers results in different container IDs, preventing automatic merging of their contents. When a container holds substantial data or serves as the primary storage for document content, overwriting it can lead to unintended hiding or loss of critical information. +This happens because parallel creation of regular child containers results in different container IDs, preventing automatic merging of their contents. When a container holds substantial data or serves as the primary storage for document content, overwriting it can lead to unintended hiding or loss of critical information. ```ts twoslash import { LoroDoc, LoroText } from "loro-crdt"; @@ -79,10 +79,20 @@ doc.import(docB.export({ mode: "update" }));
Best practices for container initialization -1. When using map containers: - - If possible, initialize all child containers during the map container's initialization - - Avoid concurrent creation of child containers with the same key in the map container to prevent overwrites -2. If it's impossible to initialize all child containers when the map container is initialized, prefer initializing them at the root level rather than as nested containers. - You can use `doc.getMap("user." + userId)` instead of `doc.getMap("user").getOrCreateContainer(userId, new LoroMap())` to avoid this problem. +1. If peers may lazily create the same child under the same Map key, use a mergeable child container: + + ```ts no_run + const entries = doc.getMap("days").ensureMergeableList("2026-06-08"); + entries.insert(0, "meeting notes"); + ``` + + The same pattern is available through `ensureMergeableText`, `ensureMergeableMap`, `ensureMergeableMovableList`, `ensureMergeableTree`, and `ensureMergeableCounter`. + +2. If the child structure is fixed and known ahead of time, initialize all child containers during the map container's initialization. + +3. If the child naturally has a unique global name, a root container is also safe. You can use `doc.getMap("user." + userId)` instead of `doc.getMap("user").getOrCreateContainer(userId, new LoroMap())`. + +4. Keep using `setContainer` or `insertContainer` when each creation should produce a distinct child object, or when you are modeling replacement rather than shared initialization.
--- diff --git a/pages/docs/tutorial/tree.mdx b/pages/docs/tutorial/tree.mdx index 172cca5..84d3d48 100644 --- a/pages/docs/tutorial/tree.mdx +++ b/pages/docs/tutorial/tree.mdx @@ -27,6 +27,10 @@ node.data.set("key", "value"); node.data.setContainer("list", new LoroList()); ``` +`node.data` is a Map. If the child container under `node.data` may be lazily +created by multiple peers at the same key, use a mergeable child such as +`node.data.ensureMergeableList("list")` instead of `setContainer`. + ## Ordered Tree Nodes In certain scenarios such as graphic design or file systems, where sibling nodes may also require a sequential relationship, we have introduced `Fractional Index` in Loro to support this capability. You can read more about `Fractional Index` in the [Figma blog](https://www.figma.com/blog/realtime-editing-of-ordered-sequences). In simple terms, `Fractional Index` assigns a sortable value to each object, and if a new insertion occurs between two objects, the `Fractional Index` of the new object will be between the left and right values. The rust-based `Fractional Index` [implementation of Drifting-in-Space](https://github.com/drifting-in-space/fractional_index) has good design and minimal document size growth in most cases. We forked it and made a simple extension for use in Loro. diff --git a/public/blog.xml b/public/blog.xml index f925764..42d829f 100644 --- a/public/blog.xml +++ b/public/blog.xml @@ -4,16 +4,16 @@ Loro Blog https://loro.dev/blog/ Updates and stories from the Loro team. - Mon, 08 Jun 2026 17:15:08 GMT + Tue, 09 Jun 2026 03:58:52 GMT https://validator.w3.org/feed/docs/rss2.html https://github.com/jpmonette/feed - <![CDATA[Mergeable Containers: Fixing Concurrent Child Creation]]> + <![CDATA[Mergeable Containers: When Child Documents Collide]]> https://loro.dev/blog/mergeable-containers https://loro.dev/blog/mergeable-containers Mon, 08 Jun 2026 16:00:00 GMT - Mergeable Containers: Fixing Concurrent Child Creation + Mergeable Containers: When Child Documents Collide

Mergeable Containers overview

Two users are offline. Both add content to the same empty note. They come back online, sync finishes, and one user's edits seem to disappear.

There is no error, and the data is not actually gone from history. But note.get("body") can only return one Text container. The other container was created concurrently and still exists in history, but it is no longer visible in the current document state. From the application's point of view, this looks like data loss.

@@ -22,39 +22,31 @@

Special thanks to Alexis Williams from Synapdeck for the substantial implementation work and design discussion behind this feature.

From the user's point of view, the API change is small. Instead of creating an on-demand child container like this:

// Peer A
-doc.getMap("days")
-    .setContainer("2026-06-08", new LoroList())
-    .insert(0, "A")
+doc.getMap("days").setContainer("2026-06-08", new LoroList()).insert(0, "A");
 
 // Peer B, offline
-doc.getMap("days")
-    .setContainer("2026-06-08", new LoroList())
-    .insert(0, "B")
+doc.getMap("days").setContainer("2026-06-08", new LoroList()).insert(0, "B");
 
 // after sync: only one List is visible at "2026-06-08"
 

you can use a mergeable child:

// Peer A
-doc.getMap("days")
-    .ensureMergeableList("2026-06-08")
-    .insert(0, "A")
+doc.getMap("days").ensureMergeableList("2026-06-08").insert(0, "A");
 
 // Peer B, offline
-doc.getMap("days")
-    .ensureMergeableList("2026-06-08")
-    .insert(0, "B")
+doc.getMap("days").ensureMergeableList("2026-06-08").insert(0, "B");
 
 // after sync: both peers edit the same List
 

As a rule of thumb, use ensureMergeable* when a child container should be identified by its logical position:

-
map.ensureMergeableText(key)
-map.ensureMergeableMap(key)
-map.ensureMergeableList(key)
-map.ensureMergeableMovableList(key)
-map.ensureMergeableTree(key)
-map.ensureMergeableCounter(key)
+
map.ensureMergeableText(key);
+map.ensureMergeableMap(key);
+map.ensureMergeableList(key);
+map.ensureMergeableMovableList(key);
+map.ensureMergeableTree(key);
+map.ensureMergeableCounter(key);
 
-

Use them for fields that should behave like one shared child document for everyone: one shared Text, one shared List, one shared Map, and so on. It should not matter which peer creates that child first. The rest of this post walks through why the problem exists and how the new encoding works.

+

Use them for fields that should behave like one shared child container for everyone: one shared Text, one shared List, one shared Map, and so on. It should not matter which peer creates that child first. The rest of this post walks through why the problem exists and how the new encoding works.

Why This Happens

CRDTs are usually good at cases like "multiple users editing the same text at the same time" or "multiple users inserting into the same list concurrently." This issue happens one layer earlier: before the peers can edit the same List, Text, or Map, they first need to agree on which child container that key refers to.

Before Mergeable Containers, the recommended workaround was to initialize all required child containers as soon as the parent LoroMap was created. For example, if every note always needs a body text, creating that body together with the note avoids the first-creation race.

@@ -63,11 +55,13 @@ map.ensureMergeableCounter(key)

The issue is not that List insertion cannot merge. Once both peers are editing the same List, List edits merge normally. The issue is that the two peers created two different Lists at the same Map key.

Why Root Containers Are Naturally Mergeable

In Loro and Yjs, top-level Root Containers are usually accessed by name:

-
doc.getMap("state")
-doc.getText("content")
+
doc.getMap("state");
+doc.getText("content");
 

Here, "state" or "content" is already a stable identity. It does not depend on which peer created it or which operation created it. As long as multiple peers access the same root name, they naturally refer to the same logical Container.

+

Automerge has a different object identity model, so this root-container comparison is specifically about Loro and Yjs. The broader issue is still similar: when composite values are created concurrently at the same key, the system needs a rule for which object identity becomes visible.

+

Regular child Containers are different. Their identity is tied to the operation that created them, so two concurrent "first creations" become two different objects.

Mergeable Containers bring the useful part of Root Container identity to selected child Containers: the child identity comes from a deterministic name, not from the creation operation.

API: Explicitly Ensuring a Mergeable Child

@@ -121,11 +115,11 @@ type = Text
At this key of this parent Map, activate a mergeable child of this type.
 

When a new Loro client reads the slot, it uses the current parent id + key + kind to derive the deterministic mergeable CID, then presents it through the public API as a normal Container:

-
const body = map.get("body")
+
const body = map.get("body");
 // body is a LoroText, not the internal binary marker
 

When the key is deleted, only the marker is removed. The mergeable child state is not immediately destroyed, because the parent slot controls visibility rather than the child's stored history. Calling this again:

-
map.ensureMergeableText("body")
+
map.ensureMergeableText("body");
 

resurfaces the same deterministic Text Container.

The marker is also bound to its exact parent, key, and type. That keeps it from accidentally activating a mergeable child if the same binary value is copied somewhere else.

@@ -165,14 +159,14 @@ type = Text

What This Solves for Users

Mergeable Containers are especially useful when eager initialization is not practical.

For example, suppose an application stores one child List per date:

-
const days = doc.getMap("days")
-const entries = days.ensureMergeableList("2026-06-08")
-entries.insert(0, "meeting notes")
+
const days = doc.getMap("days");
+const entries = days.ensureMergeableList("2026-06-08");
+entries.insert(0, "meeting notes");
 

Or suppose a schema migration lazily adds a new child Map to existing records:

-
const record = doc.getMap("records").ensureMergeableMap(recordId)
-const metadata = record.ensureMergeableMap("metadata_v2")
-metadata.set("migrated", true)
+
const record = doc.getMap("records").ensureMergeableMap(recordId);
+const metadata = record.ensureMergeableMap("metadata_v2");
+metadata.set("migrated", true);
 

In both cases, the child container identity no longer depends on which peer created it first. It depends on the logical position in the document structure.

This makes Mergeable Containers especially useful for:

@@ -240,7 +234,10 @@ function sync(a, b) { a.getMap("days").setContainer("2026-06-08", new LoroList()).insert(0, "A"); b.getMap("days").setContainer("2026-06-08", new LoroList()).insert(0, "B"); sync(a, b); - console.log("setContainer ->", JSON.stringify(a.getMap("days").get("2026-06-08").toArray())); + console.log( + "setContainer ->", + JSON.stringify(a.getMap("days").get("2026-06-08").toArray()), + ); // -> ["A"] or ["B"], never both: only one peer's List survives. // (which one wins depends on the randomly-assigned peer IDs) } @@ -252,7 +249,10 @@ function sync(a, b) { a.getMap("days").ensureMergeableList("2026-06-08").insert(0, "A"); b.getMap("days").ensureMergeableList("2026-06-08").insert(0, "B"); sync(a, b); - console.log("ensureMergeable ->", JSON.stringify(a.getMap("days").get("2026-06-08").toArray())); + console.log( + "ensureMergeable ->", + JSON.stringify(a.getMap("days").get("2026-06-08").toArray()), + ); // -> both entries, e.g. ["A","B"] (order may vary): both peers share one List. }
@@ -263,14 +263,25 @@ const a = new Y.Doc(); const b = new Y.Doc(); // Peer A and Peer B each create a Y.Array at the same key, offline. -{ const l = new Y.Array(); a.getMap("days").set("2026-06-08", l); l.insert(0, ["A"]); } -{ const l = new Y.Array(); b.getMap("days").set("2026-06-08", l); l.insert(0, ["B"]); } +{ + const l = new Y.Array(); + a.getMap("days").set("2026-06-08", l); + l.insert(0, ["A"]); +} +{ + const l = new Y.Array(); + b.getMap("days").set("2026-06-08", l); + l.insert(0, ["B"]); +} // Sync both ways. Y.applyUpdate(a, Y.encodeStateAsUpdate(b)); Y.applyUpdate(b, Y.encodeStateAsUpdate(a)); -console.log("yjs ->", JSON.stringify(a.getMap("days").get("2026-06-08").toArray())); +console.log( + "yjs ->", + JSON.stringify(a.getMap("days").get("2026-06-08").toArray()), +); // -> ["A"] or ["B"], never both: one peer's child Y.Array wins, the other is dropped.

Automerge β€” the same problem

@@ -281,21 +292,33 @@ let a = A.clone(base); let b = A.clone(base); // Peer A and Peer B each create a list at the same key, offline. -a = A.change(a, d => { d.days["2026-06-08"] = ["A"]; }); -b = A.change(b, d => { d.days["2026-06-08"] = ["B"]; }); +a = A.change(a, (d) => { + d.days["2026-06-08"] = ["A"]; +}); +b = A.change(b, (d) => { + d.days["2026-06-08"] = ["B"]; +}); let merged = A.merge(A.clone(a), b); console.log("automerge visible ->", JSON.stringify(merged.days["2026-06-08"])); // -> ["A"] or ["B"], never both: one list wins. -console.log("automerge conflicts ->", JSON.stringify(A.getConflicts(merged.days, "2026-06-08"))); +console.log( + "automerge conflicts ->", + JSON.stringify(A.getConflicts(merged.days, "2026-06-08")), +); // -> both lists keyed by op id: the losing list is retained but hidden, // reachable only via getConflicts(). // Control: when the child is created ONCE up front, concurrent edits merge. let shared = A.from({ days: { "2026-06-08": [] } }); -let c = A.clone(shared), d = A.clone(shared); -c = A.change(c, x => { x.days["2026-06-08"].push("A"); }); -d = A.change(d, x => { x.days["2026-06-08"].push("B"); }); +let c = A.clone(shared), + d = A.clone(shared); +c = A.change(c, (x) => { + x.days["2026-06-08"].push("A"); +}); +d = A.change(d, (x) => { + x.days["2026-06-08"].push("B"); +}); let ok = A.merge(A.clone(c), d); console.log("automerge pre-created ->", JSON.stringify(ok.days["2026-06-08"])); // -> ["A","B"] (order may vary): both survive β€” this is the eager-init workaround. @@ -718,6 +741,9 @@ docA.import(bytesB); console.log(docA.getText("text").toString()); // Hello!Hi! console.log(docB.getText("text").toString()); // Hello!Hi!
+

If a nested child container may be created lazily by multiple peers under the +same Map key, use Mergeable Containers instead of +regular setContainer.

diff --git a/public/llms-full.txt b/public/llms-full.txt index ccf5b2c..ca299b3 100644 --- a/public/llms-full.txt +++ b/public/llms-full.txt @@ -1,5 +1,13135 @@ +# FILE: .next/standalone/pages/docs/api/js.mdx + +import styles from "./api-reference.module.css"; +import Indent from "./indent"; +import Method from "./method"; + +
+ +# API Reference + +> _Last updated: 2025-08-09 loro-crdt@1.5.10_ + +## Overview + +Loro is a powerful Conflict-free Replicated Data Type (CRDT) library that enables real-time collaboration. If CRDTs are new to you, start with [What are CRDTs](/docs/concepts/crdt) for a gentle intro. This API reference provides comprehensive documentation for all classes, methods, and types available in the JavaScript/TypeScript binding. + +Note: Under the hood, Loro combines a Fugue-based CRDT core with Eg-walker-inspired techniques that use simple index operations and replay only the divergent history when merging. This yields fast local edits, efficient merges, and low overhead without permanent tombstones. See the primer [Event Graph Walker (Eg-walker)](/docs/advanced/event_graph_walker) and performance notes in the v1.0 blog (import/export speedups, shallow snapshots): https://loro.dev/blog/v1.0 + +## Pitfalls & Best Practices + +**Peer ID Management** + +- **Never share PeerIDs** between concurrent sessions (tabs/devices) - causes document divergence +- Use random PeerIDs (default) unless you have strict single-ownership locking +- Don't assign fixed PeerIDs to users or devices + +**UTF-16 Text Encoding** + +- All text operations use UTF-16 indices by default in JS API +- Slicing in the middle of multi-unit codepoints corrupts them +- Use `insertUtf8()`/`deleteUtf8()` for UTF-8 systems + +**Container Creation** + +- Concurrent child container creation inside the same LoroMap at same key causes overwrites +- Initialize all child containers for a LoroMap upfront when possible +- Operations on the root containers will not override each other + +- Events emit synchronously during commit/import/checkout in JS API (v1.8+). Stay on `<=1.7.x`? Await a microtask before reading batched events. +- Import/export/checkout trigger automatic commits +- Loro transactions are NOT ACID - no rollback/isolation + +**Version Control** + +- After `checkout()`, document enters read-only "detached" mode, unless `setDetachedEditing(true)` is called +- [Frontiers](/docs/concepts/frontiers) can't determine complete operation sets without history + +**Data Structure Choice** + +- Use strings in Map for URLs/IDs (LWW), LoroText for collaborative editing + +## Common Tasks & Examples + +**Getting Started** + +- **Create a document**: [`new LoroDoc()`](#LoroDoc.constructor) - Initialize a new collaborative document +- **Add containers**: [`getText`](#LoroDoc.getText), [`getList`](#LoroDoc.getList), [`getMap`](#LoroDoc.getMap), [`getTree`](#LoroDoc.getTree) +- **Listen to changes**: [`subscribe`](#LoroDoc.subscribe) - React to document modifications +- **Export/Import state**: [`export`](#LoroDoc.export) and [`import`](#LoroDoc.import) - Save and load documents + +**Real-time Collaboration** + +- **Sync between peers**: [`export`](#LoroDoc.export) with `mode: "update"` + [`import`](#LoroDoc.import)/[`importBatch`](#LoroDoc.importBatch) - Exchange incremental updates +- **Stream updates**: [`subscribeLocalUpdates`](#LoroDoc.subscribeLocalUpdates) - Send changes over WebSocket/WebRTC +- **Set unique peer ID**: [`setPeerId`](#LoroDoc.setPeerId) - Ensure each client has a unique identifier +- **Handle conflicts**: Automatic - All Loro data types are CRDTs that merge concurrent edits + +**Rich Text Editing** + +- **Create rich text**: [`getText`](#LoroDoc.getText) - Initialize a collaborative text container +- **Edit text**: [`insert`](#LoroText.insert), [`delete`](#LoroText.delete), [`applyDelta`](#LoroText.applyDelta) +- **Apply formatting**: [`mark`](#LoroText.mark) - Add bold, italic, links, custom styles +- **Copy styled snippets**: [`sliceDelta`](#LoroText.sliceDelta) - Get a Delta for a range (UTF-16; use `sliceDeltaUtf8` for byte offsets) +- **Track cursor positions**: [`getCursor`](#LoroText.getCursor) + [`getCursorPos`](#LoroDoc.getCursorPos) - Stable positions across edits +- **Configure styles**: [`configTextStyle`](#LoroDoc.configTextStyle) - Define expand behavior for marks + +**Data Structures** + +- **Ordered lists**: [`getList`](#LoroDoc.getList) - Arrays with [`push`](#LoroList.push), [`insert`](#LoroList.insert), [`delete`](#LoroList.delete) +- **Key-value maps**: [`getMap`](#LoroDoc.getMap) - Objects with [`set`](#LoroMap.set), [`get`](#LoroMap.get), [`delete`](#LoroMap.delete) +- **Hierarchical trees**: [`getTree`](#LoroDoc.getTree) - File systems, nested comments with [`createNode`](#LoroTree.createNode), [`move`](#LoroTree.move) +- **Reorderable lists**: [`getMovableList`](#LoroDoc.getMovableList) - Drag-and-drop with [`move`](#LoroMovableList.move), [`set`](#LoroMovableList.set) +- **Counters**: [`getCounter`](#LoroDoc.getCounter) - Distributed counters with [`increment`](#LoroCounter.increment) + +**Ephemeral State & Presence** + +- **User presence**: [`EphemeralStore`](#ephemeralstore) - Share cursor positions, selections, user status (not persisted) +- **Cursor syncing**: Use [`EphemeralStore.set`](#EphemeralStore.set) with cursor data from [`getCursor`](#LoroText.getCursor) +- **Live indicators**: Track who's online, typing indicators, mouse positions +- **Important**: EphemeralStore is a separate CRDT without history - perfect for temporary state that shouldn't persist + +**Version Control & History** + +- **Undo/redo**: [`UndoManager`](#undomanager) - Local undo of user's own edits +- **Time travel**: [`checkout`](#LoroDoc.checkout) to any [`frontiers`](#LoroDoc.frontiers) - Debug or review history +- **Version tracking**: [`version`](#LoroDoc.version), [`frontiers`](#LoroDoc.frontiers), [`versionVector`](#LoroDoc.versionVector) +- **Fork documents**: [`fork`](#LoroDoc.fork) or [`forkAt`](#LoroDoc.forkAt) - Create branches for experimentation +- **Merge branches**: [`import`](#LoroDoc.import) - Combine changes from forked documents + +**Performance & Storage** + +- **Incremental updates**: [`export`](#LoroDoc.export) from specific [`version`](#LoroDoc.version) - Send only changes +- **Compact history**: [`export`](#LoroDoc.export) with `mode: "snapshot"` - Full state with compressed history +- **Shallow snapshots**: [`export`](#LoroDoc.export) with `mode: "shallow-snapshot"` - State without partial history (see [Shallow Snapshots](/docs/concepts/shallow_snapshots)) + +## Basic Usage + +```typescript twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); + +// Subscribe to changes +const unsubscribe = doc.subscribe((event) => { + console.log("Document changed:", event); +}); + +// Export updates for synchronization +const updates = doc.export({ mode: "update" }); +``` + +## LoroDoc + +The `LoroDoc` class manages containers, sync, versions, and events. + +**Constructor** + +```typescript no_run +new LoroDoc(); +``` + +Creates a new Loro document with a randomly generated peer ID. + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +``` + +**Static Methods** + + +```typescript no_run +static fromSnapshot(snapshot: Uint8Array): LoroDoc +``` + + +Creates a new LoroDoc from a snapshot. This is useful for loading a document from a previously exported snapshot. + +**Parameters:** + +- `snapshot` - Binary snapshot data + +**Returns:** A new LoroDoc instance + +**Example:** + +```ts no_run twoslash +import { LoroDoc } from "loro-crdt"; + +// Assume we have a snapshot from a previous export +const prevDoc = new LoroDoc(); +prevDoc.getText("text").insert(0, "Hello"); +const snapshot: Uint8Array = prevDoc.export({ mode: "snapshot" }); + +const doc = LoroDoc.fromSnapshot(snapshot); +``` + + + +**Properties** + + +```typescript no_run +readonly peerId: bigint +``` + + +Gets the peer ID of the current writer as a bigint. + +**See Also:** [PeerID Management](/docs/concepts/peerid_management) + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const id = doc.peerId; +``` + + + + +```typescript no_run +readonly peerIdStr: `${number}` +``` + + +Gets the peer ID as a decimal string. + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const idStr = doc.peerIdStr; +``` + + + +### Configuration Methods + + +```typescript no_run +setPeerId(peer: number | bigint | `${number}`): void +``` + + +Sets the peer ID for this document. It must be a number, a BigInt, or a decimal string that fits into an unsigned 64-bit integer. +See [PeerID Management](/docs/concepts/peerid_management) for why uniqueness matters in distributed systems. + +**Parameters:** + +- `peer` - Peer ID as number, bigint, or decimal string + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setPeerId("42"); +``` + +**⚠️ Critical Pitfall:** Never let two parallel peers (e.g., multiple tabs/devices) share the same PeerID β€” it creates duplicate op IDs and causes document divergence. Common mistakes: + +- Don't assign a fixed PeerId to a user (users have multiple devices) +- Don't assign a fixed PeerId to a device (multiple tabs can open the same document) +- If you must reuse PeerIDs, enforce single ownership with strict locking mechanisms +- Best practice: Use random IDs (default behavior) unless you have a strong reason not to + +See [PeerID reuse](/docs/tutorial/tips) for safe reuse patterns. + + + + +```typescript no_run +setRecordTimestamp(auto_record: boolean): void +``` + + +Configures whether to automatically record timestamps for changes. Timestamps use Unix time (seconds since epoch). Learn more about storing timestamps and typical use cases in [Storing Timestamps](/docs/advanced/timestamp). + +**Parameters:** + +- `auto_record` - Whether to automatically record timestamps + +**⚠️ Important:** This setting doesn't persist in exported Updates or Snapshots. You must reapply this configuration each time you initialize a document. + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setRecordTimestamp(true); +``` + + + + +```typescript no_run +setChangeMergeInterval(interval: number): void +``` + + +Sets the interval in milliseconds for merging continuous local changes into a single change record. In Loro, multiple low-level operations are grouped into higher-level Changes for readability and syncing. See [Operations and Changes](/docs/concepts/operations_changes). + +**Parameters:** + +- `interval` - Merge interval in milliseconds + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setChangeMergeInterval(1000); // Merge changes within 1 second +``` + + + + +```typescript no_run +configTextStyle(styles: StyleConfig): void +``` + + +Configures the behavior of text styles (marks) in rich text containers. Marks can expand when edits happen at their edges (before/after/both/none). For a primer on rich text and marks in Loro, see [Text](/docs/tutorial/text). + +**Parameters:** + +- `styles` - Configuration object mapping style names to their config + +**StyleConfig Type:** + +```typescript no_run +type StyleConfig = Record< + string, + { + expand?: "after" | "before" | "both" | "none"; + } +>; +``` + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.configTextStyle({ + bold: { expand: "after" }, + italic: { expand: "none" }, + link: { expand: "none" }, +}); +``` + + + + +```typescript no_run +configDefaultTextStyle(style?: { expand: "after" | "before" | "both" | "none" }): void +``` + + +Configures the default text style for the document when using LoroText. If undefined is provided, the default style is reset. + +**Parameters:** + +- `style` - Default style configuration (optional) + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.configDefaultTextStyle({ expand: "after" }); +``` + + + +### Container Access Methods + +**πŸ“ Note:** Creating root containers (e.g., `doc.getText("...")`) does not record operations; nested container creation (e.g., `map.setContainer(...)`) does. + +**⚠️ Pitfall:** Avoid concurrent creation of regular child containers with the same key in LoroMaps. Instead of: + +```ts no_run +// Dangerous - concurrent peers can create different child container IDs +doc.getMap("user").getOrCreateContainer(userId, new LoroMap()); +``` + +Use a mergeable child container when the child should be identified by its logical position: + +```ts no_run +doc.getMap("user").ensureMergeableMap(userId); +``` + +Alternatively, use a unique root container if that better fits your model: + +```ts no_run +doc.getMap("user." + userId); +``` + + +```typescript no_run +getText(name: string): LoroText +``` + + + +Gets or creates a text container with the given name. New to LoroText and marks? See [Text](/docs/tutorial/text). + +**Parameters:** + +- `name` - The container name + +**Returns:** A `LoroText` instance + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const text = doc.getText("content"); +text.insert(0, "Hello"); +``` + + + + +```typescript no_run +getList(name: string): LoroList +``` + + + +Gets or creates a list container with the given name. Unsure whether to use List or MovableList? See [List and Movable List](/docs/tutorial/list) and the type selection guide [Choosing CRDT Types](/docs/concepts/choose_crdt_type). + +**Parameters:** + +- `name` - The container name + +**Returns:** A `LoroList` instance + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const list = doc.getList("items"); +list.push("Item 1"); +``` + + + + +```typescript no_run +getMap(name: string): LoroMap +``` + + +Gets or creates a map container with the given name. See [Map](/docs/tutorial/map) for basics and patterns. + +**Parameters:** + +- `name` - The container name + +**Returns:** A `LoroMap` instance + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const map = doc.getMap("settings"); +map.set("theme", "dark"); +``` + + + + +```typescript no_run +getTree(name: string): LoroTree +``` + + +Gets or creates a tree container with the given name. Learn about hierarchical editing and moves in [Tree](/docs/tutorial/tree). + +**Parameters:** + +- `name` - The container name + +**Returns:** A `LoroTree` instance + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const tree = doc.getTree("fileSystem"); +const root = tree.createNode(); +``` + + + + +```typescript no_run +getCounter(name: string): LoroCounter +``` + + +Gets or creates a counter container with the given name. Counters are special CRDTs that sum concurrent increments; see [Counter](/docs/tutorial/counter). + +**Parameters:** + +- `name` - The container name + +**Returns:** A `LoroCounter` instance + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const counter = doc.getCounter("likes"); +counter.increment(1); +``` + + + + +```typescript no_run +getMovableList(name: string): LoroMovableList +``` + + +Gets or creates a movable list container with the given name. MovableList is designed for reordering with concurrent moves. See [List and Movable List](/docs/tutorial/list) and [Choosing CRDT Types](/docs/concepts/choose_crdt_type). + +**Parameters:** + +- `name` - The container name + +**Returns:** A `LoroMovableList` instance + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const movableList = doc.getMovableList("tasks"); +movableList.push("Task 1"); +movableList.push("Task 2"); +movableList.push("Task 3"); +movableList.move(0, 2); // Move first item to third position +``` + + + + +```typescript no_run +getContainerById(id: ContainerID): Container | undefined +``` + + +Gets a container by its unique ID. Container IDs (CID) uniquely reference containers across updates; see [Container ID](/docs/advanced/cid) and [Container](/docs/concepts/container). + +**Parameters:** + +- `id` - The container ID + +**Returns:** The container instance or undefined if not found + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const text = doc.getText("text"); +const textId = text.id; +const sameText = doc.getContainerById(textId); +``` + + + +### Import/Export Methods + + +```typescript no_run +export(mode?: ExportMode): Uint8Array +``` + + +Exports the document in various formats for synchronization or persistence. For a walkthrough of export modesβ€”snapshot, update, shallow-snapshot, and updates-in-rangeβ€”see [Export Mode](/docs/tutorial/encoding). Shallow snapshots remove history while keeping current state; see [Shallow Snapshots](/docs/concepts/shallow_snapshots). VersionVector and Frontiers are two ways to represent versions; see [Version Vector](/docs/concepts/version_vector) and [Frontiers](/docs/concepts/frontiers). + +**Parameters:** + +- `mode` - Export configuration (optional) + +**ExportMode Options:** + +```typescript no_run +type ExportMode = + | { mode: "snapshot" } + | { mode: "update"; from?: VersionVector } + | { mode: "shallow-snapshot"; frontiers: Frontiers } + | { mode: "updates-in-range"; spans: { id: OpId; len: number }[] }; +``` + +**Returns:** Encoded binary data + +**⚠️ Important Notes:** + +- **Shallow snapshots**: Cannot import updates from before the shallow start point. Peers can only sync if they have versions after this point. +- **Auto-commit**: The document automatically commits pending operations before export. +- **Performance**: Export new snapshots periodically to reduce import times for new peers. + +**Examples:** + +```typescript no_run +import { LoroDoc, VersionVector } from "loro-crdt"; + +const doc = new LoroDoc(); +// ... make some changes to the document ... + +// Export full snapshot +const snapshot = doc.export({ mode: "snapshot" }); + +// Export updates from a specific version +const lastSyncVersion = doc.version(); // Get current version +// ... make more changes ... +const updates = doc.export({ + mode: "update", + from: lastSyncVersion, +}); + +// Export shallow snapshot at current version +const shallowSnapshot = doc.export({ + mode: "shallow-snapshot", + frontiers: doc.frontiers(), +}); +``` + + + + +```typescript no_run +import(data: Uint8Array): ImportStatus +``` + + +Imports updates or snapshots into the document. Returns an `ImportStatus` describing which peer ranges were applied or are pending. See [Sync](/docs/tutorial/sync) and [Import Status](/docs/concepts/import_status) for how Loro handles out-of-order and partial updates. + +**Parameters:** + +- `data` - Binary data or another LoroDoc to import from + +**⚠️ Important:** LoroDoc will automatically commits pending operations before import. If the doc is in detached mode, the imported operations are recorded into OpLog but not applied to DocState until you call `attach()`, see [Attached vs Detached States](/docs/concepts/attached_detached) adn [OpLog and DocState](/docs/concepts/oplog_docstate). + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +// Receive updates from another peer (e.g., via network) +const otherDoc = new LoroDoc(); +otherDoc.getText("text").insert(0, "Hello"); +const updates: Uint8Array = otherDoc.export({ mode: "update" }); + +// Import binary updates +const status = doc.import(updates); +console.log(status.success); +``` + + + + +```typescript no_run +importBatch(data: Uint8Array[]): ImportStatus +``` + + +Efficiently imports multiple updates in a single batch operation. See [Batch Import](/docs/advanced/import_batch) for performance considerations and usage. + +**Parameters:** + +- `data` - Array of binary updates + +**Example:** + +```typescript no_run +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +declare const update1: Uint8Array; +declare const update2: Uint8Array; +declare const update3: Uint8Array; + +// Usage example: +const updates = [update1, update2, update3]; +const status = doc.importBatch(updates); +``` + + + + +```typescript no_run +exportJsonUpdates(start?: VersionVector, end?: VersionVector, withPeerCompression?: boolean): JsonSchema +``` + + +Exports updates in JSON format for debugging or alternative storage. See [Export Mode](/docs/tutorial/encoding) for format details and trade-offs. + +**Parameters:** + +- `start` - Starting version (optional) +- `end` - Ending version (optional) + +**Returns:** JSON representation of updates + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const jsonUpdates = doc.exportJsonUpdates(); +console.log(JSON.stringify(jsonUpdates, null, 2)); +``` + + + + +```typescript no_run +importJsonUpdates(json: string | JsonSchema): void +``` + + +Imports updates from JSON format. Useful for debugging, migration, or custom storage layers; see [Export Mode](/docs/tutorial/encoding). + +**Parameters:** + +- `json` - JSON string or object containing updates + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const otherDoc = new LoroDoc(); +otherDoc.getText("text").insert(0, "Hello"); +const jsonStr = otherDoc.exportJsonUpdates(); +doc.importJsonUpdates(jsonStr); +``` + + + +## Versioning + +Work with the history DAG using frontiers (heads) and version vectors. Switch, branch, and merge versions safely without manual conflict resolution. See [Versioning Deep Dive](/docs/advanced/version_deep_dive) and [Attached vs Detached States](/docs/concepts/attached_detached). + +### Version Control Methods + + +```typescript no_run +checkout(frontiers: Frontiers): void +``` + + +Checks out the document to a specific version, making it read-only at that point in history. This is the core of time travel; see [Time Travel](/docs/tutorial/time_travel) and [Version](/docs/tutorial/version). + +**Parameters:** + +- `frontiers` - Array of OpIds representing the target version + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.frontiers(); +// Make some changes... +doc.checkout(frontiers); // Go back to previous version +``` + + + + +**⚠️ Important:** In Loro 1.0, `version()`/`frontiers()` include pending (uncommitted) local operations. + +**πŸ“ Note:** After `checkout()`, the document enters "detached" mode and becomes read-only by default. Use `attach()` or `checkoutToLatest()` to return to editing mode. See [Version Deep Dive](/docs/advanced/version_deep_dive) and [Attached vs Detached States](/docs/concepts/attached_detached). + + + + +```typescript no_run +checkoutToLatest(): void +``` + + +Returns the document to the latest version after a checkout. Related concepts: [Frontiers](/docs/concepts/frontiers) and [Version Vector](/docs/concepts/version_vector). + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.checkoutToLatest(); +``` + + + + +```typescript no_run +attach(): void +``` + + +Attaches the document to track latest changes after being detached. See [Attached vs Detached States](/docs/concepts/attached_detached) for how Loro separates current state from history. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.attach(); +``` + + + + +```typescript no_run +detach(): void +``` + + +Detaches the document from tracking latest changes, freezing it at current version. See [Attached vs Detached States](/docs/concepts/attached_detached). + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.detach(); +``` + + + + +```typescript no_run +fork(): LoroDoc +``` + + +Creates a new document that is a fork of the current one with a new peer ID. Forking is useful for branching workflows; see [Version](/docs/tutorial/version). + +**Returns:** A new LoroDoc instance + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const forkedDoc = doc.fork(); +``` + + + + +```typescript no_run +forkAt(frontiers: Frontiers): LoroDoc +``` + + +Creates a fork at a specific version in history. Learn more about versions, DAG history, and heads in [Version Deep Dive](/docs/advanced/version_deep_dive). + +**Parameters:** + +- `frontiers` - The version to fork from + +**Returns:** A new LoroDoc instance + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.frontiers(); +const forkedDoc = doc.forkAt(frontiers); +``` + + + +## Events & Transactions + +React to changes and group local operations into transactions. Starting in v1.8, events are delivered synchronously; older releases require awaiting a microtask. See [Event Handling](/docs/tutorial/event) and [Transaction Model](/docs/concepts/transaction_model). + +### Subscription Methods + + +```typescript no_run +subscribe(listener: (event: LoroEventBatch) => void): () => void +``` + + +Subscribes to all document changes. See [Event Handling](/docs/tutorial/event) for the event model and best practices. + +**Parameters:** + +- `listener` - Callback function that receives change events + +**Returns:** Unsubscribe function + +**Event Structure:** + +```typescript no_run +interface LoroEventBatch { + by: "local" | "import" | "checkout"; + origin?: string; + currentTarget?: ContainerID; + events: LoroEvent[]; + from: Frontiers; + to: Frontiers; +} +``` + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const unsubscribe = doc.subscribe((event) => { + console.log("Change type:", event.by); + event.events.forEach((e) => { + console.log("Container changed:", e.target); + console.log("Diff:", e.diff); + }); +}); + +// Later: unsubscribe(); +``` + +**⚠️ Important:** Events are emitted synchronously as of v1.8. If you are pinned to `<=1.7.x`, await a microtask before reading the batch. + +```ts no_run +doc.commit(); +// Events have already been delivered in v1.8+ +// await Promise.resolve(); // Only needed on <=1.7.x +``` + +**πŸ“ Note:** Multiple operations before a commit are batched into a single event. See [Event Handling](/docs/tutorial/event). + + + + +```typescript no_run +subscribeLocalUpdates(f: (bytes: Uint8Array) => void): () => void +``` + + +Subscribes only to local changes, useful for syncing with remote peers. This is typically wired to your transport layer; see [Sync](/docs/tutorial/sync). + +**Parameters:** + +- `f` - Callback that receives binary updates + +**Returns:** Unsubscribe function + +**Example:** + +```ts no_run threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +declare const websocket: { send: (data: Uint8Array) => void }; + +// Usage example: +const unsubscribe = doc.subscribeLocalUpdates((updates) => { + // Send updates to remote peers + websocket.send(updates); +}); +``` + + + + +```typescript no_run +subscribeFirstCommitFromPeer(f: (e: { peer: PeerID }) => void): () => void +``` + + +Subscribes to the first commit from each peer, useful for tracking peer metadata. + +**Parameters:** + +- `f` - Callback that receives peer information + +**Returns:** Unsubscribe function + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.subscribeFirstCommitFromPeer(({ peer }) => { + // Store peer metadata + doc.getMap("peers").set(peer, { + joinedAt: Date.now(), + name: `User ${peer}`, + }); +}); +``` + + + +### Transaction Methods + + +```typescript no_run +commit(options?: { origin?: string, message?: string, timestamp?: number }): void +``` + + +Commits pending changes as a single transaction. A transaction groups operations into a Change; see [Operations and Changes](/docs/concepts/operations_changes). + +**⚠️ Critical Distinction:** Loro transactions are NOT ACID database transactions: + +- No rollback capability +- No isolation guarantees +- Purpose: Bundle local operations for event batching and history grouping +- Many operations (import/export/checkout) trigger implicit commits + +See [Transaction Model](/docs/concepts/transaction_model). + +**Parameters:** + +- `options` - Optional commit configuration + - `message` - Commit message (persisted in the document like a git commit message, visible to all peers after sync) + - `origin` - Origin identifier (local only - used for marking local events, remote peers won't see this) + - `timestamp` - Unix timestamp in seconds (see [Storing Timestamps](/docs/advanced/timestamp)) + +**Important distinction:** + +- `message` is persisted in the document's history and will be synchronized to all peers, similar to git commit messages +- `origin` is only used locally for filtering events (e.g., excluding certain origins from undo) and is NOT synchronized to remote peers + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.commit({ + message: "Updated document title", // Persisted & synced to all peers + origin: "user-action", // Local only, for event filtering + timestamp: Math.floor(Date.now() / 1000), +}); +``` + + + +### Query Methods + + +```typescript no_run +toJSON(): Value +``` + + +Converts the entire document to a JSON-compatible value. If you prefer a structure where sub-containers are referenced by ID (for privacy or streaming), use getShallowValue(); see [Shallow Snapshots](/docs/concepts/shallow_snapshots). + +**Returns:** JSON representation of the document + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const json = doc.toJSON(); +console.log(JSON.stringify(json, null, 2)); +``` + + + + +```typescript no_run +getShallowValue(): Record +``` + + +Gets a shallow representation where sub-containers are represented by their IDs. This is helpful when you want to share structure without history; see [Shallow Snapshots](/docs/concepts/shallow_snapshots). + +**Returns:** Shallow JSON value + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const shallow = doc.getShallowValue(); +// Sub-containers appear as: "cid:..." +``` + + + + +```typescript no_run +getDeepValueWithID(): any +``` + + +Gets the deep value of the document with container IDs preserved. This is useful when you need to traverse the document structure while maintaining references to container IDs. + +**Returns:** Document value with container IDs + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const deepValue = doc.getDeepValueWithID(); +``` + + + + +```typescript no_run +version(): VersionVector +``` + + +Gets the current version vector of the document. Version vectors track how much data from each peer you’ve seen; see [Version Vector](/docs/concepts/version_vector). + +**Returns:** Map from PeerID to counter + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const vv = doc.version(); +console.log(vv.toJSON()); +``` + + + + +```typescript no_run +frontiers(): Frontiers +``` + + +Gets the current frontiers (heads) of the document. Frontiers are a compact representation of a version; see [Frontiers](/docs/concepts/frontiers) for when to use them instead of version vectors. + +**πŸ“ Note:** Frontiers are a compact version representation. + +**⚠️ Limitation:** When you have a Frontier pointing to operations you don't know about, you cannot determine the complete set of operation IDs included in that version. Version Vectors don't have this limitation but are more verbose. See [Frontiers](/docs/concepts/frontiers) for trade-offs. + +**Returns:** Array of OpIds + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.frontiers(); +// Can be used for checkouts or shallow snapshots +``` + + + + +```typescript no_run +diff(from: Frontiers, to: Frontiers, for_json?: boolean): [ContainerID, Diff | JsonDiff][] +``` + + +Calculates differences between two versions. Understanding how Loro computes diffs benefits from the history DAG model; see [Version Deep Dive](/docs/advanced/version_deep_dive). + +**Parameters:** + +- `from` - Starting frontiers +- `to` - Ending frontiers +- `for_json` - If true, returns JsonDiff format (default: true) + +**Returns:** Array of container IDs and their diffs + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const fromFrontiers = doc.frontiers(); +// Make changes... +const toFrontiers = doc.frontiers(); + +const diffs = doc.diff(fromFrontiers, toFrontiers); +diffs.forEach(([containerId, diff]) => { + console.log(`Container ${containerId} changed:`, diff); +}); +``` + + + +### Pre-Commit Hook + + +```typescript no_run +subscribePreCommit(f: (e: { changeMeta: Change, origin: string, modifier: ChangeModifier }) => void): () => void +``` + + +Subscribe to the pre-commit event. You can modify the message and timestamp of the next change. This hook runs right before a Change is recorded; see [Transaction Model](/docs/concepts/transaction_model) and [Operations and Changes](/docs/concepts/operations_changes). + +Pitfall: `commit()` can be triggered implicitly by `import`, `export`, and `checkout`. Use this hook to attach metadata even for those implicit commits. + +```typescript no_run +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const unsubscribe = doc.subscribePreCommit(({ modifier }) => { + modifier + .setMessage("Tagged by pre-commit") + .setTimestamp(Math.floor(Date.now() / 1000)); +}); +doc.getText("text").insert(0, "Hello"); +doc.commit(); +unsubscribe(); +``` + + + +### Cursor Utilities + + +```typescript no_run +getCursorPos(cursor: Cursor): { update?: Cursor, offset: number, side: Side } +``` + + +Resolve a stable `Cursor` to an absolute position. Cursors remain valid across concurrent edits; see [Cursor and Stable Positions](/docs/concepts/cursor_stable_positions) and [Cursor tutorial](/docs/tutorial/cursor). The side controls affinity when the cursor sits at an insertion boundary. + +```typescript no_run +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "abc"); + +// Get cursor at position 1 +const c0 = text.getCursor(1); +const pos = doc.getCursorPos(c0!); +console.log(pos.offset); // 1 +``` + + + +### Pending Operations + + +```typescript no_run +getUncommittedOpsAsJson(): JsonSchema | undefined +``` + + +Get pending operations from the current transaction in JSON format. Useful for debugging what will be included in the next Change; see [Transaction Model](/docs/concepts/transaction_model). + +```typescript no_run +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +text.insert(0, "Hello"); +const pending = doc.getUncommittedOpsAsJson(); +doc.commit(); +const none = doc.getUncommittedOpsAsJson(); // undefined after commit +``` + + + +### Change Graph & History + +These APIs traverse the history DAG of changes (ancestors/descendants, spans). If this sounds unfamiliar, start with Loro's [Versioning Deep Dive](/docs/advanced/version_deep_dive) and the [Event Graph Walker](/docs/concepts/event_graph_walker). + + +```typescript no_run +travelChangeAncestors(ids: OpId[], f: (change: Change) => boolean): void +``` + + +Visit ancestors of the given changes in causal order. + +```typescript no_run +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.getText("text").insert(0, "Hello"); +doc.commit(); +const head = doc.frontiers(); +doc.travelChangeAncestors(head, (change) => { + console.log(change.peer, change.counter); + return true; // continue +}); +``` + + + + + ```typescript no_run findIdSpansBetween(from: Frontiers, to: Frontiers): + VersionVectorDiff ``` + +Find the op id spans that lie between two versions. + + +```typescript no_run +exportJsonInIdSpan(idSpan: { peer: PeerID, counter: number, length: number }): JsonChange[] +``` + + +```typescript no_run +import { LoroDoc } from "loro-crdt"; +const a = new LoroDoc(); +const b = new LoroDoc(); + +// Usage example: +a.getText("text").update("Hello"); +a.commit(); +const snapshot = a.export({ mode: "snapshot" }); +let printed: any; +b.subscribe((e) => { +const spans = b.findIdSpansBetween(e.from, e.to); +const changes = b.exportJsonInIdSpan(spans.forward[0]); +printed = changes; +}); +b.import(snapshot); + +```` + + + +```typescript no_run +getChangedContainersIn(id: OpId, len: number): ContainerID[] +```` + + + +Get container IDs modified in the given ID range. + +```typescript no_run +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.getList("list").insert(0, 1); +doc.commit(); +const head = doc.frontiers()[0]; +const containers = doc.getChangedContainersIn(head, 1); +``` + + + +### Revert & Apply Diff + + +```typescript no_run +revertTo(frontiers: Frontiers): void +``` + + +Revert the document to a given version by generating inverse operations. + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setPeerId("1"); +const t = doc.getText("text"); +t.update("Hello"); +doc.commit(); +doc.revertTo([{ peer: "1", counter: 1 }]); +``` + + + + +```typescript no_run +applyDiff(diff: [ContainerID, Diff | JsonDiff][]): void +``` + + +Apply a batch of diffs to the document. + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc1 = new LoroDoc(); +const doc2 = new LoroDoc(); + +// Usage example: +doc1.getText("text").insert(0, "Hello"); +const diff = doc1.diff([], doc1.frontiers()); +doc2.applyDiff(diff); +``` + +**Workflow example (squash-like diffs):** For PR-style reviews, combine `diff` and `applyDiff` to send a compact change set between a base version and a new version. Operations that cancel out (insert + delete) are compressed away. + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const baseDoc = new LoroDoc(); +const baseText = baseDoc.getText("text"); +baseText.insert(0, "hello world"); + +// Fork to make isolated edits +const newDoc = baseDoc.fork(); +const newText = newDoc.getText("text"); +newText.insert(0, "abc"); +newText.delete(0, 4); + +const diff = newDoc.diff(baseDoc.frontiers(), newDoc.frontiers()); +console.log(diff); +// [ +// [ +// "cid:root-text:Text", +// { type: "text", diff: [ { delete: 1 } ] } +// ] +// ] + +baseDoc.applyDiff(diff); +console.log(baseDoc.toJSON()); +// { text: "ello world" } +``` + + + +### Detached Editing + + +```typescript no_run +setDetachedEditing(enable: boolean): void +``` + + +Enables or disables detached editing mode. Detached editing lets you stage edits separate from the latest head; see [Attached vs Detached States](/docs/concepts/attached_detached). + +**Parameters:** + +- `enable` - Whether to enable detached editing + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setDetachedEditing(true); +``` + + + + +```typescript no_run +isDetachedEditingEnabled(): boolean +``` + + +Checks if detached editing mode is enabled. + +**Returns:** True if detached editing is enabled + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const enabled = doc.isDetachedEditingEnabled(); +``` + + + + +```typescript no_run +isDetached(): boolean +``` + + +Checks if the document is currently detached. + +**Returns:** True if document is detached + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +console.log(doc.isDetached()); +``` + + + +### Commit Options Helpers + + +```typescript no_run +setNextCommitMessage(msg: string): void +``` + + +Sets the message for the next commit. + +**Parameters:** + +- `msg` - Commit message + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setNextCommitMessage("User action"); +``` + + + + +```typescript no_run +setNextCommitOrigin(origin: string): void +``` + + +Sets the origin for the next commit. + +**Parameters:** + +- `origin` - Origin identifier + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setNextCommitOrigin("ui"); +``` + + + + +```typescript no_run +setNextCommitTimestamp(timestamp: number): void +``` + + +Sets the timestamp for the next commit. + +**Parameters:** + +- `timestamp` - Unix timestamp in seconds + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setNextCommitTimestamp(Math.floor(Date.now() / 1000)); +``` + + + + +```typescript no_run +setNextCommitOptions(options: { origin?: string, timestamp?: number, message?: string }): void +``` + + +Sets multiple options for the next commit. + +**Parameters:** + +- `options` - Commit options object + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setNextCommitOptions({ origin: "ui", message: "batch" }); +doc.getText("text").insert(0, "Hi"); +doc.commit(); +``` + + + + +```typescript no_run +clearNextCommitOptions(): void +``` + + +Clears all pending commit options. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.clearNextCommitOptions(); +``` + + + +### Version & Frontier Utilities + + +```typescript no_run +frontiersToVV(frontiers: Frontiers): VersionVector +``` + + +Converts frontiers to a version vector. + +**Parameters:** + +- `frontiers` - Frontiers to convert + +**Returns:** Version vector representation + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.frontiers(); +const vv = doc.frontiersToVV(frontiers); +``` + + + + +```typescript no_run +vvToFrontiers(vv: VersionVector): Frontiers +``` + + +Converts a version vector to frontiers. + +**Parameters:** + +- `vv` - Version vector to convert + +**Returns:** Frontiers representation + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const vv = doc.version(); +const frontiers = doc.vvToFrontiers(vv); +``` + + + + +```typescript no_run +oplogVersion(): VersionVector +``` + + +Gets the oplog version vector. + +**Returns:** Oplog version vector + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const vv = doc.oplogVersion(); +``` + + + + +```typescript no_run +oplogFrontiers(): Frontiers +``` + + +Gets the oplog frontiers. + +**Returns:** Oplog frontiers + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.oplogFrontiers(); +``` + + + + +```typescript no_run +cmpWithFrontiers(frontiers: Frontiers): -1 | 0 | 1 +``` + + +Compares current document state with given frontiers. + +**Parameters:** + +- `frontiers` - Frontiers to compare with + +**Returns:** -1 if behind, 0 if equal, 1 if ahead + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.frontiers(); +const cmp = doc.cmpWithFrontiers(frontiers); +``` + + + + +```typescript no_run +cmpFrontiers(a: Frontiers, b: Frontiers): -1 | 0 | 1 | undefined +``` + + +Compares two frontiers. + +**Parameters:** + +- `a` - First frontiers +- `b` - Second frontiers + +**Returns:** -1 if a < b, 0 if equal, 1 if a > b, undefined if incomparable + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const f1 = doc.frontiers(); +const f2 = doc.frontiers(); +const cmp = doc.cmpFrontiers(f1, f2); +``` + + + +### JSONPath & Path Queries + +Use simple path strings and JSONPath to fetch nested values and containers. Paths are formed from root container names and keys (e.g., map/key or list/0). For container IDs, see [Container ID](/docs/advanced/cid). + + +```typescript no_run +getByPath(path: string): Value | Container | undefined +``` + + +Gets a value or container by its path. + +**Parameters:** + +- `path` - Path string (e.g., "map/key") + +**Returns:** Value or container at the path, or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); +map.set("key", 1); + +// Usage example: +const value = doc.getByPath("map/key"); +``` + + + + +```typescript no_run +getPathToContainer(id: ContainerID): (string | number)[] | undefined +``` + + +Gets the path to a container by its ID. + +**Parameters:** + +- `id` - Container ID + +**Returns:** Array representing the path, or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const path = doc.getPathToContainer(map.id); +``` + + + + +```typescript no_run +JSONPath(jsonpath: string): any[] +``` + + +Queries the document using JSONPath syntax. + +**Parameters:** + +- `jsonpath` - JSONPath query string + +**Returns:** Array of matching values + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); +map.set("key", 1); + +// Usage example: +const results = doc.JSONPath("$.map"); +``` + + + +### Shallow Doc Utilities + +These helpers relate to shallow snapshots and redaction. If you need a refresher on what β€œshallow” means, see [Shallow Snapshots](/docs/concepts/shallow_snapshots). + + +```typescript no_run +shallowSinceVV(): VersionVector +``` + + +Gets the version vector since which the document is shallow. + +**Returns:** Version vector + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const vv = doc.shallowSinceVV(); +``` + + + + +```typescript no_run +shallowSinceFrontiers(): Frontiers +``` + + +Gets the frontiers since which the document is shallow. + +**Returns:** Frontiers + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.shallowSinceFrontiers(); +``` + + + + +```typescript no_run +isShallow(): boolean +``` + + +Checks if the document is shallow. + +**Returns:** True if document is shallow + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const shallow = doc.isShallow(); +``` + + + + +```typescript no_run +setHideEmptyRootContainers(hide: boolean): void +``` + + +Controls whether empty root containers are hidden in JSON output. + +**Parameters:** + +- `hide` - Whether to hide empty root containers + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setHideEmptyRootContainers(true); +// Now empty roots are hidden in toJSON() +``` + + + + +```typescript no_run +deleteRootContainer(cid: ContainerID): void +``` + + +Deletes a root container. + +**Parameters:** + +- `cid` - Container ID to delete + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +// Usage example: +doc.deleteRootContainer(map.id); +``` + + + + +```typescript no_run +hasContainer(id: ContainerID): boolean +``` + + +Checks if a container exists in the document. + +**Parameters:** + +- `id` - Container ID to check + +**Returns:** True if container exists + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const exists = doc.hasContainer(map.id); +``` + + + +### JSON Serialization with Replacer + + +```typescript no_run +toJsonWithReplacer(replacer: (key: string | number, value: Value | Container) => Value | Container | undefined): Value +``` + + +Customize JSON serialization of containers and values. + +**Parameters:** + +- `replacer` - Function to transform values during serialization + +**Returns:** Customized JSON value + +**Example:** + +```ts threeslash +import { LoroDoc, LoroText } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); + +// Usage example: +const json = doc.toJsonWithReplacer((key, value) => { + if (value instanceof LoroText) { + return value.toDelta(); + } + return value; +}); +``` + + + +### Stats & Introspection + + +```typescript no_run +debugHistory(): void +``` + + +Prints debug information about the document history. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.debugHistory(); +``` + + + + +```typescript no_run +changeCount(): number +``` + + +Gets the total number of changes in the document. + +**Returns:** Number of changes + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const changes = doc.changeCount(); +``` + + + + +```typescript no_run +opCount(): number +``` + + +Gets the total number of operations in the document. + +**Returns:** Number of operations + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const ops = doc.opCount(); +``` + + + + +```typescript no_run +getAllChanges(): Map +``` + + +Gets all changes grouped by peer ID. + +**Returns:** Map of peer ID to changes + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const changes = doc.getAllChanges(); +``` + + + + +```typescript no_run +getChangeAt(id: OpId): Change +``` + + +Gets a specific change by operation ID. + +**Parameters:** + +- `id` - Operation ID + +**Returns:** Change object + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.getText("text").insert(0, "hello"); +doc.commit(); +const changes = doc.getAllChanges(); +const change = changes.get(doc.peerIdStr)?.[0]; +``` + + + + +```typescript no_run +getChangeAtLamport(peer_id: string, lamport: number): Change | undefined +``` + + +Gets a change by peer ID and Lamport timestamp. + +**Parameters:** + +- `peer_id` - Peer ID +- `lamport` - Lamport timestamp + +**Returns:** Change object or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.getText("text").insert(0, "hello"); +const change = doc.getChangeAtLamport(doc.peerIdStr, 1); +``` + + + + +```typescript no_run +getOpsInChange(id: OpId): any[] +``` + + +Gets all operations in a specific change. + +**Parameters:** + +- `id` - Operation ID + +**Returns:** Array of operations + +**Example:** + +```typescript no_run +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.getText("text").insert(0, "hello"); +const changes = doc.getAllChanges(); +const ops = doc.getOpsInChange(changes[0].id); +``` + + + + +```typescript no_run +getPendingTxnLength(): number +``` + + +Gets the number of pending operations in the current transaction. + +**Returns:** Number of pending operations + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.getText("text").insert(0, "x"); +console.log(doc.getPendingTxnLength()); +doc.commit(); +``` + + + +### Import/Export Utilities + + +```typescript no_run +decodeImportBlobMeta(blob: Uint8Array, check_checksum: boolean): ImportBlobMetadata +``` + + +Decodes metadata from an import blob. + +**Parameters:** + +- `blob` - Binary data to decode +- `check_checksum` - Whether to verify checksum + +**Returns:** Import blob metadata + +**Example:** + +```ts threeslash +import { LoroDoc, decodeImportBlobMeta } from "loro-crdt"; + +const doc = new LoroDoc(); +const updates = doc.export({ mode: "update" }); +const meta = decodeImportBlobMeta(updates, true); +``` + + + + +```typescript no_run +redactJsonUpdates(json: string | JsonSchema, version_range: any): JsonSchema +``` + + +Redacts JSON updates within a specified version range. + +Use this to safely remove accidentally leaked sensitive content from history while preserving structure. See [Tips: Redaction](/docs/tutorial/tips). + +**Parameters:** + +- `json` - JSON updates to redact +- `version_range` - Version range for redaction + +**Returns:** Redacted JSON schema + +**Example:** + +```ts threeslash +import { LoroDoc, redactJsonUpdates } from "loro-crdt"; + +const doc = new LoroDoc(); +const json = doc.exportJsonUpdates(); +const redacted = redactJsonUpdates(json, { [doc.peerIdStr]: [0, 999999] }); +``` + + + +--- + +## Container Types + +Common CRDT containers for modeling JSON-like structures. See [Choosing CRDT Types](/docs/concepts/choose_crdt_type) and [Composing CRDTs](/docs/tutorial/composition) for when to use each and how to nest them. + +### LoroText + +A rich text container supporting collaborative text editing with formatting. Supports overlapping marks (bold, italic, links) and +stable cursors. The merge semantics avoid interleaving artifacts under concurrency (Fugue + Eg-walker ideas); you use simple index +APIs and Loro handles index transformation. +See [Text](/docs/tutorial/text), [Eg-walker](/docs/advanced/event_graph_walker), and the rich text blog: https://loro.dev/blog/loro-richtext + +**⚠️ Critical: UTF-16 String Encoding** + +LoroText uses **UTF-16** encoding, matching JavaScript's native string encoding: + +- All standard methods (`insert()`, `delete()`, `mark()`, `slice()`, `charAt()`) use UTF-16 code unit indices +- `length` returns UTF-16 code units (same as JavaScript `string.length`) +- Use `insertUtf8()` and `deleteUtf8()` for UTF-8 byte-based operations when integrating with UTF-8 systems + +**⚠️ Common Pitfalls:** + +1. **Index Misalignment**: UTF-16 indices differ from visual character count +2. **Performance**: Cursor queries on deleted positions require history traversal - in that case, it will return a refreshed Cursor object that does not point to the deleted text + +**Example with emoji:** + +```typescript no_run +const text = doc.getText("text"); +text.insert(0, "Hello πŸ˜€ World"); +console.log(text.length); // 13 (emoji counts as 2) +console.log(text.toString()[6]); // ⚠️ Invalid - splits the emoji +text.delete(6, 2); // βœ… Correct - deletes entire emoji +text.delete(6, 1); // ❌ Wrong - corrupts the emoji + +// Safe iteration +text.iter((char) => { + console.log(char); // Each character handled correctly + return true; +}); +``` + +**πŸ“ Text vs String in Maps:** + +- Use `LoroText` for collaborative text editing where all concurrent edits must be preserved +- Use regular strings in `LoroMap` for atomic values (URLs, IDs, hashes) where Last-Write-Wins is preferred +- Example: URLs should be strings in maps, not LoroText. Otherwise, the automatically merged result may be an invalid URL + + + ```typescript no_run insert(index: number, text: string): void ``` + + + +Inserts text at the specified position using UTF-16 code unit indices (same as JavaScript string indices). + +**Parameters:** + +- `index` - UTF-16 code unit position to insert at (0-based, same as JavaScript string index) +- `text` - Text to insert + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +// Usage example: +text.insert(0, "Hello "); +text.insert(6, "World"); +``` + + + + + ```typescript no_run delete(index: number, len: number): void ``` + + + +Deletes text from the specified position using UTF-16 code units. + +**Parameters:** + +- `index` - Starting UTF-16 code unit position (same as JavaScript string index) +- `len` - Number of UTF-16 code units to delete + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello πŸ˜€ World"); +text.delete(6, 2); // Delete emoji (2 UTF-16 units) +text.delete(5, 1); // Delete space before World +``` + + + + +```typescript no_run +mark(range: { start: number, end: number }, key: string, value: Value): void +``` + + + +Applies formatting to a text range. Marks can be configured to expand/stop at edges via configTextStyle(); see [Text](/docs/tutorial/text) for mark behavior. + +**Parameters:** + +- `range` - The range to format +- `key` - Style attribute name +- `value` - Style value + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); +doc.configTextStyle({ bold: { expand: "after" } }); +text.mark({ start: 0, end: 5 }, "bold", true); +``` + + + + +```typescript no_run +unmark(range: { start: number, end: number }, key: string): void +``` + + +Removes formatting from a text range. For how conflicting edits on marks resolve, see [Text](/docs/tutorial/text). + +**Parameters:** + +- `range` - The range to unformat +- `key` - Style attribute to remove + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); +text.mark({ start: 0, end: 5 }, "bold", true); +text.unmark({ start: 0, end: 5 }, "bold"); +``` + + + + +```typescript no_run +toDelta(): Delta[] +``` + + +Converts text to Delta format (Quill-compatible). + +**Returns:** Array of Delta operations + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); +text.mark({ start: 0, end: 5 }, "bold", true); +const delta = text.toDelta(); +// [{ insert: "Hello", attributes: { bold: true } }, { insert: " World" }] +``` + + + + +```typescript no_run +sliceDelta(start: number, end: number): Delta[] +``` + + +Returns a Quill-style Delta for a subsection of the text, using UTF-16 indices. Useful for copying a styled span. Use `sliceDeltaUtf8` if you need UTF-8 byte offsets instead. + +**Parameters:** + +- `start` - Start UTF-16 code unit index (inclusive) +- `end` - End UTF-16 code unit index (exclusive) + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +import { expect } from "expect"; +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); +expect(snippet).toStrictEqual([ + { insert: "ello", attributes: { bold: true } }, + { insert: " " }, + { insert: "Wo", attributes: { comment: "greeting" } }, +]); +``` + + + + +```typescript no_run +sliceDeltaUtf8(start: number, end: number): Delta[] +``` + + +Returns a Quill-style Delta for a subsection of the text using **UTF-8 byte offsets**. Choose this when your offsets come from UTF-8 encoded buffers. + +**Parameters:** + +- `start` - Start byte offset (inclusive) +- `end` - End byte offset (exclusive) + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +import { expect } from "expect"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hi πŸ‘‹"); + +const enc = new TextEncoder(); +const start = enc.encode("Hi ").length; // 3 bytes +const end = enc.encode("Hi πŸ‘‹").length; // 7 bytes + +const delta = text.sliceDeltaUtf8(start, end); +expect(delta).toStrictEqual([{ insert: "πŸ‘‹" }]); +``` + + + + +```typescript no_run +applyDelta(delta: Delta[]): void +``` + + +Applies Delta operations to the text. + +**Parameters:** + +- `delta` - Array of Delta operations + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.applyDelta([ + { insert: "Hello", attributes: { bold: true } }, + { insert: " World" }, +]); +``` + + + + +```typescript no_run +update(text: string, options?: { timeoutMs?: number; useRefinedDiff?: boolean }): void +``` + + +Updates the current text to the target text using Myers' diff algorithm. + +**Parameters:** + +- `text` - New text content +- `options` - Update options + - `timeoutMs` - Optional timeout for the diff computation + - `useRefinedDiff` - Use refined diff for better quality on long texts + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +text.insert(0, "Hello"); +text.update("Hello World", { timeoutMs: 100 }); +``` + + + + +```typescript no_run +updateByLine(text: string, options?: { timeoutMs?: number; useRefinedDiff?: boolean }): void +``` + + +Line-based update that's faster for large texts (less precise than `update`). + +```typescript no_run +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +text.insert(0, "Line A\nLine C"); +text.updateByLine("Line A\nLine B\nLine C"); +``` + + + + +```typescript no_run +getCursor(pos: number, side?: Side): Cursor | undefined +``` + + +Gets a stable cursor position that survives edits. + +**Parameters:** + +- `pos` - Position in the text +- `side` - Cursor affinity (-1, 0, or 1) + +**Returns:** Cursor object or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); + +const cursor = text.getCursor(5); +// Cursor remains valid even after edits +``` + + + + +```typescript no_run +toString(): string +``` + + +Converts to plain text string. + +**Returns:** Plain text content + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); +const plainText = text.toString(); +``` + + + + +```typescript no_run +charAt(pos: number): string +``` + + +Gets the character at a specific UTF-16 code unit position. + +**Parameters:** + +- `pos` - UTF-16 code unit position + +**Returns:** Character at position + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); +const char = text.charAt(1); // "e" +``` + + + + +```typescript no_run +slice(start: number, end: number): string +``` + + +Extracts a section of the text using UTF-16 code unit positions. + +**Parameters:** + +- `start` - Start UTF-16 code unit index +- `end` - End UTF-16 code unit index (exclusive) + +**Returns:** Sliced text + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello πŸ˜€ World"); + +const slice1 = text.slice(0, 5); // "Hello" +const slice2 = text.slice(6, 8); // "πŸ˜€" (emoji spans 6-8) +const slice3 = text.slice(9, 14); // "World" +``` + + + + +```typescript no_run +splice(pos: number, len: number, s: string): string +``` + + +Replaces text at a position with new content. + +**Parameters:** + +- `pos` - Start position +- `len` - Length to delete +- `s` - String to insert + +**Returns:** Deleted text + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); + +// Usage example: +const deleted = text.splice(6, 5, "Loro"); // returns "World" +``` + + + + +```typescript no_run +push(s: string): void +``` + + +Appends text to the end of the document. + +**Parameters:** + +- `s` - String to append + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +text.push("Hello"); +text.push(" World"); +``` + + + + +```typescript no_run +iter(callback: (char: string) => boolean): void +``` + + +Iterates over each character in the text. + +**Parameters:** + +- `callback` - Function called for each character. Return false to stop iteration. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); + +// Usage example: +text.iter((char) => { + console.log(char); + return true; // continue iteration +}); +``` + + + + +```typescript no_run +insertUtf8(index: number, content: string): void +``` + + +Inserts text at a UTF-8 byte index position. + +**Parameters:** + +- `index` - UTF-8 byte index +- `content` - Text to insert + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +text.insertUtf8(0, "Hello"); +``` + + + + +```typescript no_run +deleteUtf8(index: number, len: number): void +``` + + +Deletes text at a UTF-8 byte index position. + +**Parameters:** + +- `index` - UTF-8 byte index +- `len` - Number of UTF-8 bytes to delete + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); + +// Usage example: +text.deleteUtf8(6, 5); // Delete "World" +``` + + + + +```typescript no_run +getEditorOf(pos: number): PeerID | undefined +``` + + +Gets the peer ID of who last edited the character at a position. + +**Parameters:** + +- `pos` - Character position + +**Returns:** PeerID of last editor or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); + +// Usage example: +const editor = text.getEditorOf(0); +``` + + + + +```typescript no_run +kind(): "Text" +``` + + +Returns the container type. + +**Returns:** "Text" + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +const type = text.kind(); // "Text" +``` + + + + +```typescript no_run +parent(): Container | undefined +``` + + +Gets the parent container if this text is nested. + +**Returns:** Parent container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +const text = list.insertContainer(0, doc.getText("nested")); + +// Usage example: +const parent = text.parent(); // Returns the list +``` + + + + +```typescript no_run +isAttached(): boolean +``` + + +Checks if the container is attached to a document. + +**Returns:** True if attached + +**Example:** + +```ts threeslash +import { LoroDoc, LoroText } from "loro-crdt"; +const doc = new LoroDoc(); +const text = new LoroText(); + +const attached = text.isAttached(); // false until attached to doc +``` + + + + +```typescript no_run +getAttached(): LoroText | undefined +``` + + +Gets the attached version of this container. + +**Returns:** Attached container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc, LoroText } from "loro-crdt"; +const doc = new LoroDoc(); +const text = new LoroText(); + +const attached = text.getAttached(); +``` + + + + +```typescript no_run +isDeleted(): boolean +``` + + +Checks if the container has been deleted. + +**Returns:** True if deleted + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +const deleted = text.isDeleted(); +``` + + + + +```typescript no_run +getShallowValue(): string +``` + + +Gets the text content without marks. + +**Returns:** Plain text string + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); + +// Usage example: +const value = text.getShallowValue(); // "Hello" +``` + + + + +```typescript no_run +toJSON(): any +``` + + +Converts the text to JSON representation. + +**Returns:** JSON value + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); + +// Usage example: +const json = text.toJSON(); // "Hello" +``` + + + + +```typescript no_run +readonly id: ContainerID +``` + + +Gets the unique container ID. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +const containerId = text.id; +``` + + + + +```typescript no_run +readonly length: number +``` + + +Gets the length of the text in UTF-16 code units (same as JavaScript's `string.length`). + +**⚠️ Important:** Emoji and other characters outside the Basic Multilingual Plane count as 2 UTF-16 units. This affects all index-based operations: + +```typescript no_run +text.insert(0, "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦"); // Family emoji +console.log(text.length); // 11 (not 1!) - complex emoji with ZWJ sequences +``` + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); +console.log(text.length); // 5 + +text.insert(5, " πŸ˜€"); +console.log(text.length); // 8 (space + emoji which counts as 2) +``` + + + +### LoroList + +An ordered list container for collaborative arrays. Uses index-based APIs; under concurrency, Loro transforms indices +by replaying only the necessary portion of history (Eg-walker-inspired). See [List and Movable List](/docs/tutorial/list), +[Choosing CRDT Types](/docs/concepts/choose_crdt_type), and [Eg-walker](/docs/advanced/event_graph_walker). + +**⚠️ Important: List vs Map for Coordinates** + +```typescript no_run +// ❌ WRONG - Don't use List for coordinates +const coord = doc.getList("coord"); +coord.push(10); // x +coord.push(20); // y +// Concurrent updates can create [10, 20a, 20b] instead of [10, 20] + +// βœ… CORRECT - Use Map for coordinates +const coord = doc.getMap("coord"); +coord.set("x", 10); +coord.set("y", 20); +// Concurrent updates properly merge to {x: 10, y: 20} +``` + + +```typescript no_run +insert(pos: number, value: Value | Container): void +``` + + +Inserts a value at the specified position. + +**Parameters:** + +- `pos` - Insert position +- `value` - Value to insert + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.insert(0, "First"); +list.insert(1, { type: "object" }); +``` + + + + +```typescript no_run +insertContainer(pos: number, container: T): T +``` + + +Inserts a new container at the position. + +**Parameters:** + +- `pos` - Insert position +- `container` - Container instance + +**Returns:** The inserted container + +**Example:** + +```ts threeslash +import { LoroDoc, LoroText } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +const subText = list.insertContainer(0, new LoroText()); +subText.insert(0, "Nested text"); +``` + + + + +```typescript no_run +delete(pos: number, len: number): void +``` + + +Deletes elements from the list. + +**Parameters:** + +- `pos` - Starting position +- `len` - Number of elements to delete + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push("a"); +list.push("b"); +list.push("c"); +list.push("d"); + +list.delete(1, 2); // Delete 2 elements starting at index 1 +``` + + + + +```typescript no_run +push(value: Value | Container): void +``` + + +Appends a value to the end of the list. + +**Parameters:** + +- `value` - Value to append + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push("Last item"); +``` + + + + +```typescript no_run +getIdAt(pos: number): { peer: PeerID, counter: number } | undefined +``` + + +```typescript no_run +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); + +list.insert(0, 1); +const id0 = list.getIdAt(0); + +```` + + + +```typescript no_run +pushContainer(container: T): T +```` + + + +Appends a container to the end of the list. + +**Parameters:** + +- `container` - Container to append + +**Returns:** The appended container + +**Example:** + +```ts threeslash +import { LoroDoc, LoroMap } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +const map = list.pushContainer(new LoroMap()); +map.set("key", "value"); +``` + + + + +```typescript no_run +pop(): Value | Container | undefined +``` + + +Removes and returns the last element. + +**Returns:** The removed element or undefined + +**Example:** + +```ts no_run threeslash +import { LoroList } from "loro-crdt"; +declare const list: LoroList; +// ---cut--- +const lastItem = list.pop(); +``` + + + + +```typescript no_run +get(index: number): Value | Container | undefined +``` + + +Gets the value at the specified index. + +**Parameters:** + +- `index` - Element index + +**Returns:** The value or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const list = doc.getList("items"); +list.push("first", "second"); + +const item = list.get(0); // "first" +``` + + + + +```typescript no_run +getCursor(pos: number, side?: Side): Cursor | undefined +``` + + +Gets a stable cursor for the position. + +**Parameters:** + +- `pos` - Position in the list +- `side` - Cursor affinity + +**Returns:** Cursor object or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push("a", "b", "c"); + +const cursor = list.getCursor(2); +``` + + + + +```typescript no_run +toArray(): (Value | Container)[] +``` + + +Converts the list to a JavaScript array. + +**Returns:** Array of values + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push("a", "b", "c"); +const array = list.toArray(); +``` + + + + +```typescript no_run +clear(): void +``` + + +Removes all elements from the list. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push("a", "b", "c"); +list.clear(); +``` + + + + +```typescript no_run +length: number +``` + + +Gets the number of elements in the list. + +**Returns:** List length + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push("a"); +list.push("b"); +list.push("c"); +console.log(`List has ${list.length} items`); +``` + + + + +```typescript no_run +kind(): "List" +``` + + +Returns the container type. + +**Returns:** "List" + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); + +const type = list.kind(); // "List" +``` + + + + +```typescript no_run +toJSON(): any +``` + + +Converts the list to JSON representation. + +**Returns:** JSON array + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push(1, 2, 3); + +// Usage example: +const json = list.toJSON(); // [1, 2, 3] +``` + + + + +```typescript no_run +parent(): Container | undefined +``` + + +Gets the parent container if this list is nested. + +**Returns:** Parent container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); +const list = map.setContainer("nested", doc.getList("list")); + +// Usage example: +const parent = list.parent(); // Returns the map +``` + + + + +```typescript no_run +isAttached(): boolean +``` + + +Checks if the container is attached to a document. + +**Returns:** True if attached + +**Example:** + +```ts threeslash +import { LoroDoc, LoroList } from "loro-crdt"; +const doc = new LoroDoc(); +const list = new LoroList(); + +const attached = list.isAttached(); // false until attached to doc +``` + + + + +```typescript no_run +getAttached(): LoroList | undefined +``` + + +Gets the attached version of this container. + +**Returns:** Attached container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc, LoroList } from "loro-crdt"; +const doc = new LoroDoc(); +const list = new LoroList(); + +const attached = list.getAttached(); +``` + + + + +```typescript no_run +isDeleted(): boolean +``` + + +Checks if the container has been deleted. + +**Returns:** True if deleted + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); + +const deleted = list.isDeleted(); +``` + + + + +```typescript no_run +getShallowValue(): Value[] +``` + + +Gets the list values with sub-containers as IDs. + +**Returns:** Array of values + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push(1, 2); + +// Usage example: +const values = list.getShallowValue(); // [1, 2] +``` + + + + +```typescript no_run +readonly id: ContainerID +``` + + +Gets the unique container ID. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); + +const containerId = list.id; +``` + + + +### LoroMap + +A key-value map container for collaborative objects. See [Map](/docs/tutorial/map). + + +```typescript no_run +set(key: string, value: Value | Container): void +``` + + +Sets a key-value pair. + +Note: Setting a key to the same value is a no-op (no operation recorded). See [Map basics](/docs/tutorial/map). + +**Parameters:** + +- `key` - The key +- `value` - The value + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +map.set("name", "Alice"); +map.set("age", 30); +``` + + + + +```typescript no_run +setContainer(key: string, container: T): T +``` + + +Sets a container as the value for a key. + +**⚠️ Pitfall:** Concurrent regular child container creation at the same key on a LoroMap from multiple peers causes overwrites (different container IDs cannot auto-merge). If the child should be shared by logical key, use the `ensureMergeable*` methods instead. See [Container initialization](/docs/tutorial/tips). + +**Parameters:** + +- `key` - The key +- `container` - Container instance + +**Returns:** The set container + +**Example:** + +```ts threeslash +import { LoroDoc, LoroList } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const list = map.setContainer("items", new LoroList()); +list.push("item1"); +``` + + + + +```typescript no_run +ensureMergeableText(key: string): LoroText +``` + + +Ensures that `key` points to a mergeable Text child container. Peers that call this method concurrently with the same parent Map and key will address the same logical Text container instead of creating competing child container IDs. + +Use this for lazily-created text fields that should behave like one shared document for all peers. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Text container + +**Example:** + +```ts no_run +const body = doc.getMap("note").ensureMergeableText("body"); +body.insert(0, "Hello"); +``` + + + + +```typescript no_run +ensureMergeableMap(key: string): LoroMap +``` + + +Ensures that `key` points to a mergeable Map child container. Use this when a nested record, profile, settings object, or migration-created child Map may be initialized by multiple peers. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Map container + +**Example:** + +```ts no_run +const record = doc.getMap("records").ensureMergeableMap(recordId); +record.set("updated", true); +``` + + + + +```typescript no_run +ensureMergeableList(key: string): LoroList +``` + + +Ensures that `key` points to a mergeable List child container. Use this for dynamic child lists, such as one list per day, where concurrent first creation should produce one shared List. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable List container + +**Example:** + +```ts no_run +const entries = doc.getMap("days").ensureMergeableList("2026-06-08"); +entries.insert(0, "meeting notes"); +``` + + + + +```typescript no_run +ensureMergeableMovableList(key: string): LoroMovableList +``` + + +Ensures that `key` points to a mergeable MovableList child container. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable MovableList container + + + + +```typescript no_run +ensureMergeableTree(key: string): LoroTree +``` + + +Ensures that `key` points to a mergeable Tree child container. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Tree container + + + + +```typescript no_run +ensureMergeableCounter(key: string): LoroCounter +``` + + +Ensures that `key` points to a mergeable Counter child container. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Counter container + + + + +```typescript no_run +get(key: string): Value | Container | undefined +``` + + +Gets the value for a key. + +**Parameters:** + +- `key` - The key + +**Returns:** The value or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const name = map.get("name"); +``` + + + + +```typescript no_run +getOrCreateContainer(key: string, container: T): T +``` + + +**Deprecated:** Prefer the `ensureMergeable*` methods for Map child containers that may be created lazily or concurrently by multiple peers. + +Gets an existing container or creates a new regular child container. + +**⚠️ Pitfall:** Parallel container creation for the same key across peers creates different child container IDs and can cause overwrites. This method is kept for compatibility and for cases where you explicitly want a regular child container. For shared dynamic children, use `ensureMergeableText`, `ensureMergeableMap`, `ensureMergeableList`, `ensureMergeableMovableList`, `ensureMergeableTree`, or `ensureMergeableCounter`. See [Container initialization](/docs/tutorial/tips). + +**Parameters:** + +- `key` - The key +- `container` - Container to create if not exists + +**Returns:** The container + +**Example:** + +```ts threeslash +import { LoroDoc, LoroText } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const text = map.getOrCreateContainer("description", new LoroText()); +``` + + + + +```typescript no_run +delete(key: string): void +``` + + +Removes a key-value pair. + +**Parameters:** + +- `key` - The key to remove + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +map.delete("obsoleteKey"); +``` + + + + +```typescript no_run +clear(): void +``` + + +Removes all key-value pairs. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +map.clear(); +``` + + + + +```typescript no_run +keys(): string[] +``` + + +Gets all keys in the map. + +**Returns:** Array of keys + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const allKeys = map.keys(); +``` + + + + +```typescript no_run +values(): (Value | Container)[] +``` + + +Gets all values in the map. + +**Returns:** Array of values + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const allValues = map.values(); +``` + + + + +```typescript no_run +entries(): [string, Value | Container][] +``` + + +Gets all key-value pairs. + +**Returns:** Array of entries + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +for (const [key, value] of map.entries()) { + console.log(`${key}: ${value}`); +} +``` + + + + +```typescript no_run +getLastEditor(key: string): PeerID | undefined +``` + + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +map.set("k", 1); +doc.commit(); +const who = map.getLastEditor("k"); +// who = doc.peerIdStr + +```` + + + +```typescript no_run +size: number +```` + + + +Gets the number of key-value pairs. + +**Returns:** Map size + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +console.log(`Map has ${map.size} entries`); +``` + + + + +```typescript no_run +kind(): "Map" +``` + + +Returns the container type. + +**Returns:** "Map" + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const type = map.kind(); // "Map" +``` + + + + +```typescript no_run +toJSON(): any +``` + + +Converts the map to JSON representation. + +**Returns:** JSON object + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); +map.set("name", "Alice"); + +// Usage example: +const json = map.toJSON(); // { name: "Alice" } +``` + + + + +```typescript no_run +parent(): Container | undefined +``` + + +Gets the parent container if this map is nested. + +**Returns:** Parent container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +const map = list.insertContainer(0, doc.getMap("nested")); + +// Usage example: +const parent = map.parent(); // Returns the list +``` + + + + +```typescript no_run +isAttached(): boolean +``` + + +Checks if the container is attached to a document. + +**Returns:** True if attached + +**Example:** + +```ts threeslash +import { LoroDoc, LoroMap } from "loro-crdt"; +const doc = new LoroDoc(); +const map = new LoroMap(); + +const attached = map.isAttached(); // false until attached to doc +``` + + + + +```typescript no_run +getAttached(): LoroMap | undefined +``` + + +Gets the attached version of this container. + +**Returns:** Attached container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc, LoroMap } from "loro-crdt"; +const doc = new LoroDoc(); +const map = new LoroMap(); + +const attached = map.getAttached(); +``` + + + + +```typescript no_run +isDeleted(): boolean +``` + + +Checks if the container has been deleted. + +**Returns:** True if deleted + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const deleted = map.isDeleted(); +``` + + + + +```typescript no_run +getShallowValue(): Record +``` + + +Gets the map values with sub-containers as IDs. + +**Returns:** Object with values + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); +map.set("key", "value"); + +// Usage example: +const values = map.getShallowValue(); // { key: "value" } +``` + + + + +```typescript no_run +readonly id: ContainerID +``` + + +Gets the unique container ID. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const containerId = map.id; +``` + + + +### LoroTree + +A hierarchical tree container for nested structures. Supports moving subtrees while handling concurrent edits. See [Tree](/docs/tutorial/tree). + +**⚠️ Important Tree Operation Notes:** + +- **Concurrent moves can create cycles**: Loro detects and prevents these automatically +- **Fractional indexing**: Has interleaving issues but maintains relative ordering +- **Don't disable fractional index** if you need siblings to be sorted. See [Tree](/docs/tutorial/tree). + + +```typescript no_run +createNode(parent?: TreeID, index?: number): LoroTreeNode +``` + + +Creates a new tree node. + +**Parameters:** + +- `parent` - Parent node ID (optional, creates root if omitted) +- `index` - Position among siblings (optional) + +**Returns:** The new node's handler + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const root = tree.createNode(); +const child = root.createNode(0); +``` + + + + +```typescript no_run +move(target: TreeID, parent?: TreeID, index?: number): void +``` + + +Moves a node to a new position. + +**Parameters:** + +- `target` - Node to move +- `parent` - New parent (undefined for root) +- `index` - Position among siblings + +**Example:** + +```typescript no_run threeslash +import { LoroDoc, TreeID } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +declare const nodeId: TreeID; +declare const newParentId: TreeID; + +// Usage example: +tree.move(nodeId, newParentId, 0); +``` + + + + +```typescript no_run +delete(target: TreeID): void +``` + + +Deletes a node and its descendants. + +**Parameters:** + +- `target` - Node to delete + +**Example:** + +```ts threeslash +import { LoroDoc, TreeID } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); +const nodeId = node.id; + +// Usage example: +tree.delete(nodeId); +``` + + + + +```typescript no_run +getNodeByID(id: TreeID): LoroTreeNode | undefined +``` + + +Gets a node handler by its ID. + +**Parameters:** + +- `id` - Node ID + +**Returns:** Node handler or undefined + +**Example:** + +```ts threeslash +import { LoroDoc, TreeID } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const _node = tree.createNode(); +const nodeId = _node.id; + +// Usage example: +const node = tree.getNodeByID(nodeId); +if (node) { + node.data.set("label", "New Label"); +} +``` + + + + +```typescript no_run +nodes(): LoroTreeNode[] +``` + + +Gets all nodes in the tree. + +**Returns:** Array of all nodes + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const allNodes = tree.nodes(); +``` + + + + +```typescript no_run +roots(): LoroTreeNode[] +``` + + +Gets all root nodes. + +**Returns:** Array of root nodes + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const rootNodes = tree.roots(); +``` + + + + +```typescript no_run +has(target: TreeID): boolean +``` + + +Checks if a node exists. + +**Parameters:** + +- `target` - Node ID to check + +**Returns:** Boolean indicating existence + +**Example:** + +```typescript no_run threeslash +import { LoroDoc, TreeID } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +declare const nodeId: TreeID; + +// Usage example: +if (tree.has(nodeId)) { + console.log("Node exists"); +} +``` + + + + +```typescript no_run +isNodeDeleted(target: TreeID): boolean +``` + + +Checks if a node has been deleted. + +**Parameters:** + +- `target` - Node ID to check + +**Returns:** Boolean indicating deletion status + +**Example:** + +```typescript no_run threeslash +import { LoroDoc, TreeID } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +declare const nodeId: TreeID; + +// Usage example: +if (tree.isNodeDeleted(nodeId)) { + console.log("Node was deleted"); +} +``` + + + +```typescript no_run +enableFractionalIndex(jitter: number): void +``` + + +Enables fractional indexing for better concurrent move performance. + +**Parameters:** + +- `jitter` - Jitter amount for fractional indices + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +tree.enableFractionalIndex(0.001); +``` + + + + +```typescript no_run +disableFractionalIndex(): void +``` + + +Disables fractional indexing. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +tree.disableFractionalIndex(); +``` + + + + +```typescript no_run +isFractionalIndexEnabled(): boolean +``` + + +Checks if fractional indexing is enabled. + +**Returns:** True if enabled + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const enabled = tree.isFractionalIndexEnabled(); +``` + + + + +```typescript no_run +kind(): "Tree" +``` + + +Returns the container type. + +**Returns:** "Tree" + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const type = tree.kind(); // "Tree" +``` + + + + +```typescript no_run +toJSON(): any +``` + + +Converts the tree to JSON representation. + +**Returns:** JSON tree structure + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const json = tree.toJSON(); +``` + + + + +```typescript no_run +parent(): Container | undefined +``` + + +Gets the parent container if this tree is nested. + +**Returns:** Parent container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); +const tree = map.setContainer("tree", doc.getTree("nested")); + +// Usage example: +const parent = tree.parent(); +``` + + + + +```typescript no_run +isAttached(): boolean +``` + + +Checks if the container is attached to a document. + +**Returns:** True if attached + +**Example:** + +```ts threeslash +import { LoroDoc, LoroTree } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = new LoroTree(); + +const attached = tree.isAttached(); +``` + + + + +```typescript no_run +getAttached(): LoroTree | undefined +``` + + +Gets the attached version of this container. + +**Returns:** Attached container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc, LoroTree } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = new LoroTree(); + +const attached = tree.getAttached(); +``` + + + + +```typescript no_run +isDeleted(): boolean +``` + + +Checks if the container has been deleted. + +**Returns:** True if deleted + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const deleted = tree.isDeleted(); +``` + + + + +```typescript no_run +getShallowValue(): TreeNodeShallowValue[] +``` + + +Gets the tree values with sub-containers as IDs. + +**Returns:** Array of tree node values + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const values = tree.getShallowValue(); +``` + + + + +```typescript no_run +readonly id: ContainerID +``` + + +Gets the unique container ID. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const containerId = tree.id; +``` + + + +### LoroTreeNode + +Represents a single node in the tree. + + +```typescript no_run +data: LoroMap +``` + + +A map container for storing node metadata. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); +node.data.set("title", "Node Title"); +node.data.set("expanded", true); +``` + + + + +```typescript no_run +createNode(index?: number): LoroTreeNode +``` + + +Creates a child node. + +**Parameters:** + +- `index` - Position among siblings + +**Returns:** New node's handler + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); +const childId = node.createNode(0); +``` + + + + +```typescript no_run +move(parent?: LoroTreeNode, index?: number): void +``` + + +Moves this node to a new parent. + +**Parameters:** + +- `parent` - New parent node +- `index` - Position among siblings + +**Example:** + +```ts threeslash +import { LoroDoc, LoroTreeNode } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); +const parent = tree.createNode(); + +// Usage example: +node.move(parent, 0); +``` + + + + +```typescript no_run +moveAfter(target: LoroTreeNode): void +``` + + +Moves this node after a sibling. + +**Parameters:** + +- `target` - Sibling node + +**Example:** + +```ts threeslash +import { LoroDoc, LoroTreeNode } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); +const sibling = tree.createNode(); + +// Usage example: +node.moveAfter(sibling); +``` + + + + +```typescript no_run +moveBefore(target: LoroTreeNode): void +``` + + +Moves this node before a sibling. + +**Parameters:** + +- `target` - Sibling node + +**Example:** + +```ts threeslash +import { LoroDoc, LoroTreeNode } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); +const sibling = tree.createNode(); + +// Usage example: +node.moveBefore(sibling); +``` + + + + +```typescript no_run +parent(): LoroTreeNode | undefined +``` + + +Gets the parent node. + +**Returns:** Parent node or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +// Usage example: +const parentNode = node.parent(); +``` + + + + +```typescript no_run +children(): LoroTreeNode[] +``` + + +Gets all child nodes. + +**Returns:** Array of children + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +// Usage example: +const childNodes = node.children(); +``` + + + + +```typescript no_run +index(): number | undefined +``` + + +Gets the position among siblings. + +**Returns:** Index or undefined if root + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +// Usage example: +const position = node.index(); +``` + + + + +```typescript no_run +fractionalIndex(): string | undefined +``` + + +Returns the node's fractional index used to sort siblings deterministically. +It is a hex string representation of the Fractional Index and is stable for ordering. +Returns `undefined` for the root node. Note: the tree must be attached to the document. + +**Returns:** Hex string or `undefined` for root + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const parent = tree.createNode(); +const a = parent.createNode(0); +const b = parent.createNode(1); + +const aFi = a.fractionalIndex(); +const bFi = b.fractionalIndex(); +// aFi < bFi, because b is inserted after a +``` + + + + +```typescript no_run +creationId(): { peer: PeerID, counter: number } +``` + + +Returns the OpID that created this node. + +**Returns:** `{ peer: PeerID, counter: number }` creation identifier + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +const { peer, counter } = node.creationId(); +``` + + + + +```typescript no_run +creator(): PeerID +``` + + +Returns the peer ID that created this node (equivalent to `creationId().peer`). + +**Returns:** `PeerID` + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +const author = node.creator(); +// author == doc.peerIdStr +``` + + + + +```typescript no_run +getLastMoveId(): { peer: PeerID, counter: number } | undefined +``` + + +Returns the OpID of the most recent move operation for this node, or `undefined` if the node has never been moved. + +**Returns:** Creation/move OpID or `undefined` + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +const lastMove = node.getLastMoveId(); +``` + + + + +```typescript no_run +isDeleted(): boolean +``` + + +Checks if this node has been deleted. + +**Returns:** Boolean deletion status + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +// Usage example: +if (node.isDeleted()) { + console.log("Node is deleted"); +} +``` + + + +### LoroCounter + +A counter CRDT for collaborative numeric values. + + +```typescript no_run +increment(value: number): void +``` + + +Increments the counter. + +**Parameters:** + +- `value` - Amount to increment (default: 1) + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const counter = doc.getCounter("counter"); + +counter.increment(5); // +5 +``` + + + + +```typescript no_run +decrement(value: number): void +``` + + +Decrements the counter. + +**Parameters:** + +- `value` - Amount to decrement (default: 1) + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const counter = doc.getCounter("counter"); + +counter.decrement(3); // -3 +``` + + + + +```typescript no_run +value: number +``` + + +Gets the current counter value. + +**Returns:** Current numeric value + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const counter = doc.getCounter("counter"); + +console.log(`Counter value: ${counter.value}`); +``` + + + +### LoroMovableList + +A list optimized for move operations. Designed for frequent reordering (drag-and-drop) with good behavior under +concurrent moves (concurrent moves resolve to one final position). See [List and Movable List](/docs/tutorial/list). + +**πŸ“ MovableList vs List:** + +- **Use MovableList** for: Drag-and-drop UIs, sortable lists, kanban boards +- **Use List** for: scenarios where the list items don't need to be moved +- **Key difference**: MovableList handles concurrent moves better (no duplicates) and supports set operations, List is more efficient in general. + + +```typescript no_run +move(from: number, to: number): void +``` + + +Moves an element from one position to another. + +**Parameters:** + +- `from` - Source index +- `to` - Target index + +**Example:** + +```typescript +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const movableList = doc.getMovableList("list"); +movableList.push("a"); +movableList.push("b"); +movableList.push("c"); +movableList.push("d"); +movableList.push("e"); + +movableList.move(0, 3); // Move first element to fourth position +``` + + + + +```typescript no_run +set(pos: number, value: Value | Container): void +``` + + +Replaces the value at a position. + +**Parameters:** + +- `pos` - Position to update +- `value` - New value + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const movableList = doc.getMovableList("list"); +movableList.push("a", "b", "c"); + +movableList.set(0, "Updated value"); +``` + + + + +```typescript no_run +setContainer(pos: number, container: T): T +``` + + +Replaces the value with a container. + +**Parameters:** + +- `pos` - Position to update +- `container` - New container + +**Returns:** The set container + +**Example:** + +```ts threeslash +import { LoroDoc, LoroText } from "loro-crdt"; +const doc = new LoroDoc(); +const movableList = doc.getMovableList("list"); +movableList.push("placeholder"); + +const text = movableList.setContainer(0, new LoroText()); +``` + + + +## Synchronization + +Import/export updates over any transport and choose the right encoding for speed and size. See [Sync Tutorial](/docs/tutorial/sync) and [Encoding & Export Modes](/docs/tutorial/encoding). + +- **Import order**: Loro handles out-of-order updates automatically +- **Auto-commit**: Import and export operations trigger automatic commits + +### Import/Export Patterns + +#### Basic Synchronization + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +// Peer A: Export updates +const doc1 = new LoroDoc(); +doc1.getText("text").insert(0, "Hello"); +const updates = doc1.export({ mode: "update" }); + +// Peer B: Import updates +const doc2 = new LoroDoc(); +doc2.import(updates); +// now doc2.getText("text").toString() === "Hello" +``` + +#### Continuous Sync + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc1 = new LoroDoc(); +const doc2 = new LoroDoc(); + +// Usage example: +// Set up bidirectional sync +doc1.subscribeLocalUpdates((updates) => { + doc2.import(updates); +}); + +doc2.subscribeLocalUpdates((updates) => { + doc1.import(updates); +}); +``` + +Performance tips: + +- Prefer `mode: "update"` with a `VersionVector` to sync incrementally. +- Use `mode: "shallow-snapshot"` when you only need current state; it strips history for faster import/load. +- Loro’s LSM-based encoding and Eg-walker-inspired merge keep import/export fast, even for large histories. + See [Encoding](/docs/tutorial/encoding) and v1.0 performance notes: https://loro.dev/blog/v1.0 + +#### Network Sync with WebSocket + +```ts no_run threeslash +import { LoroDoc } from "loro-crdt"; + +// Assume we have: +declare const ws: { + send: (data: Uint8Array) => void; + on: (event: string, handler: (data: any) => void) => void; +}; + +// Client side +const doc = new LoroDoc(); + +// Send local updates to server +doc.subscribeLocalUpdates((updates) => { + ws.send(updates); +}); + +// Receive updates from server +ws.on("message", (data) => { + doc.import(new Uint8Array(data)); +}); +``` + +### Shallow Snapshots + +Shallow snapshots allow for efficient storage by garbage collecting deleted operations. + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +// Create shallow snapshot +const frontiers = doc.frontiers(); +const shallowSnapshot = doc.export({ + mode: "shallow-snapshot", + frontiers: frontiers, +}); + +// Import shallow snapshot +const newDoc = new LoroDoc(); +newDoc.import(shallowSnapshot); +``` + +--- + +## Version Control + +### Time Travel + +Navigate through document history: + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +// Save current version +const v1 = doc.frontiers(); + +// Make changes +doc.getText("text").insert(0, "New text"); + +// Save new version +const v2 = doc.frontiers(); + +// Travel back +doc.checkout(v1); +console.log(doc.getText("text").toString()); // Original text + +// Travel forward +doc.checkout(v2); +console.log(doc.getText("text").toString()); // New text + +// Return to latest +doc.checkoutToLatest(); +``` + +### Forking + +Create document branches: + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +// Fork at current state +const fork1 = doc.fork(); +fork1.getText("text").insert(0, "Fork 1 changes"); + +// Fork at specific version +const historicalVersion = doc.frontiers(); +const fork2 = doc.forkAt(historicalVersion); +fork2.getText("text").insert(0, "Fork 2 changes"); + +// Original document remains unchanged +``` + +### Version Vectors + +Track document versions: + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const vv = doc.version(); +console.log(`Document has ${vv.length()} peers`); +``` + +--- + +## Events & Subscriptions + +### Event Structure + +```typescript no_run +interface LoroEventBatch { + by: "local" | "import" | "checkout"; + origin?: string; + currentTarget?: ContainerID; + events: LoroEvent[]; + from: Frontiers; + to: Frontiers; +} + +interface LoroEvent { + target: ContainerID; + diff: Diff; + path: Path; +} +``` + +### Diff Types + +Different containers produce different diff types: + +#### TextDiff + + +```typescript no_run +type TextDiff = { + type: "text" + diff: Delta[] +} +``` +Represents changes to text content using Delta format. + +**Properties:** + +- `type` - Always "text" for text diffs +- `diff` - Array of Delta operations (insert, delete, retain) + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +// Example +text.subscribe((e) => { + for (const event of e.events) { + if (event.diff.type === "text") { + event.diff.diff.forEach((delta) => { + if (delta.insert) { + console.log(`Inserted: "${delta.insert}"`); + } + if (delta.delete) { + console.log(`Deleted ${delta.delete} characters`); + } + }); + } + } +}); +``` + + + +#### ListDiff + + +```typescript no_run +type ListDiff = { + type: "list" + diff: Delta<(Value | Container)[]>[] +} +``` +Represents changes to list content using Delta format. + +**Properties:** + +- `type` - Always "list" for list diffs +- `diff` - Array of Delta operations on list items + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); + +// Example +list.subscribe((e) => { + for (const event of e.events) { + if (event.diff.type === "list") { + event.diff.diff.forEach((delta) => { + if (delta.insert) { + console.log(`Inserted items:`, delta.insert); + } + }); + } + } +}); +``` + + + +#### MapDiff + + +```typescript no_run +type MapDiff = { + type: "map" + updated: Record +} +``` +Represents changes to map content. + +**Properties:** + +- `type` - Always "map" for map diffs +- `updated` - Record of key-value changes (undefined means deleted) + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +// Example +map.subscribe((e) => { + for (const event of e.events) { + if (event.diff.type === "map") { + Object.entries(event.diff.updated).forEach(([key, value]) => { + if (value === undefined) { + console.log(`Deleted key: ${key}`); + } else { + console.log(`Updated key: ${key} = ${value}`); + } + }); + } + } +}); +``` + + + +#### TreeDiff + + +```typescript no_run +type TreeDiff = { + type: "tree" + diff: TreeDiffItem[] +} + +type TreeDiffItem = +| { +target: TreeID +action: "create" +parent: TreeID | undefined +index: number +fractionalIndex: string +} +| { +target: TreeID +action: "delete" +oldParent: TreeID | undefined +oldIndex: number +} +| { +target: TreeID +action: "move" +parent: TreeID | undefined +index: number +fractionalIndex: string +oldParent: TreeID | undefined +oldIndex: number +} + +```` +Represents changes to tree structure. + +**Properties:** +- `type` - Always "tree" for tree diffs +- `diff` - Array of TreeDiffItem operations (create, delete, move) + +**TreeDiffItem Actions:** +- `create` - Node creation with parent and position +- `delete` - Node deletion with old parent and position +- `move` - Node movement with old and new positions + +**Example:** +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +// Example +tree.subscribe((e) => { + for (const event of e.events) { + if (event.diff.type === "tree") { + event.diff.diff.forEach(item => { + switch (item.action) { + case "create": + console.log(`Created node ${item.target}`); + break; + case "move": + console.log(`Moved node ${item.target}`); + break; + case "delete": + console.log(`Deleted node ${item.target}`); + break; + } + }); + } + } +}); +```` + + + +### Deep Subscription + +Subscribe to nested changes: + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +// Subscribe to specific container +const text = doc.getText("text"); +text.subscribe((event) => { + console.log("Text changed:", event); +}); + +// Subscribe with deep observation +doc.subscribe((event) => { + // Path shows the location of the change + event.events.forEach((e) => { + console.log("Change path:", e.path); + console.log("Container:", e.target); + console.log("Diff:", e.diff); + }); +}); +``` + +--- + +## Undo/Redo + +Local undo operates on your own changes without breaking collaboration. See [Undo/Redo](/docs/advanced/undo) for design details and caveats. + +### UndoManager + +Provides local undo/redo functionality. + +**⚠️ Important Notes:** + +- **Local-only**: UndoManager only undoes the local user's operations, not remote operations +- **Origin filtering**: Use `excludeOriginPrefixes` to exclude certain operations (e.g., sync operations) from undo +- **Cursor restoration**: Use `onPush`/`onPop` callbacks to save and restore cursor positions + + +```typescript no_run +constructor(doc: LoroDoc, config: UndoConfig) +``` + + +Creates a new UndoManager instance. + +**Parameters:** + +- `doc` - The LoroDoc to manage undo/redo for +- `config` - Configuration options + - `mergeInterval?` - Time in ms to merge consecutive operations (default: 1000) + - `maxUndoSteps?` - Maximum number of undo steps to keep (default: 100) + - `excludeOriginPrefixes?` - Array of origin prefixes to exclude from undo + - `onPush?` - Callback when adding to undo stack + - `onPop?` - Callback when undoing/redoing + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; + +const doc = new LoroDoc(); +const undo = new UndoManager(doc, { + mergeInterval: 1000, + maxUndoSteps: 100, + excludeOriginPrefixes: ["sync-"], +}); +``` + + + + +```typescript no_run +undo(): boolean +``` + + +Undo the last operation. + +**Returns:** True if undo was successful + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); +const text = doc.getText("text"); +text.insert(0, "Hello"); +doc.commit(); + +// Usage example: +const success = undo.undo(); +console.log(success); // true +``` + + + + +```typescript no_run +redo(): boolean +``` + + +Redo the last undone operation. + +**Returns:** True if redo was successful + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); +const text = doc.getText("text"); +text.insert(0, "Hello"); +doc.commit(); +undo.undo(); + +// Usage example: +const success = undo.redo(); +console.log(success); // true +``` + + + + +```typescript no_run +canUndo(): boolean +``` + + +Check if undo is available. + +**Returns:** True if can undo + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +if (undo.canUndo()) { + undo.undo(); +} +``` + + + + +```typescript no_run +canRedo(): boolean +``` + + +Check if redo is available. + +**Returns:** True if can redo + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +if (undo.canRedo()) { + undo.redo(); +} +``` + + + + +```typescript no_run +peer(): PeerID +``` + + +Get the peer ID of the undo manager. + +**Returns:** The peer ID + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +const peerId = undo.peer(); +console.log(peerId); // e.g., "123456" +``` + + + + +```typescript no_run +setMaxUndoSteps(steps: number): void +``` + + +Set the maximum number of undo steps. + +**Parameters:** + +- `steps` - Maximum number of undo steps + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +undo.setMaxUndoSteps(50); +``` + + + + +```typescript no_run +setMergeInterval(interval: number): void +``` + + +Set the merge interval for grouping operations. + +**Parameters:** + +- `interval` - Merge interval in milliseconds + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +undo.setMergeInterval(2000); // 2 seconds +``` + + + + +```typescript no_run +groupStart(): void +``` + + +Begin a manual grouping of subsequent commits into a single undo step. + +**Behavior:** + +- Wrap consecutive `doc.commit()` calls so they undo together +- Calling `groupStart` again before `groupEnd` throws and leaves the current group unchanged +- Conflicting remote imports may automatically end the group and split the undo item + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; + +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); +const text = doc.getText("text"); + +undo.groupStart(); + +text.update("hello", undefined); +doc.commit(); + +text.update("hello world", undefined); +doc.commit(); + +undo.groupEnd(); + +undo.undo(); + +console.log(text.toString()); // "" +``` + + + + +```typescript no_run +groupEnd(): void +``` + + +Close the active manual group and enqueue the grouped operations as a single undo item. + +**Behavior:** + +- Must be paired with a prior `groupStart` +- Safe to call after the group was auto-closed by a conflicting remote import (becomes a no-op) +- Non-conflicting remote updates from other peers remain outside the undo item but do not break the group + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; + +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); +const text = doc.getText("text"); + +undo.groupStart(); + +text.update("hello", undefined); +doc.commit(); +text.update("hello world", undefined); +doc.commit(); + +const snapshot = doc.export({ mode: "snapshot" }); +const doc2 = new LoroDoc(); +doc2.import(snapshot); +doc2.getText("text2").update("hello world world", undefined); // touches a different container +doc2.commit(); +const update = doc2.export({ mode: "update" }); + +doc.import(update); // remote, non-conflicting change + +text.update("hello world world world", undefined); +doc.commit(); + +undo.groupEnd(); + +undo.undo(); + +console.log(text.toString()); // "" +``` + + + + +```typescript no_run +addExcludeOriginPrefix(prefix: string): void +``` + + +Add a prefix to exclude from undo stack. + +**Parameters:** + +- `prefix` - Origin prefix to exclude + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +undo.addExcludeOriginPrefix("sync-"); +undo.addExcludeOriginPrefix("import-"); +``` + + + + +```typescript no_run +clear(): void +``` + + +Clear the undo and redo stacks. + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +undo.clear(); +``` + + + +### Custom Undo Handlers + +Handle cursor restoration and side effects: + +```typescript no_run threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +declare function saveCursorPositions(): any; +declare function restoreCursorPositions(cursors: any): void; + +// Usage example: +const undo = new UndoManager(doc, { + onPush: (isUndo, counterRange, event) => { + // Save cursor positions when adding to undo stack + const cursors = saveCursorPositions(); + return { + value: doc.toJSON(), + cursors: cursors, + }; + }, + + onPop: (isUndo, { value, cursors }, counterRange) => { + // Restore cursor positions when undoing + restoreCursorPositions(cursors); + }, +}); +``` + +--- + +## Types & Interfaces + +Reference for core types used across the API. For conceptual background, see [Containers](/docs/concepts/container), [Version Vector](/docs/concepts/version_vector), [Frontiers](/docs/concepts/frontiers), and the [Versioning Deep Dive](/docs/advanced/version_deep_dive). + +### Core Types + +```typescript no_run +// Peer identifier +type PeerID = `${number}`; + +// Container identifier +type ContainerID = + | `cid:root-${string}:${ContainerType}` + | `cid:${number}@${PeerID}:${ContainerType}`; + +// Tree node identifier +type TreeID = `${number}@${PeerID}`; + +// Operation identifier +type OpId = { + peer: PeerID; + counter: number; +}; + +// Container types +type ContainerType = + | "Text" + | "Map" + | "List" + | "Tree" + | "MovableList" + | "Counter"; + +// Value types +type Value = + | ContainerID + | string + | number + | boolean + | null + | { [key: string]: Value } + | Uint8Array + | Value[] + | undefined; +``` + +### Version Types + +Loro uses two complementary version representations: Version Vectors (per-peer counters) and Frontiers (a compact set of heads). See [Version Vector](/docs/concepts/version_vector) and [Frontiers](/docs/concepts/frontiers). For the full DAG model, see [Version Deep Dive](/docs/advanced/version_deep_dive). + +```typescript no_run +// Version vector class +class VersionVector { + constructor( + value: Map | Uint8Array | VersionVector | undefined | null, + ); + static parseJSON(version: Map): VersionVector; + toJSON(): Map; + encode(): Uint8Array; + static decode(bytes: Uint8Array): VersionVector; + get(peer_id: number | bigint | `${number}`): number | undefined; + compare(other: VersionVector): number | undefined; + setEnd(id: { peer: PeerID; counter: number }): void; + setLast(id: { peer: PeerID; counter: number }): void; + remove(peer: PeerID): void; + length(): number; +} + +// Frontiers represent a specific version +type Frontiers = OpId[]; + +// ID span for range queries +type IdSpan = { + peer: PeerID; + counter: number; + length: number; +}; +``` + +### Change Types + +```typescript no_run +// Change metadata +interface Change { + peer: PeerID; + counter: number; + lamport: number; + length: number; + timestamp: number; // Unix timestamp in seconds + deps: OpId[]; + message: string | undefined; +} + +// Change modifier for pre-commit hooks +interface ChangeModifier { + setMessage(message: string): this; + setTimestamp(timestamp: number): this; +} +``` + +### Cursor Types + +Stable cursors survive concurrent edits and resolve to absolute positions on demand. See [Cursor and Stable Positions](/docs/concepts/cursor_stable_positions) and [Cursor tutorial](/docs/tutorial/cursor). + +```typescript no_run +// Stable position in containers +class Cursor { + containerId(): ContainerID; + pos(): OpId | undefined; + side(): Side; // -1 | 0 | 1 + encode(): Uint8Array; + static decode(data: Uint8Array): Cursor; +} + +// Cursor side affinity +type Side = -1 | 0 | 1; +``` + +### Delta Type + +Delta is a popular rich-text operation format (e.g., Quill). LoroText can export/import Delta; see [Text](/docs/tutorial/text). + +```typescript no_run +// Rich text delta operations +type Delta = + | { + insert: T; + attributes?: { [key in string]: {} }; + retain?: undefined; + delete?: undefined; + } + | { + delete: number; + attributes?: undefined; + retain?: undefined; + insert?: undefined; + } + | { + retain: number; + attributes?: { [key in string]: {} }; + delete?: undefined; + insert?: undefined; + }; +``` + +--- + +## Utility Functions + +Small helpers for type checks and IDs. See [Container IDs](/docs/advanced/cid) and [Versioning Deep Dive](/docs/advanced/version_deep_dive) for frontiers/version encoding. + +### Frontier Encoding + + +```typescript no_run +encodeFrontiers(frontiers: OpId[]): Uint8Array +``` + + +Encode frontiers for efficient transmission. + +**Parameters:** + +- `frontiers` - Array of operation IDs representing frontiers + +**Returns:** Encoded bytes + +**Example:** + +```ts threeslash +import { LoroDoc, encodeFrontiers } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.frontiers(); +const encoded = encodeFrontiers(frontiers); +// Send encoded to remote peers +``` + + + + +```typescript no_run +decodeFrontiers(bytes: Uint8Array): OpId[] +``` + + +Decode frontiers from bytes. + +**Parameters:** + +- `bytes` - Encoded frontier bytes + +**Returns:** Array of operation IDs + +**Example:** + +```typescript no_run threeslash +import { decodeFrontiers } from "loro-crdt"; + +declare const encodedData: Uint8Array; +const frontiers = decodeFrontiers(encodedData); +console.log(frontiers); // [{ peer: "1", counter: 10 }, ...] +``` + + + +### Debugging + + +```typescript no_run +setDebug(): void +``` + + +Enable debug mode for detailed logging. + +**Example:** + +```ts threeslash +import { setDebug } from "loro-crdt"; + +// Enable debug logging +setDebug(); +``` + + + + +```typescript no_run +LORO_VERSION(): string +``` + + +Get the current Loro version. + +**Returns:** Version string + +**Example:** + +```ts threeslash +import { LORO_VERSION } from "loro-crdt"; + +const version = LORO_VERSION(); +console.log("Loro version:", version); +``` + + + +### Import Blob Metadata + + +```typescript no_run +decodeImportBlobMeta(blob: Uint8Array, check_checksum: boolean): ImportBlobMetadata +``` + + +Decode metadata from an import blob. + +**Parameters:** + +- `blob` - The import blob bytes +- `check_checksum` - Whether to verify checksum + +**Returns:** Import blob metadata + +**Example:** + +```typescript no_run threeslash +import { decodeImportBlobMeta } from "loro-crdt"; + +declare const blob: Uint8Array; +const metadata = decodeImportBlobMeta(blob, true); +console.log("Blob metadata:", metadata); +``` + + + +--- + +## EphemeralStore + +Manages ephemeral state like cursor positions and user presence. See [Ephemeral Store](/docs/tutorial/ephemeral) for concepts and usage patterns. Each entry uses timestamp-based LWW (Last-Write-Wins) for conflict resolution. + +**⚠️ Important:** + +- EphemeralStore is a separate CRDT without history - history/operations are NOT persisted +- Perfect for temporary state: cursor positions, selections, typing indicators +- Each peer's state auto-expires after the timeout period +- Uses Last-Write-Wins + +### EphemeralStore + + +```typescript no_run +constructor(timeout?: number) +``` + + +Creates a new EphemeralStore instance. + +**Parameters:** + +- `timeout` - Duration in milliseconds. A peer's state is considered outdated if its last update is older than this timeout. Default is 30000ms (30 seconds). + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; + +// Create ephemeral store with 30 second timeout +const store = new EphemeralStore(30000); +``` + + + + +```typescript no_run +set(key: K, value: T[K]): void +``` + + +Set an ephemeral value. + +**Parameters:** + +- `key` - The key to set +- `value` - The value to store + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); + +// Usage example: +store.set("cursor", { line: 10, column: 5 }); +store.set("selection", { start: 0, end: 10 }); +store.set("user", { name: "Alice", color: "#ff0000" }); +``` + + + + +```typescript no_run +get(key: K): T[K] | undefined +``` + + +Get an ephemeral value. + +**Parameters:** + +- `key` - The key to get + +**Returns:** The stored value or undefined + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); +store.set("cursor", { line: 10 }); + +// Usage example: +const cursor = store.get("cursor"); +console.log(cursor); // { line: 10 } +``` + + + + +```typescript no_run +delete(key: K): void +``` + + +Delete an ephemeral value. + +**Parameters:** + +- `key` - The key to delete + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); +store.set("cursor", { line: 10 }); + +// Usage example: +store.delete("cursor"); +``` + + + + +```typescript no_run +getAllStates(): Partial +``` + + +Get all ephemeral states. + +**Returns:** All stored states + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); +store.set("cursor", { line: 10 }); +store.set("user", { name: "Alice" }); + +// Usage example: +const allStates = store.getAllStates(); +console.log(allStates); +``` + + + + +```typescript no_run +encode(key: K): Uint8Array +``` + + +Encode a specific key's state for transmission. + +**Parameters:** + +- `key` - The key to encode + +**Returns:** Encoded bytes + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); +store.set("cursor", { line: 10 }); + +// Usage example: +const encoded = store.encode("cursor"); +// Send encoded to remote peers +``` + + + + +```typescript no_run +encodeAll(): Uint8Array +``` + + +Encode all states for transmission. + +**Returns:** Encoded bytes + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); +store.set("cursor", { line: 10 }); +store.set("user", { name: "Alice" }); + +// Usage example: +const encoded = store.encodeAll(); +// Send encoded to remote peers +``` + + + + +```typescript no_run +apply(bytes: Uint8Array): void +``` + + +Apply remote updates. + +**Parameters:** + +- `bytes` - Encoded updates from remote peer + +**Example:** + +```typescript no_run threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); + +// Usage example: +declare const remoteData: Uint8Array; +store.apply(remoteData); +``` + + + + +```typescript no_run +keys(): string[] +``` + + +Get all keys in the store. + +**Returns:** Array of keys + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); +store.set("cursor", { line: 10 }); +store.set("user", { name: "Alice" }); + +// Usage example: +const allKeys = store.keys(); +console.log(allKeys); // ["cursor", "user"] +``` + + + + +```typescript no_run +subscribe(listener: EphemeralListener): () => void +``` + + +Subscribe to all ephemeral state changes. + +**Parameters:** + +- `listener` - Callback function for state changes + +**Returns:** Unsubscribe function + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); + +// Usage example: +const unsubscribe = store.subscribe((event) => { + console.log("Ephemeral state changed:", event); +}); + +// Later, unsubscribe +unsubscribe(); +``` + + + + +```typescript no_run +subscribeLocalUpdates(listener: EphemeralLocalListener): () => void +``` + + +Subscribe to local ephemeral updates for syncing to remote peers. + +**Parameters:** + +- `listener` - Callback function that receives encoded updates + +**Returns:** Unsubscribe function + +**Example:** + +```typescript no_run threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); + +// Usage example: +declare const websocket: { send: (data: Uint8Array) => void }; + +const unsubscribe = store.subscribeLocalUpdates((data) => { + // Send to remote peers + websocket.send(data); +}); + +// Later, unsubscribe +unsubscribe(); +``` + + + + +```typescript no_run +destroy(): void +``` + + +Clean up and destroy the ephemeral store. + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); + +// Usage example: +store.destroy(); +``` + + + +### Complete Example + +```typescript no_run threeslash +import { EphemeralStore } from "loro-crdt"; + +// Assume we have: +declare const websocket: { + send: (data: Uint8Array) => void; + on: (event: string, handler: (data: any) => void) => void; +}; + +const store = new EphemeralStore(30000); +const store2 = new EphemeralStore(30000); + +// Subscribe to local updates +store.subscribeLocalUpdates((data) => { + store2.apply(data); +}); + +// Subscribe to all updates +store2.subscribe((event) => { + console.log("event:", event); +}); + +// Set a value +store.set("key", "value"); + +// Encode the value +const encoded = store.encode("key"); + +// Apply the encoded value +store2.apply(encoded); +``` + +--- + +
+ + +# FILE: .open-next/server-functions/default/pages/docs/api/js.mdx + +import styles from "./api-reference.module.css"; +import Indent from "./indent"; +import Method from "./method"; + +
+ +# API Reference + +> _Last updated: 2025-08-09 loro-crdt@1.5.10_ + +## Overview + +Loro is a powerful Conflict-free Replicated Data Type (CRDT) library that enables real-time collaboration. If CRDTs are new to you, start with [What are CRDTs](/docs/concepts/crdt) for a gentle intro. This API reference provides comprehensive documentation for all classes, methods, and types available in the JavaScript/TypeScript binding. + +Note: Under the hood, Loro combines a Fugue-based CRDT core with Eg-walker-inspired techniques that use simple index operations and replay only the divergent history when merging. This yields fast local edits, efficient merges, and low overhead without permanent tombstones. See the primer [Event Graph Walker (Eg-walker)](/docs/advanced/event_graph_walker) and performance notes in the v1.0 blog (import/export speedups, shallow snapshots): https://loro.dev/blog/v1.0 + +## Pitfalls & Best Practices + +**Peer ID Management** + +- **Never share PeerIDs** between concurrent sessions (tabs/devices) - causes document divergence +- Use random PeerIDs (default) unless you have strict single-ownership locking +- Don't assign fixed PeerIDs to users or devices + +**UTF-16 Text Encoding** + +- All text operations use UTF-16 indices by default in JS API +- Slicing in the middle of multi-unit codepoints corrupts them +- Use `insertUtf8()`/`deleteUtf8()` for UTF-8 systems + +**Container Creation** + +- Concurrent child container creation inside the same LoroMap at same key causes overwrites +- Initialize all child containers for a LoroMap upfront when possible +- Operations on the root containers will not override each other + +- Events emit synchronously during commit/import/checkout in JS API (v1.8+). Stay on `<=1.7.x`? Await a microtask before reading batched events. +- Import/export/checkout trigger automatic commits +- Loro transactions are NOT ACID - no rollback/isolation + +**Version Control** + +- After `checkout()`, document enters read-only "detached" mode, unless `setDetachedEditing(true)` is called +- [Frontiers](/docs/concepts/frontiers) can't determine complete operation sets without history + +**Data Structure Choice** + +- Use strings in Map for URLs/IDs (LWW), LoroText for collaborative editing + +## Common Tasks & Examples + +**Getting Started** + +- **Create a document**: [`new LoroDoc()`](#LoroDoc.constructor) - Initialize a new collaborative document +- **Add containers**: [`getText`](#LoroDoc.getText), [`getList`](#LoroDoc.getList), [`getMap`](#LoroDoc.getMap), [`getTree`](#LoroDoc.getTree) +- **Listen to changes**: [`subscribe`](#LoroDoc.subscribe) - React to document modifications +- **Export/Import state**: [`export`](#LoroDoc.export) and [`import`](#LoroDoc.import) - Save and load documents + +**Real-time Collaboration** + +- **Sync between peers**: [`export`](#LoroDoc.export) with `mode: "update"` + [`import`](#LoroDoc.import)/[`importBatch`](#LoroDoc.importBatch) - Exchange incremental updates +- **Stream updates**: [`subscribeLocalUpdates`](#LoroDoc.subscribeLocalUpdates) - Send changes over WebSocket/WebRTC +- **Set unique peer ID**: [`setPeerId`](#LoroDoc.setPeerId) - Ensure each client has a unique identifier +- **Handle conflicts**: Automatic - All Loro data types are CRDTs that merge concurrent edits + +**Rich Text Editing** + +- **Create rich text**: [`getText`](#LoroDoc.getText) - Initialize a collaborative text container +- **Edit text**: [`insert`](#LoroText.insert), [`delete`](#LoroText.delete), [`applyDelta`](#LoroText.applyDelta) +- **Apply formatting**: [`mark`](#LoroText.mark) - Add bold, italic, links, custom styles +- **Copy styled snippets**: [`sliceDelta`](#LoroText.sliceDelta) - Get a Delta for a range (UTF-16; use `sliceDeltaUtf8` for byte offsets) +- **Track cursor positions**: [`getCursor`](#LoroText.getCursor) + [`getCursorPos`](#LoroDoc.getCursorPos) - Stable positions across edits +- **Configure styles**: [`configTextStyle`](#LoroDoc.configTextStyle) - Define expand behavior for marks + +**Data Structures** + +- **Ordered lists**: [`getList`](#LoroDoc.getList) - Arrays with [`push`](#LoroList.push), [`insert`](#LoroList.insert), [`delete`](#LoroList.delete) +- **Key-value maps**: [`getMap`](#LoroDoc.getMap) - Objects with [`set`](#LoroMap.set), [`get`](#LoroMap.get), [`delete`](#LoroMap.delete) +- **Hierarchical trees**: [`getTree`](#LoroDoc.getTree) - File systems, nested comments with [`createNode`](#LoroTree.createNode), [`move`](#LoroTree.move) +- **Reorderable lists**: [`getMovableList`](#LoroDoc.getMovableList) - Drag-and-drop with [`move`](#LoroMovableList.move), [`set`](#LoroMovableList.set) +- **Counters**: [`getCounter`](#LoroDoc.getCounter) - Distributed counters with [`increment`](#LoroCounter.increment) + +**Ephemeral State & Presence** + +- **User presence**: [`EphemeralStore`](#ephemeralstore) - Share cursor positions, selections, user status (not persisted) +- **Cursor syncing**: Use [`EphemeralStore.set`](#EphemeralStore.set) with cursor data from [`getCursor`](#LoroText.getCursor) +- **Live indicators**: Track who's online, typing indicators, mouse positions +- **Important**: EphemeralStore is a separate CRDT without history - perfect for temporary state that shouldn't persist + +**Version Control & History** + +- **Undo/redo**: [`UndoManager`](#undomanager) - Local undo of user's own edits +- **Time travel**: [`checkout`](#LoroDoc.checkout) to any [`frontiers`](#LoroDoc.frontiers) - Debug or review history +- **Version tracking**: [`version`](#LoroDoc.version), [`frontiers`](#LoroDoc.frontiers), [`versionVector`](#LoroDoc.versionVector) +- **Fork documents**: [`fork`](#LoroDoc.fork) or [`forkAt`](#LoroDoc.forkAt) - Create branches for experimentation +- **Merge branches**: [`import`](#LoroDoc.import) - Combine changes from forked documents + +**Performance & Storage** + +- **Incremental updates**: [`export`](#LoroDoc.export) from specific [`version`](#LoroDoc.version) - Send only changes +- **Compact history**: [`export`](#LoroDoc.export) with `mode: "snapshot"` - Full state with compressed history +- **Shallow snapshots**: [`export`](#LoroDoc.export) with `mode: "shallow-snapshot"` - State without partial history (see [Shallow Snapshots](/docs/concepts/shallow_snapshots)) + +## Basic Usage + +```typescript twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); + +// Subscribe to changes +const unsubscribe = doc.subscribe((event) => { + console.log("Document changed:", event); +}); + +// Export updates for synchronization +const updates = doc.export({ mode: "update" }); +``` + +## LoroDoc + +The `LoroDoc` class manages containers, sync, versions, and events. + +**Constructor** + +```typescript no_run +new LoroDoc(); +``` + +Creates a new Loro document with a randomly generated peer ID. + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +``` + +**Static Methods** + + +```typescript no_run +static fromSnapshot(snapshot: Uint8Array): LoroDoc +``` + + +Creates a new LoroDoc from a snapshot. This is useful for loading a document from a previously exported snapshot. + +**Parameters:** + +- `snapshot` - Binary snapshot data + +**Returns:** A new LoroDoc instance + +**Example:** + +```ts no_run twoslash +import { LoroDoc } from "loro-crdt"; + +// Assume we have a snapshot from a previous export +const prevDoc = new LoroDoc(); +prevDoc.getText("text").insert(0, "Hello"); +const snapshot: Uint8Array = prevDoc.export({ mode: "snapshot" }); + +const doc = LoroDoc.fromSnapshot(snapshot); +``` + + + +**Properties** + + +```typescript no_run +readonly peerId: bigint +``` + + +Gets the peer ID of the current writer as a bigint. + +**See Also:** [PeerID Management](/docs/concepts/peerid_management) + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const id = doc.peerId; +``` + + + + +```typescript no_run +readonly peerIdStr: `${number}` +``` + + +Gets the peer ID as a decimal string. + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const idStr = doc.peerIdStr; +``` + + + +### Configuration Methods + + +```typescript no_run +setPeerId(peer: number | bigint | `${number}`): void +``` + + +Sets the peer ID for this document. It must be a number, a BigInt, or a decimal string that fits into an unsigned 64-bit integer. +See [PeerID Management](/docs/concepts/peerid_management) for why uniqueness matters in distributed systems. + +**Parameters:** + +- `peer` - Peer ID as number, bigint, or decimal string + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setPeerId("42"); +``` + +**⚠️ Critical Pitfall:** Never let two parallel peers (e.g., multiple tabs/devices) share the same PeerID β€” it creates duplicate op IDs and causes document divergence. Common mistakes: + +- Don't assign a fixed PeerId to a user (users have multiple devices) +- Don't assign a fixed PeerId to a device (multiple tabs can open the same document) +- If you must reuse PeerIDs, enforce single ownership with strict locking mechanisms +- Best practice: Use random IDs (default behavior) unless you have a strong reason not to + +See [PeerID reuse](/docs/tutorial/tips) for safe reuse patterns. + + + + +```typescript no_run +setRecordTimestamp(auto_record: boolean): void +``` + + +Configures whether to automatically record timestamps for changes. Timestamps use Unix time (seconds since epoch). Learn more about storing timestamps and typical use cases in [Storing Timestamps](/docs/advanced/timestamp). + +**Parameters:** + +- `auto_record` - Whether to automatically record timestamps + +**⚠️ Important:** This setting doesn't persist in exported Updates or Snapshots. You must reapply this configuration each time you initialize a document. + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setRecordTimestamp(true); +``` + + + + +```typescript no_run +setChangeMergeInterval(interval: number): void +``` + + +Sets the interval in milliseconds for merging continuous local changes into a single change record. In Loro, multiple low-level operations are grouped into higher-level Changes for readability and syncing. See [Operations and Changes](/docs/concepts/operations_changes). + +**Parameters:** + +- `interval` - Merge interval in milliseconds + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setChangeMergeInterval(1000); // Merge changes within 1 second +``` + + + + +```typescript no_run +configTextStyle(styles: StyleConfig): void +``` + + +Configures the behavior of text styles (marks) in rich text containers. Marks can expand when edits happen at their edges (before/after/both/none). For a primer on rich text and marks in Loro, see [Text](/docs/tutorial/text). + +**Parameters:** + +- `styles` - Configuration object mapping style names to their config + +**StyleConfig Type:** + +```typescript no_run +type StyleConfig = Record< + string, + { + expand?: "after" | "before" | "both" | "none"; + } +>; +``` + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.configTextStyle({ + bold: { expand: "after" }, + italic: { expand: "none" }, + link: { expand: "none" }, +}); +``` + + + + +```typescript no_run +configDefaultTextStyle(style?: { expand: "after" | "before" | "both" | "none" }): void +``` + + +Configures the default text style for the document when using LoroText. If undefined is provided, the default style is reset. + +**Parameters:** + +- `style` - Default style configuration (optional) + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.configDefaultTextStyle({ expand: "after" }); +``` + + + +### Container Access Methods + +**πŸ“ Note:** Creating root containers (e.g., `doc.getText("...")`) does not record operations; nested container creation (e.g., `map.setContainer(...)`) does. + +**⚠️ Pitfall:** Avoid concurrent creation of regular child containers with the same key in LoroMaps. Instead of: + +```ts no_run +// Dangerous - concurrent peers can create different child container IDs +doc.getMap("user").getOrCreateContainer(userId, new LoroMap()); +``` + +Use a mergeable child container when the child should be identified by its logical position: + +```ts no_run +doc.getMap("user").ensureMergeableMap(userId); +``` + +Alternatively, use a unique root container if that better fits your model: + +```ts no_run +doc.getMap("user." + userId); +``` + + +```typescript no_run +getText(name: string): LoroText +``` + + + +Gets or creates a text container with the given name. New to LoroText and marks? See [Text](/docs/tutorial/text). + +**Parameters:** + +- `name` - The container name + +**Returns:** A `LoroText` instance + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const text = doc.getText("content"); +text.insert(0, "Hello"); +``` + + + + +```typescript no_run +getList(name: string): LoroList +``` + + + +Gets or creates a list container with the given name. Unsure whether to use List or MovableList? See [List and Movable List](/docs/tutorial/list) and the type selection guide [Choosing CRDT Types](/docs/concepts/choose_crdt_type). + +**Parameters:** + +- `name` - The container name + +**Returns:** A `LoroList` instance + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const list = doc.getList("items"); +list.push("Item 1"); +``` + + + + +```typescript no_run +getMap(name: string): LoroMap +``` + + +Gets or creates a map container with the given name. See [Map](/docs/tutorial/map) for basics and patterns. + +**Parameters:** + +- `name` - The container name + +**Returns:** A `LoroMap` instance + +**Example:** + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const map = doc.getMap("settings"); +map.set("theme", "dark"); +``` + + + + +```typescript no_run +getTree(name: string): LoroTree +``` + + +Gets or creates a tree container with the given name. Learn about hierarchical editing and moves in [Tree](/docs/tutorial/tree). + +**Parameters:** + +- `name` - The container name + +**Returns:** A `LoroTree` instance + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const tree = doc.getTree("fileSystem"); +const root = tree.createNode(); +``` + + + + +```typescript no_run +getCounter(name: string): LoroCounter +``` + + +Gets or creates a counter container with the given name. Counters are special CRDTs that sum concurrent increments; see [Counter](/docs/tutorial/counter). + +**Parameters:** + +- `name` - The container name + +**Returns:** A `LoroCounter` instance + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const counter = doc.getCounter("likes"); +counter.increment(1); +``` + + + + +```typescript no_run +getMovableList(name: string): LoroMovableList +``` + + +Gets or creates a movable list container with the given name. MovableList is designed for reordering with concurrent moves. See [List and Movable List](/docs/tutorial/list) and [Choosing CRDT Types](/docs/concepts/choose_crdt_type). + +**Parameters:** + +- `name` - The container name + +**Returns:** A `LoroMovableList` instance + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const movableList = doc.getMovableList("tasks"); +movableList.push("Task 1"); +movableList.push("Task 2"); +movableList.push("Task 3"); +movableList.move(0, 2); // Move first item to third position +``` + + + + +```typescript no_run +getContainerById(id: ContainerID): Container | undefined +``` + + +Gets a container by its unique ID. Container IDs (CID) uniquely reference containers across updates; see [Container ID](/docs/advanced/cid) and [Container](/docs/concepts/container). + +**Parameters:** + +- `id` - The container ID + +**Returns:** The container instance or undefined if not found + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const text = doc.getText("text"); +const textId = text.id; +const sameText = doc.getContainerById(textId); +``` + + + +### Import/Export Methods + + +```typescript no_run +export(mode?: ExportMode): Uint8Array +``` + + +Exports the document in various formats for synchronization or persistence. For a walkthrough of export modesβ€”snapshot, update, shallow-snapshot, and updates-in-rangeβ€”see [Export Mode](/docs/tutorial/encoding). Shallow snapshots remove history while keeping current state; see [Shallow Snapshots](/docs/concepts/shallow_snapshots). VersionVector and Frontiers are two ways to represent versions; see [Version Vector](/docs/concepts/version_vector) and [Frontiers](/docs/concepts/frontiers). + +**Parameters:** + +- `mode` - Export configuration (optional) + +**ExportMode Options:** + +```typescript no_run +type ExportMode = + | { mode: "snapshot" } + | { mode: "update"; from?: VersionVector } + | { mode: "shallow-snapshot"; frontiers: Frontiers } + | { mode: "updates-in-range"; spans: { id: OpId; len: number }[] }; +``` + +**Returns:** Encoded binary data + +**⚠️ Important Notes:** + +- **Shallow snapshots**: Cannot import updates from before the shallow start point. Peers can only sync if they have versions after this point. +- **Auto-commit**: The document automatically commits pending operations before export. +- **Performance**: Export new snapshots periodically to reduce import times for new peers. + +**Examples:** + +```typescript no_run +import { LoroDoc, VersionVector } from "loro-crdt"; + +const doc = new LoroDoc(); +// ... make some changes to the document ... + +// Export full snapshot +const snapshot = doc.export({ mode: "snapshot" }); + +// Export updates from a specific version +const lastSyncVersion = doc.version(); // Get current version +// ... make more changes ... +const updates = doc.export({ + mode: "update", + from: lastSyncVersion, +}); + +// Export shallow snapshot at current version +const shallowSnapshot = doc.export({ + mode: "shallow-snapshot", + frontiers: doc.frontiers(), +}); +``` + + + + +```typescript no_run +import(data: Uint8Array): ImportStatus +``` + + +Imports updates or snapshots into the document. Returns an `ImportStatus` describing which peer ranges were applied or are pending. See [Sync](/docs/tutorial/sync) and [Import Status](/docs/concepts/import_status) for how Loro handles out-of-order and partial updates. + +**Parameters:** + +- `data` - Binary data or another LoroDoc to import from + +**⚠️ Important:** LoroDoc will automatically commits pending operations before import. If the doc is in detached mode, the imported operations are recorded into OpLog but not applied to DocState until you call `attach()`, see [Attached vs Detached States](/docs/concepts/attached_detached) adn [OpLog and DocState](/docs/concepts/oplog_docstate). + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +// Receive updates from another peer (e.g., via network) +const otherDoc = new LoroDoc(); +otherDoc.getText("text").insert(0, "Hello"); +const updates: Uint8Array = otherDoc.export({ mode: "update" }); + +// Import binary updates +const status = doc.import(updates); +console.log(status.success); +``` + + + + +```typescript no_run +importBatch(data: Uint8Array[]): ImportStatus +``` + + +Efficiently imports multiple updates in a single batch operation. See [Batch Import](/docs/advanced/import_batch) for performance considerations and usage. + +**Parameters:** + +- `data` - Array of binary updates + +**Example:** + +```typescript no_run +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +declare const update1: Uint8Array; +declare const update2: Uint8Array; +declare const update3: Uint8Array; + +// Usage example: +const updates = [update1, update2, update3]; +const status = doc.importBatch(updates); +``` + + + + +```typescript no_run +exportJsonUpdates(start?: VersionVector, end?: VersionVector, withPeerCompression?: boolean): JsonSchema +``` + + +Exports updates in JSON format for debugging or alternative storage. See [Export Mode](/docs/tutorial/encoding) for format details and trade-offs. + +**Parameters:** + +- `start` - Starting version (optional) +- `end` - Ending version (optional) + +**Returns:** JSON representation of updates + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const jsonUpdates = doc.exportJsonUpdates(); +console.log(JSON.stringify(jsonUpdates, null, 2)); +``` + + + + +```typescript no_run +importJsonUpdates(json: string | JsonSchema): void +``` + + +Imports updates from JSON format. Useful for debugging, migration, or custom storage layers; see [Export Mode](/docs/tutorial/encoding). + +**Parameters:** + +- `json` - JSON string or object containing updates + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const otherDoc = new LoroDoc(); +otherDoc.getText("text").insert(0, "Hello"); +const jsonStr = otherDoc.exportJsonUpdates(); +doc.importJsonUpdates(jsonStr); +``` + + + +## Versioning + +Work with the history DAG using frontiers (heads) and version vectors. Switch, branch, and merge versions safely without manual conflict resolution. See [Versioning Deep Dive](/docs/advanced/version_deep_dive) and [Attached vs Detached States](/docs/concepts/attached_detached). + +### Version Control Methods + + +```typescript no_run +checkout(frontiers: Frontiers): void +``` + + +Checks out the document to a specific version, making it read-only at that point in history. This is the core of time travel; see [Time Travel](/docs/tutorial/time_travel) and [Version](/docs/tutorial/version). + +**Parameters:** + +- `frontiers` - Array of OpIds representing the target version + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.frontiers(); +// Make some changes... +doc.checkout(frontiers); // Go back to previous version +``` + + + + +**⚠️ Important:** In Loro 1.0, `version()`/`frontiers()` include pending (uncommitted) local operations. + +**πŸ“ Note:** After `checkout()`, the document enters "detached" mode and becomes read-only by default. Use `attach()` or `checkoutToLatest()` to return to editing mode. See [Version Deep Dive](/docs/advanced/version_deep_dive) and [Attached vs Detached States](/docs/concepts/attached_detached). + + + + +```typescript no_run +checkoutToLatest(): void +``` + + +Returns the document to the latest version after a checkout. Related concepts: [Frontiers](/docs/concepts/frontiers) and [Version Vector](/docs/concepts/version_vector). + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.checkoutToLatest(); +``` + + + + +```typescript no_run +attach(): void +``` + + +Attaches the document to track latest changes after being detached. See [Attached vs Detached States](/docs/concepts/attached_detached) for how Loro separates current state from history. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.attach(); +``` + + + + +```typescript no_run +detach(): void +``` + + +Detaches the document from tracking latest changes, freezing it at current version. See [Attached vs Detached States](/docs/concepts/attached_detached). + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.detach(); +``` + + + + +```typescript no_run +fork(): LoroDoc +``` + + +Creates a new document that is a fork of the current one with a new peer ID. Forking is useful for branching workflows; see [Version](/docs/tutorial/version). + +**Returns:** A new LoroDoc instance + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const forkedDoc = doc.fork(); +``` + + + + +```typescript no_run +forkAt(frontiers: Frontiers): LoroDoc +``` + + +Creates a fork at a specific version in history. Learn more about versions, DAG history, and heads in [Version Deep Dive](/docs/advanced/version_deep_dive). + +**Parameters:** + +- `frontiers` - The version to fork from + +**Returns:** A new LoroDoc instance + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.frontiers(); +const forkedDoc = doc.forkAt(frontiers); +``` + + + +## Events & Transactions + +React to changes and group local operations into transactions. Starting in v1.8, events are delivered synchronously; older releases require awaiting a microtask. See [Event Handling](/docs/tutorial/event) and [Transaction Model](/docs/concepts/transaction_model). + +### Subscription Methods + + +```typescript no_run +subscribe(listener: (event: LoroEventBatch) => void): () => void +``` + + +Subscribes to all document changes. See [Event Handling](/docs/tutorial/event) for the event model and best practices. + +**Parameters:** + +- `listener` - Callback function that receives change events + +**Returns:** Unsubscribe function + +**Event Structure:** + +```typescript no_run +interface LoroEventBatch { + by: "local" | "import" | "checkout"; + origin?: string; + currentTarget?: ContainerID; + events: LoroEvent[]; + from: Frontiers; + to: Frontiers; +} +``` + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const unsubscribe = doc.subscribe((event) => { + console.log("Change type:", event.by); + event.events.forEach((e) => { + console.log("Container changed:", e.target); + console.log("Diff:", e.diff); + }); +}); + +// Later: unsubscribe(); +``` + +**⚠️ Important:** Events are emitted synchronously as of v1.8. If you are pinned to `<=1.7.x`, await a microtask before reading the batch. + +```ts no_run +doc.commit(); +// Events have already been delivered in v1.8+ +// await Promise.resolve(); // Only needed on <=1.7.x +``` + +**πŸ“ Note:** Multiple operations before a commit are batched into a single event. See [Event Handling](/docs/tutorial/event). + + + + +```typescript no_run +subscribeLocalUpdates(f: (bytes: Uint8Array) => void): () => void +``` + + +Subscribes only to local changes, useful for syncing with remote peers. This is typically wired to your transport layer; see [Sync](/docs/tutorial/sync). + +**Parameters:** + +- `f` - Callback that receives binary updates + +**Returns:** Unsubscribe function + +**Example:** + +```ts no_run threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +declare const websocket: { send: (data: Uint8Array) => void }; + +// Usage example: +const unsubscribe = doc.subscribeLocalUpdates((updates) => { + // Send updates to remote peers + websocket.send(updates); +}); +``` + + + + +```typescript no_run +subscribeFirstCommitFromPeer(f: (e: { peer: PeerID }) => void): () => void +``` + + +Subscribes to the first commit from each peer, useful for tracking peer metadata. + +**Parameters:** + +- `f` - Callback that receives peer information + +**Returns:** Unsubscribe function + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.subscribeFirstCommitFromPeer(({ peer }) => { + // Store peer metadata + doc.getMap("peers").set(peer, { + joinedAt: Date.now(), + name: `User ${peer}`, + }); +}); +``` + + + +### Transaction Methods + + +```typescript no_run +commit(options?: { origin?: string, message?: string, timestamp?: number }): void +``` + + +Commits pending changes as a single transaction. A transaction groups operations into a Change; see [Operations and Changes](/docs/concepts/operations_changes). + +**⚠️ Critical Distinction:** Loro transactions are NOT ACID database transactions: + +- No rollback capability +- No isolation guarantees +- Purpose: Bundle local operations for event batching and history grouping +- Many operations (import/export/checkout) trigger implicit commits + +See [Transaction Model](/docs/concepts/transaction_model). + +**Parameters:** + +- `options` - Optional commit configuration + - `message` - Commit message (persisted in the document like a git commit message, visible to all peers after sync) + - `origin` - Origin identifier (local only - used for marking local events, remote peers won't see this) + - `timestamp` - Unix timestamp in seconds (see [Storing Timestamps](/docs/advanced/timestamp)) + +**Important distinction:** + +- `message` is persisted in the document's history and will be synchronized to all peers, similar to git commit messages +- `origin` is only used locally for filtering events (e.g., excluding certain origins from undo) and is NOT synchronized to remote peers + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.commit({ + message: "Updated document title", // Persisted & synced to all peers + origin: "user-action", // Local only, for event filtering + timestamp: Math.floor(Date.now() / 1000), +}); +``` + + + +### Query Methods + + +```typescript no_run +toJSON(): Value +``` + + +Converts the entire document to a JSON-compatible value. If you prefer a structure where sub-containers are referenced by ID (for privacy or streaming), use getShallowValue(); see [Shallow Snapshots](/docs/concepts/shallow_snapshots). + +**Returns:** JSON representation of the document + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const json = doc.toJSON(); +console.log(JSON.stringify(json, null, 2)); +``` + + + + +```typescript no_run +getShallowValue(): Record +``` + + +Gets a shallow representation where sub-containers are represented by their IDs. This is helpful when you want to share structure without history; see [Shallow Snapshots](/docs/concepts/shallow_snapshots). + +**Returns:** Shallow JSON value + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const shallow = doc.getShallowValue(); +// Sub-containers appear as: "cid:..." +``` + + + + +```typescript no_run +getDeepValueWithID(): any +``` + + +Gets the deep value of the document with container IDs preserved. This is useful when you need to traverse the document structure while maintaining references to container IDs. + +**Returns:** Document value with container IDs + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const deepValue = doc.getDeepValueWithID(); +``` + + + + +```typescript no_run +version(): VersionVector +``` + + +Gets the current version vector of the document. Version vectors track how much data from each peer you’ve seen; see [Version Vector](/docs/concepts/version_vector). + +**Returns:** Map from PeerID to counter + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const vv = doc.version(); +console.log(vv.toJSON()); +``` + + + + +```typescript no_run +frontiers(): Frontiers +``` + + +Gets the current frontiers (heads) of the document. Frontiers are a compact representation of a version; see [Frontiers](/docs/concepts/frontiers) for when to use them instead of version vectors. + +**πŸ“ Note:** Frontiers are a compact version representation. + +**⚠️ Limitation:** When you have a Frontier pointing to operations you don't know about, you cannot determine the complete set of operation IDs included in that version. Version Vectors don't have this limitation but are more verbose. See [Frontiers](/docs/concepts/frontiers) for trade-offs. + +**Returns:** Array of OpIds + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.frontiers(); +// Can be used for checkouts or shallow snapshots +``` + + + + +```typescript no_run +diff(from: Frontiers, to: Frontiers, for_json?: boolean): [ContainerID, Diff | JsonDiff][] +``` + + +Calculates differences between two versions. Understanding how Loro computes diffs benefits from the history DAG model; see [Version Deep Dive](/docs/advanced/version_deep_dive). + +**Parameters:** + +- `from` - Starting frontiers +- `to` - Ending frontiers +- `for_json` - If true, returns JsonDiff format (default: true) + +**Returns:** Array of container IDs and their diffs + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const fromFrontiers = doc.frontiers(); +// Make changes... +const toFrontiers = doc.frontiers(); + +const diffs = doc.diff(fromFrontiers, toFrontiers); +diffs.forEach(([containerId, diff]) => { + console.log(`Container ${containerId} changed:`, diff); +}); +``` + + + +### Pre-Commit Hook + + +```typescript no_run +subscribePreCommit(f: (e: { changeMeta: Change, origin: string, modifier: ChangeModifier }) => void): () => void +``` + + +Subscribe to the pre-commit event. You can modify the message and timestamp of the next change. This hook runs right before a Change is recorded; see [Transaction Model](/docs/concepts/transaction_model) and [Operations and Changes](/docs/concepts/operations_changes). + +Pitfall: `commit()` can be triggered implicitly by `import`, `export`, and `checkout`. Use this hook to attach metadata even for those implicit commits. + +```typescript no_run +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const unsubscribe = doc.subscribePreCommit(({ modifier }) => { + modifier + .setMessage("Tagged by pre-commit") + .setTimestamp(Math.floor(Date.now() / 1000)); +}); +doc.getText("text").insert(0, "Hello"); +doc.commit(); +unsubscribe(); +``` + + + +### Cursor Utilities + + +```typescript no_run +getCursorPos(cursor: Cursor): { update?: Cursor, offset: number, side: Side } +``` + + +Resolve a stable `Cursor` to an absolute position. Cursors remain valid across concurrent edits; see [Cursor and Stable Positions](/docs/concepts/cursor_stable_positions) and [Cursor tutorial](/docs/tutorial/cursor). The side controls affinity when the cursor sits at an insertion boundary. + +```typescript no_run +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "abc"); + +// Get cursor at position 1 +const c0 = text.getCursor(1); +const pos = doc.getCursorPos(c0!); +console.log(pos.offset); // 1 +``` + + + +### Pending Operations + + +```typescript no_run +getUncommittedOpsAsJson(): JsonSchema | undefined +``` + + +Get pending operations from the current transaction in JSON format. Useful for debugging what will be included in the next Change; see [Transaction Model](/docs/concepts/transaction_model). + +```typescript no_run +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +text.insert(0, "Hello"); +const pending = doc.getUncommittedOpsAsJson(); +doc.commit(); +const none = doc.getUncommittedOpsAsJson(); // undefined after commit +``` + + + +### Change Graph & History + +These APIs traverse the history DAG of changes (ancestors/descendants, spans). If this sounds unfamiliar, start with Loro's [Versioning Deep Dive](/docs/advanced/version_deep_dive) and the [Event Graph Walker](/docs/concepts/event_graph_walker). + + +```typescript no_run +travelChangeAncestors(ids: OpId[], f: (change: Change) => boolean): void +``` + + +Visit ancestors of the given changes in causal order. + +```typescript no_run +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.getText("text").insert(0, "Hello"); +doc.commit(); +const head = doc.frontiers(); +doc.travelChangeAncestors(head, (change) => { + console.log(change.peer, change.counter); + return true; // continue +}); +``` + + + + + ```typescript no_run findIdSpansBetween(from: Frontiers, to: Frontiers): + VersionVectorDiff ``` + +Find the op id spans that lie between two versions. + + +```typescript no_run +exportJsonInIdSpan(idSpan: { peer: PeerID, counter: number, length: number }): JsonChange[] +``` + + +```typescript no_run +import { LoroDoc } from "loro-crdt"; +const a = new LoroDoc(); +const b = new LoroDoc(); + +// Usage example: +a.getText("text").update("Hello"); +a.commit(); +const snapshot = a.export({ mode: "snapshot" }); +let printed: any; +b.subscribe((e) => { +const spans = b.findIdSpansBetween(e.from, e.to); +const changes = b.exportJsonInIdSpan(spans.forward[0]); +printed = changes; +}); +b.import(snapshot); + +```` + + + +```typescript no_run +getChangedContainersIn(id: OpId, len: number): ContainerID[] +```` + + + +Get container IDs modified in the given ID range. + +```typescript no_run +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.getList("list").insert(0, 1); +doc.commit(); +const head = doc.frontiers()[0]; +const containers = doc.getChangedContainersIn(head, 1); +``` + + + +### Revert & Apply Diff + + +```typescript no_run +revertTo(frontiers: Frontiers): void +``` + + +Revert the document to a given version by generating inverse operations. + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setPeerId("1"); +const t = doc.getText("text"); +t.update("Hello"); +doc.commit(); +doc.revertTo([{ peer: "1", counter: 1 }]); +``` + + + + +```typescript no_run +applyDiff(diff: [ContainerID, Diff | JsonDiff][]): void +``` + + +Apply a batch of diffs to the document. + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc1 = new LoroDoc(); +const doc2 = new LoroDoc(); + +// Usage example: +doc1.getText("text").insert(0, "Hello"); +const diff = doc1.diff([], doc1.frontiers()); +doc2.applyDiff(diff); +``` + +**Workflow example (squash-like diffs):** For PR-style reviews, combine `diff` and `applyDiff` to send a compact change set between a base version and a new version. Operations that cancel out (insert + delete) are compressed away. + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const baseDoc = new LoroDoc(); +const baseText = baseDoc.getText("text"); +baseText.insert(0, "hello world"); + +// Fork to make isolated edits +const newDoc = baseDoc.fork(); +const newText = newDoc.getText("text"); +newText.insert(0, "abc"); +newText.delete(0, 4); + +const diff = newDoc.diff(baseDoc.frontiers(), newDoc.frontiers()); +console.log(diff); +// [ +// [ +// "cid:root-text:Text", +// { type: "text", diff: [ { delete: 1 } ] } +// ] +// ] + +baseDoc.applyDiff(diff); +console.log(baseDoc.toJSON()); +// { text: "ello world" } +``` + + + +### Detached Editing + + +```typescript no_run +setDetachedEditing(enable: boolean): void +``` + + +Enables or disables detached editing mode. Detached editing lets you stage edits separate from the latest head; see [Attached vs Detached States](/docs/concepts/attached_detached). + +**Parameters:** + +- `enable` - Whether to enable detached editing + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setDetachedEditing(true); +``` + + + + +```typescript no_run +isDetachedEditingEnabled(): boolean +``` + + +Checks if detached editing mode is enabled. + +**Returns:** True if detached editing is enabled + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const enabled = doc.isDetachedEditingEnabled(); +``` + + + + +```typescript no_run +isDetached(): boolean +``` + + +Checks if the document is currently detached. + +**Returns:** True if document is detached + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +console.log(doc.isDetached()); +``` + + + +### Commit Options Helpers + + +```typescript no_run +setNextCommitMessage(msg: string): void +``` + + +Sets the message for the next commit. + +**Parameters:** + +- `msg` - Commit message + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setNextCommitMessage("User action"); +``` + + + + +```typescript no_run +setNextCommitOrigin(origin: string): void +``` + + +Sets the origin for the next commit. + +**Parameters:** + +- `origin` - Origin identifier + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setNextCommitOrigin("ui"); +``` + + + + +```typescript no_run +setNextCommitTimestamp(timestamp: number): void +``` + + +Sets the timestamp for the next commit. + +**Parameters:** + +- `timestamp` - Unix timestamp in seconds + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setNextCommitTimestamp(Math.floor(Date.now() / 1000)); +``` + + + + +```typescript no_run +setNextCommitOptions(options: { origin?: string, timestamp?: number, message?: string }): void +``` + + +Sets multiple options for the next commit. + +**Parameters:** + +- `options` - Commit options object + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setNextCommitOptions({ origin: "ui", message: "batch" }); +doc.getText("text").insert(0, "Hi"); +doc.commit(); +``` + + + + +```typescript no_run +clearNextCommitOptions(): void +``` + + +Clears all pending commit options. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.clearNextCommitOptions(); +``` + + + +### Version & Frontier Utilities + + +```typescript no_run +frontiersToVV(frontiers: Frontiers): VersionVector +``` + + +Converts frontiers to a version vector. + +**Parameters:** + +- `frontiers` - Frontiers to convert + +**Returns:** Version vector representation + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.frontiers(); +const vv = doc.frontiersToVV(frontiers); +``` + + + + +```typescript no_run +vvToFrontiers(vv: VersionVector): Frontiers +``` + + +Converts a version vector to frontiers. + +**Parameters:** + +- `vv` - Version vector to convert + +**Returns:** Frontiers representation + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const vv = doc.version(); +const frontiers = doc.vvToFrontiers(vv); +``` + + + + +```typescript no_run +oplogVersion(): VersionVector +``` + + +Gets the oplog version vector. + +**Returns:** Oplog version vector + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const vv = doc.oplogVersion(); +``` + + + + +```typescript no_run +oplogFrontiers(): Frontiers +``` + + +Gets the oplog frontiers. + +**Returns:** Oplog frontiers + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.oplogFrontiers(); +``` + + + + +```typescript no_run +cmpWithFrontiers(frontiers: Frontiers): -1 | 0 | 1 +``` + + +Compares current document state with given frontiers. + +**Parameters:** + +- `frontiers` - Frontiers to compare with + +**Returns:** -1 if behind, 0 if equal, 1 if ahead + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.frontiers(); +const cmp = doc.cmpWithFrontiers(frontiers); +``` + + + + +```typescript no_run +cmpFrontiers(a: Frontiers, b: Frontiers): -1 | 0 | 1 | undefined +``` + + +Compares two frontiers. + +**Parameters:** + +- `a` - First frontiers +- `b` - Second frontiers + +**Returns:** -1 if a < b, 0 if equal, 1 if a > b, undefined if incomparable + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const f1 = doc.frontiers(); +const f2 = doc.frontiers(); +const cmp = doc.cmpFrontiers(f1, f2); +``` + + + +### JSONPath & Path Queries + +Use simple path strings and JSONPath to fetch nested values and containers. Paths are formed from root container names and keys (e.g., map/key or list/0). For container IDs, see [Container ID](/docs/advanced/cid). + + +```typescript no_run +getByPath(path: string): Value | Container | undefined +``` + + +Gets a value or container by its path. + +**Parameters:** + +- `path` - Path string (e.g., "map/key") + +**Returns:** Value or container at the path, or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); +map.set("key", 1); + +// Usage example: +const value = doc.getByPath("map/key"); +``` + + + + +```typescript no_run +getPathToContainer(id: ContainerID): (string | number)[] | undefined +``` + + +Gets the path to a container by its ID. + +**Parameters:** + +- `id` - Container ID + +**Returns:** Array representing the path, or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const path = doc.getPathToContainer(map.id); +``` + + + + +```typescript no_run +JSONPath(jsonpath: string): any[] +``` + + +Queries the document using JSONPath syntax. + +**Parameters:** + +- `jsonpath` - JSONPath query string + +**Returns:** Array of matching values + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); +map.set("key", 1); + +// Usage example: +const results = doc.JSONPath("$.map"); +``` + + + +### Shallow Doc Utilities + +These helpers relate to shallow snapshots and redaction. If you need a refresher on what β€œshallow” means, see [Shallow Snapshots](/docs/concepts/shallow_snapshots). + + +```typescript no_run +shallowSinceVV(): VersionVector +``` + + +Gets the version vector since which the document is shallow. + +**Returns:** Version vector + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const vv = doc.shallowSinceVV(); +``` + + + + +```typescript no_run +shallowSinceFrontiers(): Frontiers +``` + + +Gets the frontiers since which the document is shallow. + +**Returns:** Frontiers + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.shallowSinceFrontiers(); +``` + + + + +```typescript no_run +isShallow(): boolean +``` + + +Checks if the document is shallow. + +**Returns:** True if document is shallow + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const shallow = doc.isShallow(); +``` + + + + +```typescript no_run +setHideEmptyRootContainers(hide: boolean): void +``` + + +Controls whether empty root containers are hidden in JSON output. + +**Parameters:** + +- `hide` - Whether to hide empty root containers + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.setHideEmptyRootContainers(true); +// Now empty roots are hidden in toJSON() +``` + + + + +```typescript no_run +deleteRootContainer(cid: ContainerID): void +``` + + +Deletes a root container. + +**Parameters:** + +- `cid` - Container ID to delete + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +// Usage example: +doc.deleteRootContainer(map.id); +``` + + + + +```typescript no_run +hasContainer(id: ContainerID): boolean +``` + + +Checks if a container exists in the document. + +**Parameters:** + +- `id` - Container ID to check + +**Returns:** True if container exists + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const exists = doc.hasContainer(map.id); +``` + + + +### JSON Serialization with Replacer + + +```typescript no_run +toJsonWithReplacer(replacer: (key: string | number, value: Value | Container) => Value | Container | undefined): Value +``` + + +Customize JSON serialization of containers and values. + +**Parameters:** + +- `replacer` - Function to transform values during serialization + +**Returns:** Customized JSON value + +**Example:** + +```ts threeslash +import { LoroDoc, LoroText } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); + +// Usage example: +const json = doc.toJsonWithReplacer((key, value) => { + if (value instanceof LoroText) { + return value.toDelta(); + } + return value; +}); +``` + + + +### Stats & Introspection + + +```typescript no_run +debugHistory(): void +``` + + +Prints debug information about the document history. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.debugHistory(); +``` + + + + +```typescript no_run +changeCount(): number +``` + + +Gets the total number of changes in the document. + +**Returns:** Number of changes + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const changes = doc.changeCount(); +``` + + + + +```typescript no_run +opCount(): number +``` + + +Gets the total number of operations in the document. + +**Returns:** Number of operations + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const ops = doc.opCount(); +``` + + + + +```typescript no_run +getAllChanges(): Map +``` + + +Gets all changes grouped by peer ID. + +**Returns:** Map of peer ID to changes + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const changes = doc.getAllChanges(); +``` + + + + +```typescript no_run +getChangeAt(id: OpId): Change +``` + + +Gets a specific change by operation ID. + +**Parameters:** + +- `id` - Operation ID + +**Returns:** Change object + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.getText("text").insert(0, "hello"); +doc.commit(); +const changes = doc.getAllChanges(); +const change = changes.get(doc.peerIdStr)?.[0]; +``` + + + + +```typescript no_run +getChangeAtLamport(peer_id: string, lamport: number): Change | undefined +``` + + +Gets a change by peer ID and Lamport timestamp. + +**Parameters:** + +- `peer_id` - Peer ID +- `lamport` - Lamport timestamp + +**Returns:** Change object or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.getText("text").insert(0, "hello"); +const change = doc.getChangeAtLamport(doc.peerIdStr, 1); +``` + + + + +```typescript no_run +getOpsInChange(id: OpId): any[] +``` + + +Gets all operations in a specific change. + +**Parameters:** + +- `id` - Operation ID + +**Returns:** Array of operations + +**Example:** + +```typescript no_run +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.getText("text").insert(0, "hello"); +const changes = doc.getAllChanges(); +const ops = doc.getOpsInChange(changes[0].id); +``` + + + + +```typescript no_run +getPendingTxnLength(): number +``` + + +Gets the number of pending operations in the current transaction. + +**Returns:** Number of pending operations + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +doc.getText("text").insert(0, "x"); +console.log(doc.getPendingTxnLength()); +doc.commit(); +``` + + + +### Import/Export Utilities + + +```typescript no_run +decodeImportBlobMeta(blob: Uint8Array, check_checksum: boolean): ImportBlobMetadata +``` + + +Decodes metadata from an import blob. + +**Parameters:** + +- `blob` - Binary data to decode +- `check_checksum` - Whether to verify checksum + +**Returns:** Import blob metadata + +**Example:** + +```ts threeslash +import { LoroDoc, decodeImportBlobMeta } from "loro-crdt"; + +const doc = new LoroDoc(); +const updates = doc.export({ mode: "update" }); +const meta = decodeImportBlobMeta(updates, true); +``` + + + + +```typescript no_run +redactJsonUpdates(json: string | JsonSchema, version_range: any): JsonSchema +``` + + +Redacts JSON updates within a specified version range. + +Use this to safely remove accidentally leaked sensitive content from history while preserving structure. See [Tips: Redaction](/docs/tutorial/tips). + +**Parameters:** + +- `json` - JSON updates to redact +- `version_range` - Version range for redaction + +**Returns:** Redacted JSON schema + +**Example:** + +```ts threeslash +import { LoroDoc, redactJsonUpdates } from "loro-crdt"; + +const doc = new LoroDoc(); +const json = doc.exportJsonUpdates(); +const redacted = redactJsonUpdates(json, { [doc.peerIdStr]: [0, 999999] }); +``` + + + +--- + +## Container Types + +Common CRDT containers for modeling JSON-like structures. See [Choosing CRDT Types](/docs/concepts/choose_crdt_type) and [Composing CRDTs](/docs/tutorial/composition) for when to use each and how to nest them. + +### LoroText + +A rich text container supporting collaborative text editing with formatting. Supports overlapping marks (bold, italic, links) and +stable cursors. The merge semantics avoid interleaving artifacts under concurrency (Fugue + Eg-walker ideas); you use simple index +APIs and Loro handles index transformation. +See [Text](/docs/tutorial/text), [Eg-walker](/docs/advanced/event_graph_walker), and the rich text blog: https://loro.dev/blog/loro-richtext + +**⚠️ Critical: UTF-16 String Encoding** + +LoroText uses **UTF-16** encoding, matching JavaScript's native string encoding: + +- All standard methods (`insert()`, `delete()`, `mark()`, `slice()`, `charAt()`) use UTF-16 code unit indices +- `length` returns UTF-16 code units (same as JavaScript `string.length`) +- Use `insertUtf8()` and `deleteUtf8()` for UTF-8 byte-based operations when integrating with UTF-8 systems + +**⚠️ Common Pitfalls:** + +1. **Index Misalignment**: UTF-16 indices differ from visual character count +2. **Performance**: Cursor queries on deleted positions require history traversal - in that case, it will return a refreshed Cursor object that does not point to the deleted text + +**Example with emoji:** + +```typescript no_run +const text = doc.getText("text"); +text.insert(0, "Hello πŸ˜€ World"); +console.log(text.length); // 13 (emoji counts as 2) +console.log(text.toString()[6]); // ⚠️ Invalid - splits the emoji +text.delete(6, 2); // βœ… Correct - deletes entire emoji +text.delete(6, 1); // ❌ Wrong - corrupts the emoji + +// Safe iteration +text.iter((char) => { + console.log(char); // Each character handled correctly + return true; +}); +``` + +**πŸ“ Text vs String in Maps:** + +- Use `LoroText` for collaborative text editing where all concurrent edits must be preserved +- Use regular strings in `LoroMap` for atomic values (URLs, IDs, hashes) where Last-Write-Wins is preferred +- Example: URLs should be strings in maps, not LoroText. Otherwise, the automatically merged result may be an invalid URL + + + ```typescript no_run insert(index: number, text: string): void ``` + + + +Inserts text at the specified position using UTF-16 code unit indices (same as JavaScript string indices). + +**Parameters:** + +- `index` - UTF-16 code unit position to insert at (0-based, same as JavaScript string index) +- `text` - Text to insert + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +// Usage example: +text.insert(0, "Hello "); +text.insert(6, "World"); +``` + + + + + ```typescript no_run delete(index: number, len: number): void ``` + + + +Deletes text from the specified position using UTF-16 code units. + +**Parameters:** + +- `index` - Starting UTF-16 code unit position (same as JavaScript string index) +- `len` - Number of UTF-16 code units to delete + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello πŸ˜€ World"); +text.delete(6, 2); // Delete emoji (2 UTF-16 units) +text.delete(5, 1); // Delete space before World +``` + + + + +```typescript no_run +mark(range: { start: number, end: number }, key: string, value: Value): void +``` + + + +Applies formatting to a text range. Marks can be configured to expand/stop at edges via configTextStyle(); see [Text](/docs/tutorial/text) for mark behavior. + +**Parameters:** + +- `range` - The range to format +- `key` - Style attribute name +- `value` - Style value + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); +doc.configTextStyle({ bold: { expand: "after" } }); +text.mark({ start: 0, end: 5 }, "bold", true); +``` + + + + +```typescript no_run +unmark(range: { start: number, end: number }, key: string): void +``` + + +Removes formatting from a text range. For how conflicting edits on marks resolve, see [Text](/docs/tutorial/text). + +**Parameters:** + +- `range` - The range to unformat +- `key` - Style attribute to remove + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); +text.mark({ start: 0, end: 5 }, "bold", true); +text.unmark({ start: 0, end: 5 }, "bold"); +``` + + + + +```typescript no_run +toDelta(): Delta[] +``` + + +Converts text to Delta format (Quill-compatible). + +**Returns:** Array of Delta operations + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); +text.mark({ start: 0, end: 5 }, "bold", true); +const delta = text.toDelta(); +// [{ insert: "Hello", attributes: { bold: true } }, { insert: " World" }] +``` + + + + +```typescript no_run +sliceDelta(start: number, end: number): Delta[] +``` + + +Returns a Quill-style Delta for a subsection of the text, using UTF-16 indices. Useful for copying a styled span. Use `sliceDeltaUtf8` if you need UTF-8 byte offsets instead. + +**Parameters:** + +- `start` - Start UTF-16 code unit index (inclusive) +- `end` - End UTF-16 code unit index (exclusive) + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +import { expect } from "expect"; +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); +expect(snippet).toStrictEqual([ + { insert: "ello", attributes: { bold: true } }, + { insert: " " }, + { insert: "Wo", attributes: { comment: "greeting" } }, +]); +``` + + + + +```typescript no_run +sliceDeltaUtf8(start: number, end: number): Delta[] +``` + + +Returns a Quill-style Delta for a subsection of the text using **UTF-8 byte offsets**. Choose this when your offsets come from UTF-8 encoded buffers. + +**Parameters:** + +- `start` - Start byte offset (inclusive) +- `end` - End byte offset (exclusive) + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +import { expect } from "expect"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hi πŸ‘‹"); + +const enc = new TextEncoder(); +const start = enc.encode("Hi ").length; // 3 bytes +const end = enc.encode("Hi πŸ‘‹").length; // 7 bytes + +const delta = text.sliceDeltaUtf8(start, end); +expect(delta).toStrictEqual([{ insert: "πŸ‘‹" }]); +``` + + + + +```typescript no_run +applyDelta(delta: Delta[]): void +``` + + +Applies Delta operations to the text. + +**Parameters:** + +- `delta` - Array of Delta operations + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.applyDelta([ + { insert: "Hello", attributes: { bold: true } }, + { insert: " World" }, +]); +``` + + + + +```typescript no_run +update(text: string, options?: { timeoutMs?: number; useRefinedDiff?: boolean }): void +``` + + +Updates the current text to the target text using Myers' diff algorithm. + +**Parameters:** + +- `text` - New text content +- `options` - Update options + - `timeoutMs` - Optional timeout for the diff computation + - `useRefinedDiff` - Use refined diff for better quality on long texts + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +text.insert(0, "Hello"); +text.update("Hello World", { timeoutMs: 100 }); +``` + + + + +```typescript no_run +updateByLine(text: string, options?: { timeoutMs?: number; useRefinedDiff?: boolean }): void +``` + + +Line-based update that's faster for large texts (less precise than `update`). + +```typescript no_run +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +text.insert(0, "Line A\nLine C"); +text.updateByLine("Line A\nLine B\nLine C"); +``` + + + + +```typescript no_run +getCursor(pos: number, side?: Side): Cursor | undefined +``` + + +Gets a stable cursor position that survives edits. + +**Parameters:** + +- `pos` - Position in the text +- `side` - Cursor affinity (-1, 0, or 1) + +**Returns:** Cursor object or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); + +const cursor = text.getCursor(5); +// Cursor remains valid even after edits +``` + + + + +```typescript no_run +toString(): string +``` + + +Converts to plain text string. + +**Returns:** Plain text content + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); +const plainText = text.toString(); +``` + + + + +```typescript no_run +charAt(pos: number): string +``` + + +Gets the character at a specific UTF-16 code unit position. + +**Parameters:** + +- `pos` - UTF-16 code unit position + +**Returns:** Character at position + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); +const char = text.charAt(1); // "e" +``` + + + + +```typescript no_run +slice(start: number, end: number): string +``` + + +Extracts a section of the text using UTF-16 code unit positions. + +**Parameters:** + +- `start` - Start UTF-16 code unit index +- `end` - End UTF-16 code unit index (exclusive) + +**Returns:** Sliced text + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello πŸ˜€ World"); + +const slice1 = text.slice(0, 5); // "Hello" +const slice2 = text.slice(6, 8); // "πŸ˜€" (emoji spans 6-8) +const slice3 = text.slice(9, 14); // "World" +``` + + + + +```typescript no_run +splice(pos: number, len: number, s: string): string +``` + + +Replaces text at a position with new content. + +**Parameters:** + +- `pos` - Start position +- `len` - Length to delete +- `s` - String to insert + +**Returns:** Deleted text + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); + +// Usage example: +const deleted = text.splice(6, 5, "Loro"); // returns "World" +``` + + + + +```typescript no_run +push(s: string): void +``` + + +Appends text to the end of the document. + +**Parameters:** + +- `s` - String to append + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +text.push("Hello"); +text.push(" World"); +``` + + + + +```typescript no_run +iter(callback: (char: string) => boolean): void +``` + + +Iterates over each character in the text. + +**Parameters:** + +- `callback` - Function called for each character. Return false to stop iteration. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); + +// Usage example: +text.iter((char) => { + console.log(char); + return true; // continue iteration +}); +``` + + + + +```typescript no_run +insertUtf8(index: number, content: string): void +``` + + +Inserts text at a UTF-8 byte index position. + +**Parameters:** + +- `index` - UTF-8 byte index +- `content` - Text to insert + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +text.insertUtf8(0, "Hello"); +``` + + + + +```typescript no_run +deleteUtf8(index: number, len: number): void +``` + + +Deletes text at a UTF-8 byte index position. + +**Parameters:** + +- `index` - UTF-8 byte index +- `len` - Number of UTF-8 bytes to delete + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); + +// Usage example: +text.deleteUtf8(6, 5); // Delete "World" +``` + + + + +```typescript no_run +getEditorOf(pos: number): PeerID | undefined +``` + + +Gets the peer ID of who last edited the character at a position. + +**Parameters:** + +- `pos` - Character position + +**Returns:** PeerID of last editor or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); + +// Usage example: +const editor = text.getEditorOf(0); +``` + + + + +```typescript no_run +kind(): "Text" +``` + + +Returns the container type. + +**Returns:** "Text" + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +const type = text.kind(); // "Text" +``` + + + + +```typescript no_run +parent(): Container | undefined +``` + + +Gets the parent container if this text is nested. + +**Returns:** Parent container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +const text = list.insertContainer(0, doc.getText("nested")); + +// Usage example: +const parent = text.parent(); // Returns the list +``` + + + + +```typescript no_run +isAttached(): boolean +``` + + +Checks if the container is attached to a document. + +**Returns:** True if attached + +**Example:** + +```ts threeslash +import { LoroDoc, LoroText } from "loro-crdt"; +const doc = new LoroDoc(); +const text = new LoroText(); + +const attached = text.isAttached(); // false until attached to doc +``` + + + + +```typescript no_run +getAttached(): LoroText | undefined +``` + + +Gets the attached version of this container. + +**Returns:** Attached container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc, LoroText } from "loro-crdt"; +const doc = new LoroDoc(); +const text = new LoroText(); + +const attached = text.getAttached(); +``` + + + + +```typescript no_run +isDeleted(): boolean +``` + + +Checks if the container has been deleted. + +**Returns:** True if deleted + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +const deleted = text.isDeleted(); +``` + + + + +```typescript no_run +getShallowValue(): string +``` + + +Gets the text content without marks. + +**Returns:** Plain text string + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); + +// Usage example: +const value = text.getShallowValue(); // "Hello" +``` + + + + +```typescript no_run +toJSON(): any +``` + + +Converts the text to JSON representation. + +**Returns:** JSON value + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); + +// Usage example: +const json = text.toJSON(); // "Hello" +``` + + + + +```typescript no_run +readonly id: ContainerID +``` + + +Gets the unique container ID. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +const containerId = text.id; +``` + + + + +```typescript no_run +readonly length: number +``` + + +Gets the length of the text in UTF-16 code units (same as JavaScript's `string.length`). + +**⚠️ Important:** Emoji and other characters outside the Basic Multilingual Plane count as 2 UTF-16 units. This affects all index-based operations: + +```typescript no_run +text.insert(0, "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦"); // Family emoji +console.log(text.length); // 11 (not 1!) - complex emoji with ZWJ sequences +``` + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello"); +console.log(text.length); // 5 + +text.insert(5, " πŸ˜€"); +console.log(text.length); // 8 (space + emoji which counts as 2) +``` + + + +### LoroList + +An ordered list container for collaborative arrays. Uses index-based APIs; under concurrency, Loro transforms indices +by replaying only the necessary portion of history (Eg-walker-inspired). See [List and Movable List](/docs/tutorial/list), +[Choosing CRDT Types](/docs/concepts/choose_crdt_type), and [Eg-walker](/docs/advanced/event_graph_walker). + +**⚠️ Important: List vs Map for Coordinates** + +```typescript no_run +// ❌ WRONG - Don't use List for coordinates +const coord = doc.getList("coord"); +coord.push(10); // x +coord.push(20); // y +// Concurrent updates can create [10, 20a, 20b] instead of [10, 20] + +// βœ… CORRECT - Use Map for coordinates +const coord = doc.getMap("coord"); +coord.set("x", 10); +coord.set("y", 20); +// Concurrent updates properly merge to {x: 10, y: 20} +``` + + +```typescript no_run +insert(pos: number, value: Value | Container): void +``` + + +Inserts a value at the specified position. + +**Parameters:** + +- `pos` - Insert position +- `value` - Value to insert + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.insert(0, "First"); +list.insert(1, { type: "object" }); +``` + + + + +```typescript no_run +insertContainer(pos: number, container: T): T +``` + + +Inserts a new container at the position. + +**Parameters:** + +- `pos` - Insert position +- `container` - Container instance + +**Returns:** The inserted container + +**Example:** + +```ts threeslash +import { LoroDoc, LoroText } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +const subText = list.insertContainer(0, new LoroText()); +subText.insert(0, "Nested text"); +``` + + + + +```typescript no_run +delete(pos: number, len: number): void +``` + + +Deletes elements from the list. + +**Parameters:** + +- `pos` - Starting position +- `len` - Number of elements to delete + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push("a"); +list.push("b"); +list.push("c"); +list.push("d"); + +list.delete(1, 2); // Delete 2 elements starting at index 1 +``` + + + + +```typescript no_run +push(value: Value | Container): void +``` + + +Appends a value to the end of the list. + +**Parameters:** + +- `value` - Value to append + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push("Last item"); +``` + + + + +```typescript no_run +getIdAt(pos: number): { peer: PeerID, counter: number } | undefined +``` + + +```typescript no_run +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); + +list.insert(0, 1); +const id0 = list.getIdAt(0); + +```` + + + +```typescript no_run +pushContainer(container: T): T +```` + + + +Appends a container to the end of the list. + +**Parameters:** + +- `container` - Container to append + +**Returns:** The appended container + +**Example:** + +```ts threeslash +import { LoroDoc, LoroMap } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +const map = list.pushContainer(new LoroMap()); +map.set("key", "value"); +``` + + + + +```typescript no_run +pop(): Value | Container | undefined +``` + + +Removes and returns the last element. + +**Returns:** The removed element or undefined + +**Example:** + +```ts no_run threeslash +import { LoroList } from "loro-crdt"; +declare const list: LoroList; +// ---cut--- +const lastItem = list.pop(); +``` + + + + +```typescript no_run +get(index: number): Value | Container | undefined +``` + + +Gets the value at the specified index. + +**Parameters:** + +- `index` - Element index + +**Returns:** The value or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const list = doc.getList("items"); +list.push("first", "second"); + +const item = list.get(0); // "first" +``` + + + + +```typescript no_run +getCursor(pos: number, side?: Side): Cursor | undefined +``` + + +Gets a stable cursor for the position. + +**Parameters:** + +- `pos` - Position in the list +- `side` - Cursor affinity + +**Returns:** Cursor object or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push("a", "b", "c"); + +const cursor = list.getCursor(2); +``` + + + + +```typescript no_run +toArray(): (Value | Container)[] +``` + + +Converts the list to a JavaScript array. + +**Returns:** Array of values + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push("a", "b", "c"); +const array = list.toArray(); +``` + + + + +```typescript no_run +clear(): void +``` + + +Removes all elements from the list. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push("a", "b", "c"); +list.clear(); +``` + + + + +```typescript no_run +length: number +``` + + +Gets the number of elements in the list. + +**Returns:** List length + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push("a"); +list.push("b"); +list.push("c"); +console.log(`List has ${list.length} items`); +``` + + + + +```typescript no_run +kind(): "List" +``` + + +Returns the container type. + +**Returns:** "List" + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); + +const type = list.kind(); // "List" +``` + + + + +```typescript no_run +toJSON(): any +``` + + +Converts the list to JSON representation. + +**Returns:** JSON array + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push(1, 2, 3); + +// Usage example: +const json = list.toJSON(); // [1, 2, 3] +``` + + + + +```typescript no_run +parent(): Container | undefined +``` + + +Gets the parent container if this list is nested. + +**Returns:** Parent container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); +const list = map.setContainer("nested", doc.getList("list")); + +// Usage example: +const parent = list.parent(); // Returns the map +``` + + + + +```typescript no_run +isAttached(): boolean +``` + + +Checks if the container is attached to a document. + +**Returns:** True if attached + +**Example:** + +```ts threeslash +import { LoroDoc, LoroList } from "loro-crdt"; +const doc = new LoroDoc(); +const list = new LoroList(); + +const attached = list.isAttached(); // false until attached to doc +``` + + + + +```typescript no_run +getAttached(): LoroList | undefined +``` + + +Gets the attached version of this container. + +**Returns:** Attached container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc, LoroList } from "loro-crdt"; +const doc = new LoroDoc(); +const list = new LoroList(); + +const attached = list.getAttached(); +``` + + + + +```typescript no_run +isDeleted(): boolean +``` + + +Checks if the container has been deleted. + +**Returns:** True if deleted + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); + +const deleted = list.isDeleted(); +``` + + + + +```typescript no_run +getShallowValue(): Value[] +``` + + +Gets the list values with sub-containers as IDs. + +**Returns:** Array of values + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +list.push(1, 2); + +// Usage example: +const values = list.getShallowValue(); // [1, 2] +``` + + + + +```typescript no_run +readonly id: ContainerID +``` + + +Gets the unique container ID. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); + +const containerId = list.id; +``` + + + +### LoroMap + +A key-value map container for collaborative objects. See [Map](/docs/tutorial/map). + + +```typescript no_run +set(key: string, value: Value | Container): void +``` + + +Sets a key-value pair. + +Note: Setting a key to the same value is a no-op (no operation recorded). See [Map basics](/docs/tutorial/map). + +**Parameters:** + +- `key` - The key +- `value` - The value + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +map.set("name", "Alice"); +map.set("age", 30); +``` + + + + +```typescript no_run +setContainer(key: string, container: T): T +``` + + +Sets a container as the value for a key. + +**⚠️ Pitfall:** Concurrent regular child container creation at the same key on a LoroMap from multiple peers causes overwrites (different container IDs cannot auto-merge). If the child should be shared by logical key, use the `ensureMergeable*` methods instead. See [Container initialization](/docs/tutorial/tips). + +**Parameters:** + +- `key` - The key +- `container` - Container instance + +**Returns:** The set container + +**Example:** + +```ts threeslash +import { LoroDoc, LoroList } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const list = map.setContainer("items", new LoroList()); +list.push("item1"); +``` + + + + +```typescript no_run +ensureMergeableText(key: string): LoroText +``` + + +Ensures that `key` points to a mergeable Text child container. Peers that call this method concurrently with the same parent Map and key will address the same logical Text container instead of creating competing child container IDs. + +Use this for lazily-created text fields that should behave like one shared document for all peers. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Text container + +**Example:** + +```ts no_run +const body = doc.getMap("note").ensureMergeableText("body"); +body.insert(0, "Hello"); +``` + + + + +```typescript no_run +ensureMergeableMap(key: string): LoroMap +``` + + +Ensures that `key` points to a mergeable Map child container. Use this when a nested record, profile, settings object, or migration-created child Map may be initialized by multiple peers. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Map container + +**Example:** + +```ts no_run +const record = doc.getMap("records").ensureMergeableMap(recordId); +record.set("updated", true); +``` + + + + +```typescript no_run +ensureMergeableList(key: string): LoroList +``` + + +Ensures that `key` points to a mergeable List child container. Use this for dynamic child lists, such as one list per day, where concurrent first creation should produce one shared List. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable List container + +**Example:** + +```ts no_run +const entries = doc.getMap("days").ensureMergeableList("2026-06-08"); +entries.insert(0, "meeting notes"); +``` + + + + +```typescript no_run +ensureMergeableMovableList(key: string): LoroMovableList +``` + + +Ensures that `key` points to a mergeable MovableList child container. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable MovableList container + + + + +```typescript no_run +ensureMergeableTree(key: string): LoroTree +``` + + +Ensures that `key` points to a mergeable Tree child container. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Tree container + + + + +```typescript no_run +ensureMergeableCounter(key: string): LoroCounter +``` + + +Ensures that `key` points to a mergeable Counter child container. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Counter container + + + + +```typescript no_run +get(key: string): Value | Container | undefined +``` + + +Gets the value for a key. + +**Parameters:** + +- `key` - The key + +**Returns:** The value or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const name = map.get("name"); +``` + + + + +```typescript no_run +getOrCreateContainer(key: string, container: T): T +``` + + +**Deprecated:** Prefer the `ensureMergeable*` methods for Map child containers that may be created lazily or concurrently by multiple peers. + +Gets an existing container or creates a new regular child container. + +**⚠️ Pitfall:** Parallel container creation for the same key across peers creates different child container IDs and can cause overwrites. This method is kept for compatibility and for cases where you explicitly want a regular child container. For shared dynamic children, use `ensureMergeableText`, `ensureMergeableMap`, `ensureMergeableList`, `ensureMergeableMovableList`, `ensureMergeableTree`, or `ensureMergeableCounter`. See [Container initialization](/docs/tutorial/tips). + +**Parameters:** + +- `key` - The key +- `container` - Container to create if not exists + +**Returns:** The container + +**Example:** + +```ts threeslash +import { LoroDoc, LoroText } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const text = map.getOrCreateContainer("description", new LoroText()); +``` + + + + +```typescript no_run +delete(key: string): void +``` + + +Removes a key-value pair. + +**Parameters:** + +- `key` - The key to remove + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +map.delete("obsoleteKey"); +``` + + + + +```typescript no_run +clear(): void +``` + + +Removes all key-value pairs. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +map.clear(); +``` + + + + +```typescript no_run +keys(): string[] +``` + + +Gets all keys in the map. + +**Returns:** Array of keys + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const allKeys = map.keys(); +``` + + + + +```typescript no_run +values(): (Value | Container)[] +``` + + +Gets all values in the map. + +**Returns:** Array of values + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const allValues = map.values(); +``` + + + + +```typescript no_run +entries(): [string, Value | Container][] +``` + + +Gets all key-value pairs. + +**Returns:** Array of entries + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +for (const [key, value] of map.entries()) { + console.log(`${key}: ${value}`); +} +``` + + + + +```typescript no_run +getLastEditor(key: string): PeerID | undefined +``` + + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +map.set("k", 1); +doc.commit(); +const who = map.getLastEditor("k"); +// who = doc.peerIdStr + +```` + + + +```typescript no_run +size: number +```` + + + +Gets the number of key-value pairs. + +**Returns:** Map size + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +console.log(`Map has ${map.size} entries`); +``` + + + + +```typescript no_run +kind(): "Map" +``` + + +Returns the container type. + +**Returns:** "Map" + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const type = map.kind(); // "Map" +``` + + + + +```typescript no_run +toJSON(): any +``` + + +Converts the map to JSON representation. + +**Returns:** JSON object + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); +map.set("name", "Alice"); + +// Usage example: +const json = map.toJSON(); // { name: "Alice" } +``` + + + + +```typescript no_run +parent(): Container | undefined +``` + + +Gets the parent container if this map is nested. + +**Returns:** Parent container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); +const map = list.insertContainer(0, doc.getMap("nested")); + +// Usage example: +const parent = map.parent(); // Returns the list +``` + + + + +```typescript no_run +isAttached(): boolean +``` + + +Checks if the container is attached to a document. + +**Returns:** True if attached + +**Example:** + +```ts threeslash +import { LoroDoc, LoroMap } from "loro-crdt"; +const doc = new LoroDoc(); +const map = new LoroMap(); + +const attached = map.isAttached(); // false until attached to doc +``` + + + + +```typescript no_run +getAttached(): LoroMap | undefined +``` + + +Gets the attached version of this container. + +**Returns:** Attached container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc, LoroMap } from "loro-crdt"; +const doc = new LoroDoc(); +const map = new LoroMap(); + +const attached = map.getAttached(); +``` + + + + +```typescript no_run +isDeleted(): boolean +``` + + +Checks if the container has been deleted. + +**Returns:** True if deleted + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const deleted = map.isDeleted(); +``` + + + + +```typescript no_run +getShallowValue(): Record +``` + + +Gets the map values with sub-containers as IDs. + +**Returns:** Object with values + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); +map.set("key", "value"); + +// Usage example: +const values = map.getShallowValue(); // { key: "value" } +``` + + + + +```typescript no_run +readonly id: ContainerID +``` + + +Gets the unique container ID. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +const containerId = map.id; +``` + + + +### LoroTree + +A hierarchical tree container for nested structures. Supports moving subtrees while handling concurrent edits. See [Tree](/docs/tutorial/tree). + +**⚠️ Important Tree Operation Notes:** + +- **Concurrent moves can create cycles**: Loro detects and prevents these automatically +- **Fractional indexing**: Has interleaving issues but maintains relative ordering +- **Don't disable fractional index** if you need siblings to be sorted. See [Tree](/docs/tutorial/tree). + + +```typescript no_run +createNode(parent?: TreeID, index?: number): LoroTreeNode +``` + + +Creates a new tree node. + +**Parameters:** + +- `parent` - Parent node ID (optional, creates root if omitted) +- `index` - Position among siblings (optional) + +**Returns:** The new node's handler + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const root = tree.createNode(); +const child = root.createNode(0); +``` + + + + +```typescript no_run +move(target: TreeID, parent?: TreeID, index?: number): void +``` + + +Moves a node to a new position. + +**Parameters:** + +- `target` - Node to move +- `parent` - New parent (undefined for root) +- `index` - Position among siblings + +**Example:** + +```typescript no_run threeslash +import { LoroDoc, TreeID } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +declare const nodeId: TreeID; +declare const newParentId: TreeID; + +// Usage example: +tree.move(nodeId, newParentId, 0); +``` + + + + +```typescript no_run +delete(target: TreeID): void +``` + + +Deletes a node and its descendants. + +**Parameters:** + +- `target` - Node to delete + +**Example:** + +```ts threeslash +import { LoroDoc, TreeID } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); +const nodeId = node.id; + +// Usage example: +tree.delete(nodeId); +``` + + + + +```typescript no_run +getNodeByID(id: TreeID): LoroTreeNode | undefined +``` + + +Gets a node handler by its ID. + +**Parameters:** + +- `id` - Node ID + +**Returns:** Node handler or undefined + +**Example:** + +```ts threeslash +import { LoroDoc, TreeID } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const _node = tree.createNode(); +const nodeId = _node.id; + +// Usage example: +const node = tree.getNodeByID(nodeId); +if (node) { + node.data.set("label", "New Label"); +} +``` + + + + +```typescript no_run +nodes(): LoroTreeNode[] +``` + + +Gets all nodes in the tree. + +**Returns:** Array of all nodes + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const allNodes = tree.nodes(); +``` + + + + +```typescript no_run +roots(): LoroTreeNode[] +``` + + +Gets all root nodes. + +**Returns:** Array of root nodes + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const rootNodes = tree.roots(); +``` + + + + +```typescript no_run +has(target: TreeID): boolean +``` + + +Checks if a node exists. + +**Parameters:** + +- `target` - Node ID to check + +**Returns:** Boolean indicating existence + +**Example:** + +```typescript no_run threeslash +import { LoroDoc, TreeID } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +declare const nodeId: TreeID; + +// Usage example: +if (tree.has(nodeId)) { + console.log("Node exists"); +} +``` + + + + +```typescript no_run +isNodeDeleted(target: TreeID): boolean +``` + + +Checks if a node has been deleted. + +**Parameters:** + +- `target` - Node ID to check + +**Returns:** Boolean indicating deletion status + +**Example:** + +```typescript no_run threeslash +import { LoroDoc, TreeID } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +declare const nodeId: TreeID; + +// Usage example: +if (tree.isNodeDeleted(nodeId)) { + console.log("Node was deleted"); +} +``` + + + +```typescript no_run +enableFractionalIndex(jitter: number): void +``` + + +Enables fractional indexing for better concurrent move performance. + +**Parameters:** + +- `jitter` - Jitter amount for fractional indices + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +tree.enableFractionalIndex(0.001); +``` + + + + +```typescript no_run +disableFractionalIndex(): void +``` + + +Disables fractional indexing. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +tree.disableFractionalIndex(); +``` + + + + +```typescript no_run +isFractionalIndexEnabled(): boolean +``` + + +Checks if fractional indexing is enabled. + +**Returns:** True if enabled + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const enabled = tree.isFractionalIndexEnabled(); +``` + + + + +```typescript no_run +kind(): "Tree" +``` + + +Returns the container type. + +**Returns:** "Tree" + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const type = tree.kind(); // "Tree" +``` + + + + +```typescript no_run +toJSON(): any +``` + + +Converts the tree to JSON representation. + +**Returns:** JSON tree structure + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const json = tree.toJSON(); +``` + + + + +```typescript no_run +parent(): Container | undefined +``` + + +Gets the parent container if this tree is nested. + +**Returns:** Parent container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); +const tree = map.setContainer("tree", doc.getTree("nested")); + +// Usage example: +const parent = tree.parent(); +``` + + + + +```typescript no_run +isAttached(): boolean +``` + + +Checks if the container is attached to a document. + +**Returns:** True if attached + +**Example:** + +```ts threeslash +import { LoroDoc, LoroTree } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = new LoroTree(); + +const attached = tree.isAttached(); +``` + + + + +```typescript no_run +getAttached(): LoroTree | undefined +``` + + +Gets the attached version of this container. + +**Returns:** Attached container or undefined + +**Example:** + +```ts threeslash +import { LoroDoc, LoroTree } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = new LoroTree(); + +const attached = tree.getAttached(); +``` + + + + +```typescript no_run +isDeleted(): boolean +``` + + +Checks if the container has been deleted. + +**Returns:** True if deleted + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const deleted = tree.isDeleted(); +``` + + + + +```typescript no_run +getShallowValue(): TreeNodeShallowValue[] +``` + + +Gets the tree values with sub-containers as IDs. + +**Returns:** Array of tree node values + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const values = tree.getShallowValue(); +``` + + + + +```typescript no_run +readonly id: ContainerID +``` + + +Gets the unique container ID. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +const containerId = tree.id; +``` + + + +### LoroTreeNode + +Represents a single node in the tree. + + +```typescript no_run +data: LoroMap +``` + + +A map container for storing node metadata. + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); +node.data.set("title", "Node Title"); +node.data.set("expanded", true); +``` + + + + +```typescript no_run +createNode(index?: number): LoroTreeNode +``` + + +Creates a child node. + +**Parameters:** + +- `index` - Position among siblings + +**Returns:** New node's handler + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); +const childId = node.createNode(0); +``` + + + + +```typescript no_run +move(parent?: LoroTreeNode, index?: number): void +``` + + +Moves this node to a new parent. + +**Parameters:** + +- `parent` - New parent node +- `index` - Position among siblings + +**Example:** + +```ts threeslash +import { LoroDoc, LoroTreeNode } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); +const parent = tree.createNode(); + +// Usage example: +node.move(parent, 0); +``` + + + + +```typescript no_run +moveAfter(target: LoroTreeNode): void +``` + + +Moves this node after a sibling. + +**Parameters:** + +- `target` - Sibling node + +**Example:** + +```ts threeslash +import { LoroDoc, LoroTreeNode } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); +const sibling = tree.createNode(); + +// Usage example: +node.moveAfter(sibling); +``` + + + + +```typescript no_run +moveBefore(target: LoroTreeNode): void +``` + + +Moves this node before a sibling. + +**Parameters:** + +- `target` - Sibling node + +**Example:** + +```ts threeslash +import { LoroDoc, LoroTreeNode } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); +const sibling = tree.createNode(); + +// Usage example: +node.moveBefore(sibling); +``` + + + + +```typescript no_run +parent(): LoroTreeNode | undefined +``` + + +Gets the parent node. + +**Returns:** Parent node or undefined + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +// Usage example: +const parentNode = node.parent(); +``` + + + + +```typescript no_run +children(): LoroTreeNode[] +``` + + +Gets all child nodes. + +**Returns:** Array of children + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +// Usage example: +const childNodes = node.children(); +``` + + + + +```typescript no_run +index(): number | undefined +``` + + +Gets the position among siblings. + +**Returns:** Index or undefined if root + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +// Usage example: +const position = node.index(); +``` + + + + +```typescript no_run +fractionalIndex(): string | undefined +``` + + +Returns the node's fractional index used to sort siblings deterministically. +It is a hex string representation of the Fractional Index and is stable for ordering. +Returns `undefined` for the root node. Note: the tree must be attached to the document. + +**Returns:** Hex string or `undefined` for root + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const parent = tree.createNode(); +const a = parent.createNode(0); +const b = parent.createNode(1); + +const aFi = a.fractionalIndex(); +const bFi = b.fractionalIndex(); +// aFi < bFi, because b is inserted after a +``` + + + + +```typescript no_run +creationId(): { peer: PeerID, counter: number } +``` + + +Returns the OpID that created this node. + +**Returns:** `{ peer: PeerID, counter: number }` creation identifier + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +const { peer, counter } = node.creationId(); +``` + + + + +```typescript no_run +creator(): PeerID +``` + + +Returns the peer ID that created this node (equivalent to `creationId().peer`). + +**Returns:** `PeerID` + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +const author = node.creator(); +// author == doc.peerIdStr +``` + + + + +```typescript no_run +getLastMoveId(): { peer: PeerID, counter: number } | undefined +``` + + +Returns the OpID of the most recent move operation for this node, or `undefined` if the node has never been moved. + +**Returns:** Creation/move OpID or `undefined` + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +const lastMove = node.getLastMoveId(); +``` + + + + +```typescript no_run +isDeleted(): boolean +``` + + +Checks if this node has been deleted. + +**Returns:** Boolean deletion status + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); +const node = tree.createNode(); + +// Usage example: +if (node.isDeleted()) { + console.log("Node is deleted"); +} +``` + + + +### LoroCounter + +A counter CRDT for collaborative numeric values. + + +```typescript no_run +increment(value: number): void +``` + + +Increments the counter. + +**Parameters:** + +- `value` - Amount to increment (default: 1) + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const counter = doc.getCounter("counter"); + +counter.increment(5); // +5 +``` + + + + +```typescript no_run +decrement(value: number): void +``` + + +Decrements the counter. + +**Parameters:** + +- `value` - Amount to decrement (default: 1) + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const counter = doc.getCounter("counter"); + +counter.decrement(3); // -3 +``` + + + + +```typescript no_run +value: number +``` + + +Gets the current counter value. + +**Returns:** Current numeric value + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const counter = doc.getCounter("counter"); + +console.log(`Counter value: ${counter.value}`); +``` + + + +### LoroMovableList + +A list optimized for move operations. Designed for frequent reordering (drag-and-drop) with good behavior under +concurrent moves (concurrent moves resolve to one final position). See [List and Movable List](/docs/tutorial/list). + +**πŸ“ MovableList vs List:** + +- **Use MovableList** for: Drag-and-drop UIs, sortable lists, kanban boards +- **Use List** for: scenarios where the list items don't need to be moved +- **Key difference**: MovableList handles concurrent moves better (no duplicates) and supports set operations, List is more efficient in general. + + +```typescript no_run +move(from: number, to: number): void +``` + + +Moves an element from one position to another. + +**Parameters:** + +- `from` - Source index +- `to` - Target index + +**Example:** + +```typescript +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const movableList = doc.getMovableList("list"); +movableList.push("a"); +movableList.push("b"); +movableList.push("c"); +movableList.push("d"); +movableList.push("e"); + +movableList.move(0, 3); // Move first element to fourth position +``` + + + + +```typescript no_run +set(pos: number, value: Value | Container): void +``` + + +Replaces the value at a position. + +**Parameters:** + +- `pos` - Position to update +- `value` - New value + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const movableList = doc.getMovableList("list"); +movableList.push("a", "b", "c"); + +movableList.set(0, "Updated value"); +``` + + + + +```typescript no_run +setContainer(pos: number, container: T): T +``` + + +Replaces the value with a container. + +**Parameters:** + +- `pos` - Position to update +- `container` - New container + +**Returns:** The set container + +**Example:** + +```ts threeslash +import { LoroDoc, LoroText } from "loro-crdt"; +const doc = new LoroDoc(); +const movableList = doc.getMovableList("list"); +movableList.push("placeholder"); + +const text = movableList.setContainer(0, new LoroText()); +``` + + + +## Synchronization + +Import/export updates over any transport and choose the right encoding for speed and size. See [Sync Tutorial](/docs/tutorial/sync) and [Encoding & Export Modes](/docs/tutorial/encoding). + +- **Import order**: Loro handles out-of-order updates automatically +- **Auto-commit**: Import and export operations trigger automatic commits + +### Import/Export Patterns + +#### Basic Synchronization + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +// Peer A: Export updates +const doc1 = new LoroDoc(); +doc1.getText("text").insert(0, "Hello"); +const updates = doc1.export({ mode: "update" }); + +// Peer B: Import updates +const doc2 = new LoroDoc(); +doc2.import(updates); +// now doc2.getText("text").toString() === "Hello" +``` + +#### Continuous Sync + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc1 = new LoroDoc(); +const doc2 = new LoroDoc(); + +// Usage example: +// Set up bidirectional sync +doc1.subscribeLocalUpdates((updates) => { + doc2.import(updates); +}); + +doc2.subscribeLocalUpdates((updates) => { + doc1.import(updates); +}); +``` + +Performance tips: + +- Prefer `mode: "update"` with a `VersionVector` to sync incrementally. +- Use `mode: "shallow-snapshot"` when you only need current state; it strips history for faster import/load. +- Loro’s LSM-based encoding and Eg-walker-inspired merge keep import/export fast, even for large histories. + See [Encoding](/docs/tutorial/encoding) and v1.0 performance notes: https://loro.dev/blog/v1.0 + +#### Network Sync with WebSocket + +```ts no_run threeslash +import { LoroDoc } from "loro-crdt"; + +// Assume we have: +declare const ws: { + send: (data: Uint8Array) => void; + on: (event: string, handler: (data: any) => void) => void; +}; + +// Client side +const doc = new LoroDoc(); + +// Send local updates to server +doc.subscribeLocalUpdates((updates) => { + ws.send(updates); +}); + +// Receive updates from server +ws.on("message", (data) => { + doc.import(new Uint8Array(data)); +}); +``` + +### Shallow Snapshots + +Shallow snapshots allow for efficient storage by garbage collecting deleted operations. + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +// Create shallow snapshot +const frontiers = doc.frontiers(); +const shallowSnapshot = doc.export({ + mode: "shallow-snapshot", + frontiers: frontiers, +}); + +// Import shallow snapshot +const newDoc = new LoroDoc(); +newDoc.import(shallowSnapshot); +``` + +--- + +## Version Control + +### Time Travel + +Navigate through document history: + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +// Save current version +const v1 = doc.frontiers(); + +// Make changes +doc.getText("text").insert(0, "New text"); + +// Save new version +const v2 = doc.frontiers(); + +// Travel back +doc.checkout(v1); +console.log(doc.getText("text").toString()); // Original text + +// Travel forward +doc.checkout(v2); +console.log(doc.getText("text").toString()); // New text + +// Return to latest +doc.checkoutToLatest(); +``` + +### Forking + +Create document branches: + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +// Fork at current state +const fork1 = doc.fork(); +fork1.getText("text").insert(0, "Fork 1 changes"); + +// Fork at specific version +const historicalVersion = doc.frontiers(); +const fork2 = doc.forkAt(historicalVersion); +fork2.getText("text").insert(0, "Fork 2 changes"); + +// Original document remains unchanged +``` + +### Version Vectors + +Track document versions: + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const vv = doc.version(); +console.log(`Document has ${vv.length()} peers`); +``` + +--- + +## Events & Subscriptions + +### Event Structure + +```typescript no_run +interface LoroEventBatch { + by: "local" | "import" | "checkout"; + origin?: string; + currentTarget?: ContainerID; + events: LoroEvent[]; + from: Frontiers; + to: Frontiers; +} + +interface LoroEvent { + target: ContainerID; + diff: Diff; + path: Path; +} +``` + +### Diff Types + +Different containers produce different diff types: + +#### TextDiff + + +```typescript no_run +type TextDiff = { + type: "text" + diff: Delta[] +} +``` +Represents changes to text content using Delta format. + +**Properties:** + +- `type` - Always "text" for text diffs +- `diff` - Array of Delta operations (insert, delete, retain) + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); + +// Example +text.subscribe((e) => { + for (const event of e.events) { + if (event.diff.type === "text") { + event.diff.diff.forEach((delta) => { + if (delta.insert) { + console.log(`Inserted: "${delta.insert}"`); + } + if (delta.delete) { + console.log(`Deleted ${delta.delete} characters`); + } + }); + } + } +}); +``` + + + +#### ListDiff + + +```typescript no_run +type ListDiff = { + type: "list" + diff: Delta<(Value | Container)[]>[] +} +``` +Represents changes to list content using Delta format. + +**Properties:** + +- `type` - Always "list" for list diffs +- `diff` - Array of Delta operations on list items + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const list = doc.getList("list"); + +// Example +list.subscribe((e) => { + for (const event of e.events) { + if (event.diff.type === "list") { + event.diff.diff.forEach((delta) => { + if (delta.insert) { + console.log(`Inserted items:`, delta.insert); + } + }); + } + } +}); +``` + + + +#### MapDiff + + +```typescript no_run +type MapDiff = { + type: "map" + updated: Record +} +``` +Represents changes to map content. + +**Properties:** + +- `type` - Always "map" for map diffs +- `updated` - Record of key-value changes (undefined means deleted) + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const map = doc.getMap("map"); + +// Example +map.subscribe((e) => { + for (const event of e.events) { + if (event.diff.type === "map") { + Object.entries(event.diff.updated).forEach(([key, value]) => { + if (value === undefined) { + console.log(`Deleted key: ${key}`); + } else { + console.log(`Updated key: ${key} = ${value}`); + } + }); + } + } +}); +``` + + + +#### TreeDiff + + +```typescript no_run +type TreeDiff = { + type: "tree" + diff: TreeDiffItem[] +} + +type TreeDiffItem = +| { +target: TreeID +action: "create" +parent: TreeID | undefined +index: number +fractionalIndex: string +} +| { +target: TreeID +action: "delete" +oldParent: TreeID | undefined +oldIndex: number +} +| { +target: TreeID +action: "move" +parent: TreeID | undefined +index: number +fractionalIndex: string +oldParent: TreeID | undefined +oldIndex: number +} + +```` +Represents changes to tree structure. + +**Properties:** +- `type` - Always "tree" for tree diffs +- `diff` - Array of TreeDiffItem operations (create, delete, move) + +**TreeDiffItem Actions:** +- `create` - Node creation with parent and position +- `delete` - Node deletion with old parent and position +- `move` - Node movement with old and new positions + +**Example:** +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const tree = doc.getTree("tree"); + +// Example +tree.subscribe((e) => { + for (const event of e.events) { + if (event.diff.type === "tree") { + event.diff.diff.forEach(item => { + switch (item.action) { + case "create": + console.log(`Created node ${item.target}`); + break; + case "move": + console.log(`Moved node ${item.target}`); + break; + case "delete": + console.log(`Deleted node ${item.target}`); + break; + } + }); + } + } +}); +```` + + + +### Deep Subscription + +Subscribe to nested changes: + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +// Subscribe to specific container +const text = doc.getText("text"); +text.subscribe((event) => { + console.log("Text changed:", event); +}); + +// Subscribe with deep observation +doc.subscribe((event) => { + // Path shows the location of the change + event.events.forEach((e) => { + console.log("Change path:", e.path); + console.log("Container:", e.target); + console.log("Diff:", e.diff); + }); +}); +``` + +--- + +## Undo/Redo + +Local undo operates on your own changes without breaking collaboration. See [Undo/Redo](/docs/advanced/undo) for design details and caveats. + +### UndoManager + +Provides local undo/redo functionality. + +**⚠️ Important Notes:** + +- **Local-only**: UndoManager only undoes the local user's operations, not remote operations +- **Origin filtering**: Use `excludeOriginPrefixes` to exclude certain operations (e.g., sync operations) from undo +- **Cursor restoration**: Use `onPush`/`onPop` callbacks to save and restore cursor positions + + +```typescript no_run +constructor(doc: LoroDoc, config: UndoConfig) +``` + + +Creates a new UndoManager instance. + +**Parameters:** + +- `doc` - The LoroDoc to manage undo/redo for +- `config` - Configuration options + - `mergeInterval?` - Time in ms to merge consecutive operations (default: 1000) + - `maxUndoSteps?` - Maximum number of undo steps to keep (default: 100) + - `excludeOriginPrefixes?` - Array of origin prefixes to exclude from undo + - `onPush?` - Callback when adding to undo stack + - `onPop?` - Callback when undoing/redoing + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; + +const doc = new LoroDoc(); +const undo = new UndoManager(doc, { + mergeInterval: 1000, + maxUndoSteps: 100, + excludeOriginPrefixes: ["sync-"], +}); +``` + + + + +```typescript no_run +undo(): boolean +``` + + +Undo the last operation. + +**Returns:** True if undo was successful + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); +const text = doc.getText("text"); +text.insert(0, "Hello"); +doc.commit(); + +// Usage example: +const success = undo.undo(); +console.log(success); // true +``` + + + + +```typescript no_run +redo(): boolean +``` + + +Redo the last undone operation. + +**Returns:** True if redo was successful + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); +const text = doc.getText("text"); +text.insert(0, "Hello"); +doc.commit(); +undo.undo(); + +// Usage example: +const success = undo.redo(); +console.log(success); // true +``` + + + + +```typescript no_run +canUndo(): boolean +``` + + +Check if undo is available. + +**Returns:** True if can undo + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +if (undo.canUndo()) { + undo.undo(); +} +``` + + + + +```typescript no_run +canRedo(): boolean +``` + + +Check if redo is available. + +**Returns:** True if can redo + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +if (undo.canRedo()) { + undo.redo(); +} +``` + + + + +```typescript no_run +peer(): PeerID +``` + + +Get the peer ID of the undo manager. + +**Returns:** The peer ID + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +const peerId = undo.peer(); +console.log(peerId); // e.g., "123456" +``` + + + + +```typescript no_run +setMaxUndoSteps(steps: number): void +``` + + +Set the maximum number of undo steps. + +**Parameters:** + +- `steps` - Maximum number of undo steps + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +undo.setMaxUndoSteps(50); +``` + + + + +```typescript no_run +setMergeInterval(interval: number): void +``` + + +Set the merge interval for grouping operations. + +**Parameters:** + +- `interval` - Merge interval in milliseconds + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +undo.setMergeInterval(2000); // 2 seconds +``` + + + + +```typescript no_run +groupStart(): void +``` + + +Begin a manual grouping of subsequent commits into a single undo step. + +**Behavior:** + +- Wrap consecutive `doc.commit()` calls so they undo together +- Calling `groupStart` again before `groupEnd` throws and leaves the current group unchanged +- Conflicting remote imports may automatically end the group and split the undo item + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; + +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); +const text = doc.getText("text"); + +undo.groupStart(); + +text.update("hello", undefined); +doc.commit(); + +text.update("hello world", undefined); +doc.commit(); + +undo.groupEnd(); + +undo.undo(); + +console.log(text.toString()); // "" +``` + + + + +```typescript no_run +groupEnd(): void +``` + + +Close the active manual group and enqueue the grouped operations as a single undo item. + +**Behavior:** + +- Must be paired with a prior `groupStart` +- Safe to call after the group was auto-closed by a conflicting remote import (becomes a no-op) +- Non-conflicting remote updates from other peers remain outside the undo item but do not break the group + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; + +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); +const text = doc.getText("text"); + +undo.groupStart(); + +text.update("hello", undefined); +doc.commit(); +text.update("hello world", undefined); +doc.commit(); + +const snapshot = doc.export({ mode: "snapshot" }); +const doc2 = new LoroDoc(); +doc2.import(snapshot); +doc2.getText("text2").update("hello world world", undefined); // touches a different container +doc2.commit(); +const update = doc2.export({ mode: "update" }); + +doc.import(update); // remote, non-conflicting change + +text.update("hello world world world", undefined); +doc.commit(); + +undo.groupEnd(); + +undo.undo(); + +console.log(text.toString()); // "" +``` + + + + +```typescript no_run +addExcludeOriginPrefix(prefix: string): void +``` + + +Add a prefix to exclude from undo stack. + +**Parameters:** + +- `prefix` - Origin prefix to exclude + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +undo.addExcludeOriginPrefix("sync-"); +undo.addExcludeOriginPrefix("import-"); +``` + + + + +```typescript no_run +clear(): void +``` + + +Clear the undo and redo stacks. + +**Example:** + +```ts threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +const undo = new UndoManager(doc, {}); + +// Usage example: +undo.clear(); +``` + + + +### Custom Undo Handlers + +Handle cursor restoration and side effects: + +```typescript no_run threeslash +import { LoroDoc, UndoManager } from "loro-crdt"; +const doc = new LoroDoc(); +declare function saveCursorPositions(): any; +declare function restoreCursorPositions(cursors: any): void; + +// Usage example: +const undo = new UndoManager(doc, { + onPush: (isUndo, counterRange, event) => { + // Save cursor positions when adding to undo stack + const cursors = saveCursorPositions(); + return { + value: doc.toJSON(), + cursors: cursors, + }; + }, + + onPop: (isUndo, { value, cursors }, counterRange) => { + // Restore cursor positions when undoing + restoreCursorPositions(cursors); + }, +}); +``` + +--- + +## Types & Interfaces + +Reference for core types used across the API. For conceptual background, see [Containers](/docs/concepts/container), [Version Vector](/docs/concepts/version_vector), [Frontiers](/docs/concepts/frontiers), and the [Versioning Deep Dive](/docs/advanced/version_deep_dive). + +### Core Types + +```typescript no_run +// Peer identifier +type PeerID = `${number}`; + +// Container identifier +type ContainerID = + | `cid:root-${string}:${ContainerType}` + | `cid:${number}@${PeerID}:${ContainerType}`; + +// Tree node identifier +type TreeID = `${number}@${PeerID}`; + +// Operation identifier +type OpId = { + peer: PeerID; + counter: number; +}; + +// Container types +type ContainerType = + | "Text" + | "Map" + | "List" + | "Tree" + | "MovableList" + | "Counter"; + +// Value types +type Value = + | ContainerID + | string + | number + | boolean + | null + | { [key: string]: Value } + | Uint8Array + | Value[] + | undefined; +``` + +### Version Types + +Loro uses two complementary version representations: Version Vectors (per-peer counters) and Frontiers (a compact set of heads). See [Version Vector](/docs/concepts/version_vector) and [Frontiers](/docs/concepts/frontiers). For the full DAG model, see [Version Deep Dive](/docs/advanced/version_deep_dive). + +```typescript no_run +// Version vector class +class VersionVector { + constructor( + value: Map | Uint8Array | VersionVector | undefined | null, + ); + static parseJSON(version: Map): VersionVector; + toJSON(): Map; + encode(): Uint8Array; + static decode(bytes: Uint8Array): VersionVector; + get(peer_id: number | bigint | `${number}`): number | undefined; + compare(other: VersionVector): number | undefined; + setEnd(id: { peer: PeerID; counter: number }): void; + setLast(id: { peer: PeerID; counter: number }): void; + remove(peer: PeerID): void; + length(): number; +} + +// Frontiers represent a specific version +type Frontiers = OpId[]; + +// ID span for range queries +type IdSpan = { + peer: PeerID; + counter: number; + length: number; +}; +``` + +### Change Types + +```typescript no_run +// Change metadata +interface Change { + peer: PeerID; + counter: number; + lamport: number; + length: number; + timestamp: number; // Unix timestamp in seconds + deps: OpId[]; + message: string | undefined; +} + +// Change modifier for pre-commit hooks +interface ChangeModifier { + setMessage(message: string): this; + setTimestamp(timestamp: number): this; +} +``` + +### Cursor Types + +Stable cursors survive concurrent edits and resolve to absolute positions on demand. See [Cursor and Stable Positions](/docs/concepts/cursor_stable_positions) and [Cursor tutorial](/docs/tutorial/cursor). + +```typescript no_run +// Stable position in containers +class Cursor { + containerId(): ContainerID; + pos(): OpId | undefined; + side(): Side; // -1 | 0 | 1 + encode(): Uint8Array; + static decode(data: Uint8Array): Cursor; +} + +// Cursor side affinity +type Side = -1 | 0 | 1; +``` + +### Delta Type + +Delta is a popular rich-text operation format (e.g., Quill). LoroText can export/import Delta; see [Text](/docs/tutorial/text). + +```typescript no_run +// Rich text delta operations +type Delta = + | { + insert: T; + attributes?: { [key in string]: {} }; + retain?: undefined; + delete?: undefined; + } + | { + delete: number; + attributes?: undefined; + retain?: undefined; + insert?: undefined; + } + | { + retain: number; + attributes?: { [key in string]: {} }; + delete?: undefined; + insert?: undefined; + }; +``` + +--- + +## Utility Functions + +Small helpers for type checks and IDs. See [Container IDs](/docs/advanced/cid) and [Versioning Deep Dive](/docs/advanced/version_deep_dive) for frontiers/version encoding. + +### Frontier Encoding + + +```typescript no_run +encodeFrontiers(frontiers: OpId[]): Uint8Array +``` + + +Encode frontiers for efficient transmission. + +**Parameters:** + +- `frontiers` - Array of operation IDs representing frontiers + +**Returns:** Encoded bytes + +**Example:** + +```ts threeslash +import { LoroDoc, encodeFrontiers } from "loro-crdt"; + +const doc = new LoroDoc(); +const frontiers = doc.frontiers(); +const encoded = encodeFrontiers(frontiers); +// Send encoded to remote peers +``` + + + + +```typescript no_run +decodeFrontiers(bytes: Uint8Array): OpId[] +``` + + +Decode frontiers from bytes. + +**Parameters:** + +- `bytes` - Encoded frontier bytes + +**Returns:** Array of operation IDs + +**Example:** + +```typescript no_run threeslash +import { decodeFrontiers } from "loro-crdt"; + +declare const encodedData: Uint8Array; +const frontiers = decodeFrontiers(encodedData); +console.log(frontiers); // [{ peer: "1", counter: 10 }, ...] +``` + + + +### Debugging + + +```typescript no_run +setDebug(): void +``` + + +Enable debug mode for detailed logging. + +**Example:** + +```ts threeslash +import { setDebug } from "loro-crdt"; + +// Enable debug logging +setDebug(); +``` + + + + +```typescript no_run +LORO_VERSION(): string +``` + + +Get the current Loro version. + +**Returns:** Version string + +**Example:** + +```ts threeslash +import { LORO_VERSION } from "loro-crdt"; + +const version = LORO_VERSION(); +console.log("Loro version:", version); +``` + + + +### Import Blob Metadata + + +```typescript no_run +decodeImportBlobMeta(blob: Uint8Array, check_checksum: boolean): ImportBlobMetadata +``` + + +Decode metadata from an import blob. + +**Parameters:** + +- `blob` - The import blob bytes +- `check_checksum` - Whether to verify checksum + +**Returns:** Import blob metadata + +**Example:** + +```typescript no_run threeslash +import { decodeImportBlobMeta } from "loro-crdt"; + +declare const blob: Uint8Array; +const metadata = decodeImportBlobMeta(blob, true); +console.log("Blob metadata:", metadata); +``` + + + +--- + +## EphemeralStore + +Manages ephemeral state like cursor positions and user presence. See [Ephemeral Store](/docs/tutorial/ephemeral) for concepts and usage patterns. Each entry uses timestamp-based LWW (Last-Write-Wins) for conflict resolution. + +**⚠️ Important:** + +- EphemeralStore is a separate CRDT without history - history/operations are NOT persisted +- Perfect for temporary state: cursor positions, selections, typing indicators +- Each peer's state auto-expires after the timeout period +- Uses Last-Write-Wins + +### EphemeralStore + + +```typescript no_run +constructor(timeout?: number) +``` + + +Creates a new EphemeralStore instance. + +**Parameters:** + +- `timeout` - Duration in milliseconds. A peer's state is considered outdated if its last update is older than this timeout. Default is 30000ms (30 seconds). + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; + +// Create ephemeral store with 30 second timeout +const store = new EphemeralStore(30000); +``` + + + + +```typescript no_run +set(key: K, value: T[K]): void +``` + + +Set an ephemeral value. + +**Parameters:** + +- `key` - The key to set +- `value` - The value to store + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); + +// Usage example: +store.set("cursor", { line: 10, column: 5 }); +store.set("selection", { start: 0, end: 10 }); +store.set("user", { name: "Alice", color: "#ff0000" }); +``` + + + + +```typescript no_run +get(key: K): T[K] | undefined +``` + + +Get an ephemeral value. + +**Parameters:** + +- `key` - The key to get + +**Returns:** The stored value or undefined + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); +store.set("cursor", { line: 10 }); + +// Usage example: +const cursor = store.get("cursor"); +console.log(cursor); // { line: 10 } +``` + + + + +```typescript no_run +delete(key: K): void +``` + + +Delete an ephemeral value. + +**Parameters:** + +- `key` - The key to delete + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); +store.set("cursor", { line: 10 }); + +// Usage example: +store.delete("cursor"); +``` + + + + +```typescript no_run +getAllStates(): Partial +``` + + +Get all ephemeral states. + +**Returns:** All stored states + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); +store.set("cursor", { line: 10 }); +store.set("user", { name: "Alice" }); + +// Usage example: +const allStates = store.getAllStates(); +console.log(allStates); +``` + + + + +```typescript no_run +encode(key: K): Uint8Array +``` + + +Encode a specific key's state for transmission. + +**Parameters:** + +- `key` - The key to encode + +**Returns:** Encoded bytes + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); +store.set("cursor", { line: 10 }); + +// Usage example: +const encoded = store.encode("cursor"); +// Send encoded to remote peers +``` + + + + +```typescript no_run +encodeAll(): Uint8Array +``` + + +Encode all states for transmission. + +**Returns:** Encoded bytes + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); +store.set("cursor", { line: 10 }); +store.set("user", { name: "Alice" }); + +// Usage example: +const encoded = store.encodeAll(); +// Send encoded to remote peers +``` + + + + +```typescript no_run +apply(bytes: Uint8Array): void +``` + + +Apply remote updates. + +**Parameters:** + +- `bytes` - Encoded updates from remote peer + +**Example:** + +```typescript no_run threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); + +// Usage example: +declare const remoteData: Uint8Array; +store.apply(remoteData); +``` + + + + +```typescript no_run +keys(): string[] +``` + + +Get all keys in the store. + +**Returns:** Array of keys + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); +store.set("cursor", { line: 10 }); +store.set("user", { name: "Alice" }); + +// Usage example: +const allKeys = store.keys(); +console.log(allKeys); // ["cursor", "user"] +``` + + + + +```typescript no_run +subscribe(listener: EphemeralListener): () => void +``` + + +Subscribe to all ephemeral state changes. + +**Parameters:** + +- `listener` - Callback function for state changes + +**Returns:** Unsubscribe function + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); + +// Usage example: +const unsubscribe = store.subscribe((event) => { + console.log("Ephemeral state changed:", event); +}); + +// Later, unsubscribe +unsubscribe(); +``` + + + + +```typescript no_run +subscribeLocalUpdates(listener: EphemeralLocalListener): () => void +``` + + +Subscribe to local ephemeral updates for syncing to remote peers. + +**Parameters:** + +- `listener` - Callback function that receives encoded updates + +**Returns:** Unsubscribe function + +**Example:** + +```typescript no_run threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); + +// Usage example: +declare const websocket: { send: (data: Uint8Array) => void }; + +const unsubscribe = store.subscribeLocalUpdates((data) => { + // Send to remote peers + websocket.send(data); +}); + +// Later, unsubscribe +unsubscribe(); +``` + + + + +```typescript no_run +destroy(): void +``` + + +Clean up and destroy the ephemeral store. + +**Example:** + +```ts threeslash +import { EphemeralStore } from "loro-crdt"; +const store = new EphemeralStore(30000); + +// Usage example: +store.destroy(); +``` + + + +### Complete Example + +```typescript no_run threeslash +import { EphemeralStore } from "loro-crdt"; + +// Assume we have: +declare const websocket: { + send: (data: Uint8Array) => void; + on: (event: string, handler: (data: any) => void) => void; +}; + +const store = new EphemeralStore(30000); +const store2 = new EphemeralStore(30000); + +// Subscribe to local updates +store.subscribeLocalUpdates((data) => { + store2.apply(data); +}); + +// Subscribe to all updates +store2.subscribe((event) => { + console.log("event:", event); +}); + +// Set a value +store.set("key", "value"); + +// Encode the value +const encoded = store.encode("key"); + +// Apply the encoded value +store2.apply(encoded); +``` + +--- + +
+ + # FILE: pages/changelog/v1.8.0.mdx --- @@ -123,6 +13253,43 @@ The following are the specific API changes: - `exportFrom(version)` and `exportSnapshot()` are deprecated, use `export(mode: ExportMode)` instead. +# FILE: pages/changelog/v1.9.0.mdx + +--- +version: "v1.9.0" +title: "Release Loro v1.9.0" +date: 2025/11/10 +--- + +## Highlights + +- JSONPath queries are now RFC 9535 compliant across both Rust and WASM bindings. A new parser, evaluator, and conformance test suite (PR #848, commit `21d13218`) unlock richer filters like `in` and `contains` while delivering clearer error messages. Thanks @zolero! +- Source maps become first-class in the WASM toolchain. `loro-wasm-tools` now embeds symbols during bundling (#836) and the new `loro-wasm-map` package publishes maps next to the runtime bundle (#844), drastically simplifying debugging in browsers, Vite, and other build systems. + +## Breaking change + +- **Removed legacy v0.x encoding path (#849, `10a405b4`)**. Legacy serializers, compatibility fuzz tests, and decoding shims are gone. All runtimes now rely on the v1.* encoding format. Documents created with v0.x must be migrated via an intermediate ≀1.8.x release before installing 1.9.0. + +## New features & improvements + +- **Spec-compliant JSONPath (#848, `21d13218`)**. Ships a Pest-based parser, richer AST, the `in`/`contains` filter operators, better existence checks, and shared tests/benchmarks to keep Rust and WASM perfectly aligned. +- **WASM debugging experience (#836, `6f987022`; #844, `53f55331`)**. Adds `loro-wasm-tools` for embedding sourcemaps, automates sourcemap publishing in CI, and hosts the maps in a dedicated workspace package so source-level debugging works without manual steps. +- **Better bundler support (#852 `3af6a857`, #851 `366f0161`)**. Patches the JS entry points so esbuild and rsbuild can import the WASM bundle with zero loader overrides. +- **Bun runtime improvements (#829 `b8c070fd`, #828 `cf123453`, #827 `8b622619`, #834 `865bccba`)**. Streams the WASM binary during bundling, pre-seeds the externref table, integrates Bun into CI, and removes earlier hacks that hid loader bugs. +- **JavaScript performance (#820 `c2c535c1`)**. Event conversion now runs exactly once per callback invocation, trimming overhead during heavy real-time editing sessions. +- **Core dependency refresh (`e8f79de8`, `b2f5e107`)**. Upgrades `generic-btree` and the columnar storage crate to pick up the latest bug fixes and perf wins. + +## Bug fixes & stability + +- Fixed tree undo operations that moved nodes between siblings (#821 `76a8728e`). +- Resolved container ID bookkeeping when exporting shallow snapshots (#823 `b72a759a`) and ensured pending containers now return `None` instead of leaking state (#840 `35d9064d`). +- Prevented `LoroMap` entries from turning into `null` after `applyDiff` (#825 `3afc4d52`). +- Ensured undo manager callbacks fire without tripping Rust aliasing violations (#831 `a39daf85`). +- Guarded against panics when cursors point to deleted entries (#835 `e97e6056`) and when fetching unknown cursors in JS integration tests. +- Cleaned up WASM loader glue so we no longer rely on an extra patch (#834 `865bccba`) and added multiple Bun regression tests (#828 `cf123453`). +- Tightened CI release scripts (`74b78514`, `2383cdc1`, `4740a04c`, `3373e046`, `24f93249`) to keep `loro-wasm` artifacts and sourcemaps publishing in sync. + + # FILE: pages/changelog/v1.3.0.mdx --- @@ -211,8 +13378,8 @@ date: 2025/04/30 Try it here: [Loro Inspector](https://inspector.loro.dev/) -Now you can directly browse the current state and complete edit history of your Loro -documents in the browser. You can also use this tool to time travel to any version +Now you can directly browse the current state and complete edit history of your Loro +documents in the browser. You can also use this tool to time travel to any version in the history of your Loro document. import { ReactPlayer } from "../../components/video"; @@ -349,7 +13516,7 @@ console.log(newStore.get("user-alice")) - Fixed text styling at end "\n" character - Added JSON support for current transaction operations -- For environments that support multi-threading such as Rust and Swift, LoroDoc can now be directly and safely +- For environments that support multi-threading such as Rust and Swift, LoroDoc can now be directly and safely shared and accessed in parallel across multiple threads without triggering the previous WouldBlock panic. @@ -541,6 +13708,131 @@ import { ReactPlayer } from "../../../components/video"; /> +# FILE: pages/docs/advanced/jsonpath.mdx + +--- +keywords: "jsonpath, querying, loro, crdt, advanced" +description: "Learn how to query Loro documents using RFC 9535 JSONPath support." +--- + +# JSONPath Queries + +[JSONPath](https://www.rfc-editor.org/rfc/rfc9535.html) queries integrate +directly with Loro documents so you can traverse complex data without writing +ad hoc tree walkers. This guide outlines the query surface, highlights +supported syntax, and walks through examples that you can adapt to your own +application. + +> JSONPath functions such as `count()`, `length()`, `value()`, `match()`, and +> `search()` are parsed but not evaluated at runtime. Each currently returns +> `JsonPathError::EvaluationError::Unimplemented`. + +## Preparing Sample Data + +The snippets below use the WASM bindings, but the same structure works with the +Rust API. We create a bookstore dataset inside a `LoroDoc` so we can run queries +against it. + +> ⚠️ **Illustration only**: This approach stuffs plain JavaScript objects into a +> document for convenience. In production code prefer composing `LoroMap` and +> `LoroList` instances explicitly so you can control how data enters the CRDT and +> benefit from fine-grained updates. + +```ts twoslash +import { LoroDoc } from "loro-crdt"; + +const doc = new LoroDoc(); +const testData = { + books: [ + { title: "1984", author: "George Orwell", price: 10, available: true, isbn: "978-0451524935" }, + { title: "Animal Farm", author: "George Orwell", price: 8, available: true }, + { title: "Brave New World", author: "Aldous Huxley", price: 12, available: false }, + { title: "Fahrenheit 451", author: "Ray Bradbury", price: 9, available: true, isbn: "978-1451673318" }, + { title: "The Great Gatsby", author: "F. Scott Fitzgerald", price: null, available: true }, + { title: "To Kill a Mockingbird", author: "Harper Lee", price: 11, available: true }, + { title: "The Catcher in the Rye", author: "J.D. Salinger", price: 10, available: false }, + { title: "Lord of the Flies", author: "William Golding", price: 9, available: true }, + { title: "Pride and Prejudice", author: "Jane Austen", price: 7, available: true }, + { title: "The Hobbit", author: "J.R.R. Tolkien", price: 14, available: true } + ], + featured_author: "George Orwell", + min_price: 10, + featured_authors: ["George Orwell", "Jane Austen"], +}; + +const store = doc.getMap("store"); +Object.entries(testData).forEach(([key, value]) => store.set(key, value)); +doc.commit(); +``` + +## Executing JSONPath Queries + +Run a query with the `JSONPath` method on WASM or `jsonpath` on Rust. The API +returns a list of values for any matches. Container results currently surface as +handlers internally, but that interface is still evolving and treated as +opaque in this guide. + +```ts twoslash +import { LoroDoc } from "loro-crdt"; +// ---cut--- +const doc = new LoroDoc(); +// setup omitted for brevity +const results = doc.JSONPath("$.store.books[*].title"); +console.log(results); +// => ["1984", "Animal Farm", ...] +``` + +```rust +use loro::prelude::*; + +let doc = LoroDoc::new(); +// setup omitted for brevity +let titles = doc.jsonpath("$.store.books[*].title")?; +for title in titles { + if let ValueOrHandler::Value(value) = title { + println!("{value}"); + } +} +``` + +### Supported Selectors and Filters + +RFC 9535 syntax works end-to-end, including: + +- **Selectors** – names, indices (positive/negative), slices, unions, wildcards, + and recursive descent (`..`). +- **Filters** – logical operators (`&&`, `||`, `!`), comparisons, membership via + `in`, substring checks with `contains`, property existence, and nested + subqueries through `$` (root) or `@` (current node). +- **Functions** – `count()`, `length()`, and `value()` are parsed, while `match()` + and `search()` are reserved for expansion. All functions currently return an + unimplemented evaluation error at runtime. + +## Cookbook Examples + +Once the document is populated you can combine selectors and filters to extract +precisely what you need. + +| Query | Description | Result | +| ----- | ----------- | ------ | +| `$.store.books[*].title` | All book titles. | `["1984", "Animal Farm", "Brave New World", "Fahrenheit 451", "The Great Gatsby", "To Kill a Mockingbird", "The Catcher in the Rye", "Lord of the Flies", "Pride and Prejudice", "The Hobbit"]` | +| `$.store.books[?(@.available)].title` | Titles of books in stock. | `["1984", "Animal Farm", "The Great Gatsby", "To Kill a Mockingbird", "Lord of the Flies", "Pride and Prejudice", "The Hobbit"]` | +| `$.store.books[?(@.author in $.store.featured_authors)].title` | Books by featured authors. | `["1984", "Animal Farm", "Pride and Prejudice"]` | +| `$.store.books[?(@.price > 12)].title` | Books priced above 12. | `["The Hobbit"]` | +| `$..price` | All price fields via recursive descent. | `[10, 8, 12, 9, null, 11, 10, 9, 7, 14]` | +| `$.store.books[0:3].title` | Slice syntax for the first three titles. | `["1984", "Animal Farm", "Brave New World"]` | +| `$.store.books[0,2,-1].title` | Union of specific indices. | `["1984", "Brave New World", "The Hobbit"]` | +| `count($.store.books[?(@.available)])` | Planned helper to count available books *(currently returns an unimplemented error)*. | `JsonPathError::EvaluationError::Unimplemented` | +| `length($.store.featured_authors)` | Planned array length helper *(currently returns an unimplemented error)*. | `JsonPathError::EvaluationError::Unimplemented` | +| `$.store.books[?(@.isbn && @.price >= $.store.min_price)].title` | Filter by field existence and comparison. | `["1984"]` | +| `$.store.books[?(!@.available)].title` | Negated availability filter. | `["Brave New World", "The Catcher in the Rye"]` | +| `$.store.books[?(@.title contains "The")].author` | Authors with "The" in the title. | `["F. Scott Fitzgerald", "J.R.R. Tolkien"]` | + +> JSONPath always returns values in document order. When a filter references +> another query (such as `$.store.featured_authors`), the subquery is evaluated +> for each candidate element. + + # FILE: pages/docs/advanced/cid.mdx # Container ID @@ -589,6 +13881,16 @@ export type ContainerID = containers - Contains the Operation ID of its creation within its Container ID +3. **Mergeable Containers** + - Created through `LoroMap.ensureMergeableText`, + `ensureMergeableMap`, `ensureMergeableList`, and the other + `ensureMergeable*` methods + - Use a special form of Root Container ID. The stable name inside that ID is + derived from the logical position `(parent Map, key, container type)`, not + from the operation that first created the child + - Useful when multiple peers may lazily initialize the same child container + under the same Map key + ## Container States and IDs The ContainerID is not a random UUID but is deterministically generated based on the container's context. To understand how ContainerIDs work, it's important to first understand container states. @@ -598,6 +13900,7 @@ The ContainerID is not a random UUID but is deterministically generated based on Key points about ContainerID generation: - **Root containers**: Derive their ID from their name (e.g., "text" in `doc.getText("text")`) - **Normal containers**: Derive their ID from the operation (OpID) that created them +- **Mergeable containers**: Use a special Root Container ID whose stable name is derived from the parent Map, key, and container type - **Detached containers**: Have a default placeholder ID until they're inserted into a document ## Container Overwrites @@ -637,20 +13940,34 @@ document content, overwriting it can lead to the unintended hiding/loss of critical information. For this reason, it is essential to implement careful and systematic container initialization practices to prevent such issues. +### Mergeable Containers + +The overwrite behavior occurs because parallel creation of regular child +containers results in different container IDs, preventing automatic merging of +their contents. If peers should address the same child by logical Map key, use a +mergeable child container: + +```ts no_run +const text = doc.getMap("map").ensureMergeableText("text"); +``` + +This uses a special Root Container ID for the child. Its stable name is derived +from the parent Map, key, and type, rather than from the operation that first +created it. + ### Best Practices -1. When containers might be initialized concurrently, prefer initializing them - at the root level rather than as nested containers +1. Use `ensureMergeable*` when a Map child container should be shared by logical + key even if peers initialize it concurrently. -2. When using map containers: +2. When the structure is fixed and known ahead of time: - If possible, initialize all child containers during the map container's initialization - - Avoid concurrent creation of child containers with the same key in the map - container to prevent overwrites -The overwrite behavior occurs because parallel creation of child containers -results in different container IDs, preventing automatic merging of their -contents. +3. Use root containers when the child naturally has a stable top-level name. + +4. Use regular child containers when each creation should have a distinct + operation-derived identity. # FILE: pages/docs/advanced/import_batch.mdx @@ -928,7 +14245,7 @@ The `onPush` and `onPop` callbacks are triggered when these stacks change: The `mergeInterval` option in the UndoManager controls how closely spaced operations are grouped: -```ts twoslash no_run +```ts twoslash no_run import { UndoManager, LoroDoc } from "loro-crdt"; // ---cut--- declare const doc: LoroDoc; @@ -986,7 +14303,7 @@ undoManager.undo(); // Reverts both commits in one step Consider a simple text editor that uses Loro for collaboration. Let's walk through what happens during typical editing operations: -```ts twoslash no_run +```ts twoslash no_run import { LoroDoc, UndoManager } from "loro-crdt"; // ---cut--- // Create document and undo manager @@ -1008,17 +14325,17 @@ const undoManager = new UndoManager(doc, { cursors: [/* your cursor positions */] }; }, - + onPop: (isUndo, storedData) => { // Access the data you stored during onPush const { value, cursors } = storedData; - + if (isUndo) { console.log("Retrieving data for undo"); } else { console.log("Retrieving data for redo"); } - + // Use the stored cursors to restore selection // applyStoredCursors(cursors); }, @@ -1059,8 +14376,8 @@ This approach ensures that local changes can be undone without affecting other u ### Cursor Efficiency -The built-in cursor solution is optimized for performance and handles collaborative scenarios efficiently, including situations where peers may -change the document concurrently during undo/redo operations. For complex editors like rich text editors, the cursor implementation provides the +The built-in cursor solution is optimized for performance and handles collaborative scenarios efficiently, including situations where peers may +change the document concurrently during undo/redo operations. For complex editors like rich text editors, the cursor implementation provides the best balance of performance and correctness. @@ -1740,6 +15057,16 @@ description: "how to use loro map crdt and show all APIs of loro map crdt." Loro's Map uses LWW (Last-Write-Wins) semantics. When concurrent edits conflict, it compares Lamport logic timestamps to determine the winner. +Map slot conflicts are still resolved by LWW. If the value at a key is a child +container, regular child containers have operation-derived identities, so two +peers that create a child at the same key can create two different children. If +the child should be shared by logical key, use a mergeable child container: + +```ts no_run +const body = doc.getMap("notes").ensureMergeableText(noteId); +body.insert(0, "Hello"); +``` + Here is how to use it: ```ts twoslash @@ -1896,7 +15223,7 @@ map.set("complex", { }); ``` -Note: When you need to store a CRDT container (like LoroText, LoroList, etc.) within another container, use `setContainer()` or `insertContainer()` methods instead of regular `set()` or `insert()`. This creates a proper sub-container relationship that maintains CRDT properties. +Note: When you need to store a CRDT container (like LoroText, LoroList, etc.) within another container, use `setContainer()` or `insertContainer()` methods instead of regular `set()` or `insert()`. This creates a proper sub-container relationship that maintains CRDT properties. If a child container under a Map may be created lazily by multiple peers at the same key, use the `ensureMergeable*` methods instead; see [Containers](/docs/concepts/container) and [Composing CRDTs](/docs/tutorial/composition). ## Container Types @@ -2679,7 +16006,7 @@ with changes in the document state. - When `LoroDoc.commit()` is explicitly called - Automatically before an import or export operation - Note that events are emitted asynchronously after a microtask. If you need to handle events immediately after a commit, you should await a microtask: +Starting from `loro-crdt@1.8.0`, events are emitted synchronously during the commit cycle. If you are using an older version (`<=1.7.x`), you will still need to await a microtask for the callbacks to fire. ```ts no_run twoslash import { LoroDoc } from "loro-crdt"; @@ -2692,9 +16019,7 @@ doc.subscribe((event) => { const text = doc.getText("text"); text.insert(0, "Hello"); doc.commit(); -// Event hasn't been emitted yet -await Promise.resolve(); -// Now the event has been emitted +// Event has already been emitted synchronously ``` 3. **Import**: When importing changes from a remote source using the `import()` @@ -2710,21 +16035,19 @@ doc.subscribe((event) => { console.log("Event:", event); }); -doc.import(remoteChanges); // This will trigger events after a microtask -await Promise.resolve(); // Wait for events to be emitted +doc.import(remoteChanges); // This immediately triggers events (v1.8+) ``` 4. **Version Checkout**: When you switch document state to a different version using `doc.checkout(frontiers)`, Loro emits an event to reflect this change. - Like other events, these are also emitted after a microtask. + As of v1.8, checkout events fire synchronously alongside the state change. ```ts no_run twoslash import { LoroDoc } from "loro-crdt"; // ---cut--- const doc = new LoroDoc(); const frontiers = doc.frontiers(); -doc.checkout(frontiers); // This will trigger events after a microtask -await Promise.resolve(); // Wait for events to be emitted +doc.checkout(frontiers); // This triggers events immediately (v1.8+) ``` ## Transaction Behavior @@ -3085,6 +16408,15 @@ Delete text at the given range. Get a string slice. +### `sliceDelta(start: number, end: number): Delta[]` + +Get a Quill-style Delta slice for the given UTF-16 range. Use +`sliceDeltaUtf8` to slice by UTF-8 byte offsets instead. + +### `sliceDeltaUtf8(start: number, end: number): Delta[]` + +Get a Quill-style Delta slice for the given UTF-8 byte range. + ### `toString(): string` Get the plain text value. @@ -3213,6 +16545,33 @@ console.log(text.toDelta()); // ] ``` +### Slice a Delta snippet + +Use `sliceDelta(start, end)` when you only need a portion of the Delta (for example, to copy a styled snippet). It uses UTF-16 indices just like other text APIs; use `sliceDeltaUtf8` if you need to slice by UTF-8 byte offsets instead. + +```ts twoslash +import { LoroDoc } from "loro-crdt"; +import { expect } from "expect"; +// ---cut--- +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); +expect(snippet).toStrictEqual([ + { insert: "ello", attributes: { bold: true } }, + { insert: " " }, + { insert: "Wo", attributes: { comment: "greeting" } }, +]); +``` + ### `mark(range: {start: number, end: number}, key: string, value: any): void` Mark the given range with a key-value pair. @@ -3456,6 +16815,12 @@ expect(docA.toJSON()).toStrictEqual(docB.toJSON()); Due to CRDT properties, document consistency is guaranteed when peers receive the same updates, regardless of order or duplicates. +Consistency does not remove the need to choose the right child container +identity. If two offline peers create regular child containers under the same +Map key, sync will converge to one visible Map value and the other child can be +hidden by the Map conflict rule. For dynamic child containers that should be +shared by logical key, use `ensureMergeable*` when modeling the document. + ### Sync Strategies 1. **First Sync** (Initial synchronization between peers): @@ -3787,6 +17152,15 @@ expect(doc.toJSON()).toStrictEqual({ map: { list: [0, 1, "Hello, World!"] } }); } ``` +`setContainer` creates a regular child container. This is fine for fixed +schemas, or when each creation should produce a distinct child object. If +multiple peers may lazily create the same child under the same Map key, use a +mergeable child container instead: + +```ts no_run +const sharedList = map.ensureMergeableList("list"); +``` + # FILE: pages/docs/tutorial/tips.mdx @@ -3812,6 +17186,8 @@ from an identical baseline without generating unnecessary operations. When using `setPeerId`, you must avoid having two parallel peers use the same PeerId. This can cause serious consistency problems in your application. +If you plan to reuse a PeerId across sessions, make sure that you persist the document's local state together with that PeerId and load it before applying any remote updates. Otherwise, the same `peerId + counter` combination could end up referring to two different operations, producing divergence that cannot be resolved automatically. +
Why @@ -3826,6 +17202,8 @@ Be careful when reusing PeerIds (this optimization is often unnecessary). You sh If you must reuse PeerIds, you need to carefully manage your local PeerId cache with proper locking mechanisms. This would allow only one tab to "take" a specific PeerId, while other tabs use random IDs. The PeerId should be returned to the cache when no longer in use. +In addition to locking, avoid having multiple browser tabs (or apps on different processes) use the same PeerId concurrently. Coordinating access in this way ensures that a reused PeerId never emits two different operations with the same operation ID. +
--- @@ -3838,12 +17216,12 @@ This behavior contrasts with non-root containers. For example, when you execute --- -##### When initializing child containers of LoroMap in parallel, overwrites can occur instead of automatic merging. +##### When initializing child containers of LoroMap in parallel, use mergeable containers when you want one shared child.
Why this happens -This happens because parallel creation of child containers results in different container IDs, preventing automatic merging of their contents. When a container holds substantial data or serves as the primary storage for document content, overwriting it can lead to unintended hiding or loss of critical information. +This happens because parallel creation of regular child containers results in different container IDs, preventing automatic merging of their contents. When a container holds substantial data or serves as the primary storage for document content, overwriting it can lead to unintended hiding or loss of critical information. ```ts twoslash import { LoroDoc, LoroText } from "loro-crdt"; @@ -3867,15 +17245,54 @@ doc.import(docB.export({ mode: "update" }));
Best practices for container initialization -1. When using map containers: - - If possible, initialize all child containers during the map container's initialization - - Avoid concurrent creation of child containers with the same key in the map container to prevent overwrites -2. If it's impossible to initialize all child containers when the map container is initialized, prefer initializing them at the root level rather than as nested containers. - - You can use `doc.getMap("user." + userId)` instead of `doc.getMap("user").getOrCreateContainer(userId, new LoroMap())` to avoid this problem. +1. If peers may lazily create the same child under the same Map key, use a mergeable child container: + + ```ts no_run + const entries = doc.getMap("days").ensureMergeableList("2026-06-08"); + entries.insert(0, "meeting notes"); + ``` + + The same pattern is available through `ensureMergeableText`, `ensureMergeableMap`, `ensureMergeableMovableList`, `ensureMergeableTree`, and `ensureMergeableCounter`. + +2. If the child structure is fixed and known ahead of time, initialize all child containers during the map container's initialization. + +3. If the child naturally has a unique global name, a root container is also safe. You can use `doc.getMap("user." + userId)` instead of `doc.getMap("user").getOrCreateContainer(userId, new LoroMap())`. + +4. Keep using `setContainer` or `insertContainer` when each creation should produce a distinct child object, or when you are modeling replacement rather than shared initialization.
--- +##### Use `diff` + `applyDiff` to share squash-like change sets + +For PR-style workflows, compute a diff from the base version to the new version, then apply it back. The diff is compact: operations that cancel out (e.g., insert then delete) are compressed away. + +```ts twoslash +import { LoroDoc } from "loro-crdt"; +// ---cut--- +const baseDoc = new LoroDoc(); +baseDoc.getText("text").insert(0, "hello world"); + +const newDoc = baseDoc.fork(); +const text = newDoc.getText("text"); +text.insert(0, "abc"); +text.delete(0, 4); // cancels most of the insert + +const diff = newDoc.diff(baseDoc.frontiers(), newDoc.frontiers()); +baseDoc.applyDiff(diff); + +console.log(diff); +// [ +// [ +// "cid:root-text:Text", +// { type: "text", diff: [ { delete: 1 } ] } +// ] +// ] +console.log(baseDoc.toJSON()); // { text: "ello world" } +``` + +--- + ##### Use redaction to safely share document history There are times when users might accidentally paste sensitive information (like API keys, passwords, or personal data) into a collaborative document. When this happens, you need a way to remove just that sensitive content from the document history without compromising the rest of the document's integrity. @@ -4106,6 +17523,10 @@ node.data.set("key", "value"); node.data.setContainer("list", new LoroList()); ``` +`node.data` is a Map. If the child container under `node.data` may be lazily +created by multiple peers at the same key, use a mergeable child such as +`node.data.ensureMergeableList("list")` instead of `setContainer`. + ## Ordered Tree Nodes In certain scenarios such as graphic design or file systems, where sibling nodes may also require a sequential relationship, we have introduced `Fractional Index` in Loro to support this capability. You can read more about `Fractional Index` in the [Figma blog](https://www.figma.com/blog/realtime-editing-of-ordered-sequences). In simple terms, `Fractional Index` assigns a sortable value to each object, and if a new insertion occurs between two objects, the `Fractional Index` of the new object will be between the left and right values. The rust-based `Fractional Index` [implementation of Drifting-in-Space](https://github.com/drifting-in-space/fractional_index) has good design and minimal document size growth in most cases. We forked it and made a simple extension for use in Loro. @@ -4234,6 +17655,7 @@ To achieve both quick saves and resource efficiency: keystroke or with debounce/throttle). Store these binary data in fast-write storage like user disks or a key-value database. This ensures quick saves with low resource cost. +- Loro validates the checksum embedded in every snapshot/update header during import, so corrupted binary payloads (for example, from bit flips in queues or object storage) are rejected before they touch your document state. - When loading a document, import the snapshot and all related updates to get the latest version. - After importing, export a new snapshot to replace the old one and remove @@ -4295,6 +17717,7 @@ You can use Loro in your application by using: - [`loro-cs`](https://github.com/sensslen/loro-cs) Community-maintained C# package - You can also find a list of examples in [Loro examples in Deno](https://github.com/loro-dev/loro-examples-deno). +- [`loro-go`](https://github.com/aholstenson/loro-go) Community-maintained Go package You can use [Loro Inspector](/docs/advanced/inspector) to debug and visualize the state and history of Loro documents. @@ -4447,6 +17870,8 @@ const bytes = doc.export({ mode: "snapshot" }); // Bytes can be saved to local storage, database, or sent over the network ``` +Snapshots and updates include a checksum in their headers, so any corruption from storage or transmission (like bit flips) is detected during import before it can affect your document. + Loading your app state: ```ts no_run twoslash @@ -4886,6 +18311,7 @@ CRDT libraries. | Time Travel | βœ… | βœ… | βœ…[1] | βœ… | | [Fugue](https://arxiv.org/abs/2305.00583) / Maximal non-interleaving | βœ… | βœ… | ❌ | ❌ | | JSON Types | βœ… | ❓ | βœ… | βœ… | +| [Mergeable Containers](/blog/mergeable-containers) | βœ… | ❌ | ❌ | ❌ | | Merging Elements in Memory by Run Length Encoding | βœ… | βœ… | βœ… Inventor | ❌ | | Byzantine-fault-tolerance | ❌ | ❌ | ❌ | βœ… | | Version Control | βœ… | ❌ | ❌ | βœ… | @@ -4919,7 +18345,7 @@ Op IDs: 1 2 3 4 5 6 7 8 9 A B Cursor: References ID 5 (after 'o') After concurrent insert at start: -Text: N e w H e l l o W o r l d +Text: N e w H e l l o W o r l d Op IDs: C D E F 1 2 3 4 5 6 7 8 9 A B Cursor: Still references ID 5 - position automatically adjusted ``` @@ -4987,8 +18413,8 @@ description: "When CRDTs are not the right tool: hard invariants, exclusivity, o # When Not to Use CRDTs -CRDTs shine for collaborative editing and offline-friendly applications. -But they are not a universal replacement for coordination, transactions, +CRDTs shine for collaborative editing and offline-friendly applications. +But they are not a universal replacement for coordination, transactions, or authorization. Use this guide to recognize when CRDTs are a poor fit and what to use instead. ## Quick Reference @@ -5117,7 +18543,7 @@ async function handleImport( fetchMissing: (from: VersionVector) => Promise ) { const status = doc.import(update); - + if (status.pending) { const missing = await fetchMissing(doc.version()); doc.import(missing); @@ -5458,7 +18884,7 @@ One of Eg-Walker's most significant advantages is **safe garbage collection**: Eg-Walker is based on rigorous academic research: -> [Collaborative Text Editing with Eg-walker: Better, Faster, Smaller](https://arxiv.org/abs/2409.14252) +> [Collaborative Text Editing with Eg-walker: Better, Faster, Smaller](https://arxiv.org/abs/2409.14252) > By: Joseph Gentle, Martin Kleppmann The algorithm has been proven to: @@ -5643,7 +19069,7 @@ of CRDTs. (PS: Marc Shapiro actually wrote a paper [Designing a commutative replicated data type](https://hal.inria.fr/inria-00177693v2/document) -in 2007. In 2011, he reworded commutative into conflict-free in 2011, expanding +in 2007. In 2011, he reworded commutative into conflict-free, expanding the definition of commutative to include state-based CRDT) According to [CAP theorem](https://en.wikipedia.org/wiki/CAP_theorem), it is @@ -5918,20 +19344,40 @@ document content, overwriting it can lead to the unintended hiding/loss of critical information. For this reason, it is essential to implement careful and systematic container initialization practices to prevent such issues. +### Mergeable Containers + +The overwrite happens because `setContainer` creates a regular child container. +Its Container ID includes the operation that created it, so two peers that +create `map["text"]` concurrently create two different Text containers. + +When a child container should be identified by its logical position in a Map, +use a mergeable child container instead: + +```ts no_run +const text = doc.getMap("map").ensureMergeableText("text"); +text.insert(0, "A"); +``` + +Peers that call `ensureMergeableText("text")` on the same parent Map address the +same logical Text container. The same pattern is available for Map, List, +MovableList, Tree, and Counter children through the `ensureMergeable*` methods. + ### Best Practices -1. When containers might be initialized concurrently, prefer initializing them - at the root level rather than as nested containers +1. For dynamic Map keys where peers may lazily create the same child, use + `ensureMergeableText`, `ensureMergeableMap`, `ensureMergeableList`, and the + other `ensureMergeable*` methods. -2. When using map containers: +2. When the structure is fixed and known ahead of time: - If possible, initialize all child containers during the map container's initialization - - Avoid concurrent creation of child containers with the same key in the map - container to prevent overwrites -The overwrite behavior occurs because parallel creation of child containers -results in different container IDs, preventing automatic merging of their -contents. +3. Use regular `setContainer` / `insertContainer` when each creation should + produce a distinct child object, or when you are modeling replacement rather + than shared initialization. + +4. A unique root container name can still be a good fit when the child naturally + belongs at the root, for example `doc.getMap("user." + userId)`. ## Related Concepts @@ -6009,13 +19455,13 @@ async function archiveAndTrim(doc: LoroDoc) { // 1. Archive full history const full = doc.export({ mode: "snapshot" }); await saveToArchive(full); - + // 2. Create shallow for active use const shallow = doc.export({ mode: "shallow-snapshot", frontiers: doc.frontiers(), }); - + return shallow; } @@ -6030,11 +19476,11 @@ import { LoroDoc } from "loro-crdt"; // ---cut--- class PrivacyDoc { constructor(private doc: LoroDoc) {} - + async redactSensitive() { // Delete sensitive content this.doc.getText("private").delete(0, this.doc.getText("private").length); - + // Create clean snapshot return this.doc.export({ mode: "shallow-snapshot", @@ -6164,7 +19610,7 @@ class RelayServer { ## Best Practices - **Export modes**: Updates for sync, snapshots for persistence, shallow for startup -- **Optimize by use case**: +- **Optimize by use case**: - Editors: Both in memory - Relays: OpLog only @@ -6227,7 +19673,7 @@ doc.commit(); // Merges with #1 (same peer, consecutive) 1. **Cross-peer dependencies**: After importing remote operations 2. **Time separation**: When timestamps enabled and > the merge interval (default 1000s) between commits -3. **Different commit messages**: +3. **Different commit messages**: ```ts twoslash import { LoroDoc } from "loro-crdt"; @@ -6306,6 +19752,8 @@ doc.setPeerId("123123123"); // You can only set 64 bit integers as peer IDs ⚠️ **Warning**: Manual assignment requires careful conflict avoidance. +If you intentionally reuse a Peer ID, persist the document's local data (snapshot, updates, or durable cache) alongside that ID and load it before fetching or applying any remote updates. This ensures the `(peerId, counter)` pairs continue to reference the same operations. Skipping this step risks generating a new operation that reuses an existing operation ID, which leads to inconsistent replicas. + ## Counter System Each peer maintains a monotonic counter starting at 0: @@ -6330,6 +19778,7 @@ console.log(doc.version()); // { "1": 2 } - Use user IDs as peer IDs, because an user can have multiple devices - Use fixed IDs - Reuse IDs without proper management +- Allow multiple browser tabs or processes to operate on the same reused Peer ID in parallelβ€”coordinate access with locks so that only one session emits operations for that ID at a time ## Related Documentation @@ -6353,6 +19802,22 @@ For text, you can choose to represent it directly as a Value on a Map (where the For Lists, concurrently removing the same element and inserting a single element creates a new element, differentiating from the semantics of Set on a Map (we may consider providing a list set method in the future). For representing coordinates, it's better to use a Map rather than a List. If you represent coordinates as [x, y], and the A client updates the y coordinate by deleting the y element and reinserting a new y_a, and the B client also deletes y and inserts y_b, then after merging, the array will become [x, y_a, y_b], which does not conform to the user's schema. Using a Map can prevent this problem. +For nested data, also decide how the child container should get its identity: + +- Use a Map value, such as a string or plain object, when the field should have + Last-Write-Wins replacement semantics. +- Use a regular child container with `setContainer` or `insertContainer` when + each creation should produce a distinct child object. +- Use a mergeable child container with `ensureMergeableText`, + `ensureMergeableMap`, `ensureMergeableList`, and the other + `ensureMergeable*` methods when multiple peers may lazily initialize the same + child under the same Map key and should end up editing one shared child. + +This distinction matters for dynamic keys, schema migrations, date-keyed lists, +and per-entity subdocuments. In those cases, the child identity usually should +come from the logical position `(parent Map, key, type)` rather than from the +operation that happened to create it first. + # FILE: pages/docs/concepts/version_vector.mdx @@ -6513,18 +19978,15 @@ doc.export({ mode: "update" }); // Implicit commit before export # FILE: pages/docs/api/js.mdx -import styles from './api-reference.module.css' -import Indent from './indent'; -import Method from './method'; -import CopyRawMdxButton from '@/components/CopyRawMdxButton'; +import styles from "./api-reference.module.css"; +import Indent from "./indent"; +import Method from "./method";
# API Reference -> *Last updated: 2025-08-09 loro-crdt@1.5.10* - - +> _Last updated: 2025-08-09 loro-crdt@1.5.10_ ## Overview @@ -6552,9 +20014,7 @@ Note: Under the hood, Loro combines a Fugue-based CRDT core with Eg-walker-inspi - Initialize all child containers for a LoroMap upfront when possible - Operations on the root containers will not override each other -**Events & Transactions** - -- Events emit asynchronously after a microtask in JS API +- Events emit synchronously during commit/import/checkout in JS API (v1.8+). Stay on `<=1.7.x`? Await a microtask before reading batched events. - Import/export/checkout trigger automatic commits - Loro transactions are NOT ACID - no rollback/isolation @@ -6588,6 +20048,7 @@ Note: Under the hood, Loro combines a Fugue-based CRDT core with Eg-walker-inspi - **Create rich text**: [`getText`](#LoroDoc.getText) - Initialize a collaborative text container - **Edit text**: [`insert`](#LoroText.insert), [`delete`](#LoroText.delete), [`applyDelta`](#LoroText.applyDelta) - **Apply formatting**: [`mark`](#LoroText.mark) - Add bold, italic, links, custom styles +- **Copy styled snippets**: [`sliceDelta`](#LoroText.sliceDelta) - Get a Delta for a range (UTF-16; use `sliceDeltaUtf8` for byte offsets) - **Track cursor positions**: [`getCursor`](#LoroText.getCursor) + [`getCursorPos`](#LoroDoc.getCursorPos) - Stable positions across edits - **Configure styles**: [`configTextStyle`](#LoroDoc.configTextStyle) - Define expand behavior for marks @@ -6645,12 +20106,13 @@ The `LoroDoc` class manages containers, sync, versions, and events. **Constructor** ```typescript no_run -new LoroDoc() +new LoroDoc(); ``` Creates a new Loro document with a randomly generated peer ID. **Example:** + ```ts twoslash import { LoroDoc } from "loro-crdt"; @@ -6668,11 +20130,13 @@ static fromSnapshot(snapshot: Uint8Array): LoroDoc Creates a new LoroDoc from a snapshot. This is useful for loading a document from a previously exported snapshot. **Parameters:** + - `snapshot` - Binary snapshot data **Returns:** A new LoroDoc instance **Example:** + ```ts no_run twoslash import { LoroDoc } from "loro-crdt"; @@ -6683,6 +20147,7 @@ const snapshot: Uint8Array = prevDoc.export({ mode: "snapshot" }); const doc = LoroDoc.fromSnapshot(snapshot); ``` + **Properties** @@ -6698,12 +20163,14 @@ Gets the peer ID of the current writer as a bigint. **See Also:** [PeerID Management](/docs/concepts/peerid_management) **Example:** + ```ts twoslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const id = doc.peerId; ``` + @@ -6715,12 +20182,14 @@ readonly peerIdStr: `${number}` Gets the peer ID as a decimal string. **Example:** + ```ts twoslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const idStr = doc.peerIdStr; ``` + ### Configuration Methods @@ -6731,13 +20200,15 @@ setPeerId(peer: number | bigint | `${number}`): void ``` -Sets the peer ID for this document. It must be a number, a BigInt, or a decimal string that fits into an unsigned 64-bit integer. +Sets the peer ID for this document. It must be a number, a BigInt, or a decimal string that fits into an unsigned 64-bit integer. See [PeerID Management](/docs/concepts/peerid_management) for why uniqueness matters in distributed systems. **Parameters:** + - `peer` - Peer ID as number, bigint, or decimal string **Example:** + ```ts twoslash import { LoroDoc } from "loro-crdt"; @@ -6746,12 +20217,14 @@ doc.setPeerId("42"); ``` **⚠️ Critical Pitfall:** Never let two parallel peers (e.g., multiple tabs/devices) share the same PeerID β€” it creates duplicate op IDs and causes document divergence. Common mistakes: + - Don't assign a fixed PeerId to a user (users have multiple devices) - Don't assign a fixed PeerId to a device (multiple tabs can open the same document) - If you must reuse PeerIDs, enforce single ownership with strict locking mechanisms - Best practice: Use random IDs (default behavior) unless you have a strong reason not to See [PeerID reuse](/docs/tutorial/tips) for safe reuse patterns. + @@ -6763,17 +20236,20 @@ setRecordTimestamp(auto_record: boolean): void Configures whether to automatically record timestamps for changes. Timestamps use Unix time (seconds since epoch). Learn more about storing timestamps and typical use cases in [Storing Timestamps](/docs/advanced/timestamp). **Parameters:** + - `auto_record` - Whether to automatically record timestamps **⚠️ Important:** This setting doesn't persist in exported Updates or Snapshots. You must reapply this configuration each time you initialize a document. **Example:** + ```ts twoslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.setRecordTimestamp(true); ``` + @@ -6785,15 +20261,18 @@ setChangeMergeInterval(interval: number): void Sets the interval in milliseconds for merging continuous local changes into a single change record. In Loro, multiple low-level operations are grouped into higher-level Changes for readability and syncing. See [Operations and Changes](/docs/concepts/operations_changes). **Parameters:** + - `interval` - Merge interval in milliseconds **Example:** + ```ts twoslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.setChangeMergeInterval(1000); // Merge changes within 1 second ``` + @@ -6805,16 +20284,22 @@ configTextStyle(styles: StyleConfig): void Configures the behavior of text styles (marks) in rich text containers. Marks can expand when edits happen at their edges (before/after/both/none). For a primer on rich text and marks in Loro, see [Text](/docs/tutorial/text). **Parameters:** + - `styles` - Configuration object mapping style names to their config **StyleConfig Type:** + ```typescript no_run -type StyleConfig = Record +type StyleConfig = Record< + string, + { + expand?: "after" | "before" | "both" | "none"; + } +>; ``` **Example:** + ```ts twoslash import { LoroDoc } from "loro-crdt"; @@ -6822,9 +20307,10 @@ const doc = new LoroDoc(); doc.configTextStyle({ bold: { expand: "after" }, italic: { expand: "none" }, - link: { expand: "none" } + link: { expand: "none" }, }); ``` + @@ -6836,30 +20322,41 @@ configDefaultTextStyle(style?: { expand: "after" | "before" | "both" | "none" }) Configures the default text style for the document when using LoroText. If undefined is provided, the default style is reset. **Parameters:** + - `style` - Default style configuration (optional) **Example:** + ```ts twoslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.configDefaultTextStyle({ expand: "after" }); ``` + ### Container Access Methods **πŸ“ Note:** Creating root containers (e.g., `doc.getText("...")`) does not record operations; nested container creation (e.g., `map.setContainer(...)`) does. -**⚠️ Pitfall:** Avoid concurrent creation of child containers with the same key in LoroMaps. Instead of: +**⚠️ Pitfall:** Avoid concurrent creation of regular child containers with the same key in LoroMaps. Instead of: + +```ts no_run +// Dangerous - concurrent peers can create different child container IDs +doc.getMap("user").getOrCreateContainer(userId, new LoroMap()); +``` + +Use a mergeable child container when the child should be identified by its logical position: + ```ts no_run -// Dangerous - can cause overwrites -doc.getMap("user").getOrCreateContainer(userId, new LoroMap()) +doc.getMap("user").ensureMergeableMap(userId); ``` -Use: + +Alternatively, use a unique root container if that better fits your model: + ```ts no_run -// Safe - unique root container per user -doc.getMap("user." + userId) +doc.getMap("user." + userId); ``` @@ -6872,11 +20369,13 @@ getText(name: string): LoroText Gets or creates a text container with the given name. New to LoroText and marks? See [Text](/docs/tutorial/text). **Parameters:** + - `name` - The container name **Returns:** A `LoroText` instance **Example:** + ```ts twoslash import { LoroDoc } from "loro-crdt"; @@ -6884,6 +20383,7 @@ const doc = new LoroDoc(); const text = doc.getText("content"); text.insert(0, "Hello"); ``` + @@ -6896,11 +20396,13 @@ getList(name: string): LoroList Gets or creates a list container with the given name. Unsure whether to use List or MovableList? See [List and Movable List](/docs/tutorial/list) and the type selection guide [Choosing CRDT Types](/docs/concepts/choose_crdt_type). **Parameters:** + - `name` - The container name **Returns:** A `LoroList` instance **Example:** + ```ts twoslash import { LoroDoc } from "loro-crdt"; @@ -6908,6 +20410,7 @@ const doc = new LoroDoc(); const list = doc.getList("items"); list.push("Item 1"); ``` + @@ -6919,11 +20422,13 @@ getMap(name: string): LoroMap Gets or creates a map container with the given name. See [Map](/docs/tutorial/map) for basics and patterns. **Parameters:** + - `name` - The container name **Returns:** A `LoroMap` instance **Example:** + ```ts twoslash import { LoroDoc } from "loro-crdt"; @@ -6931,6 +20436,7 @@ const doc = new LoroDoc(); const map = doc.getMap("settings"); map.set("theme", "dark"); ``` + @@ -6942,11 +20448,13 @@ getTree(name: string): LoroTree Gets or creates a tree container with the given name. Learn about hierarchical editing and moves in [Tree](/docs/tutorial/tree). **Parameters:** + - `name` - The container name **Returns:** A `LoroTree` instance **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -6954,6 +20462,7 @@ const doc = new LoroDoc(); const tree = doc.getTree("fileSystem"); const root = tree.createNode(); ``` + @@ -6965,11 +20474,13 @@ getCounter(name: string): LoroCounter Gets or creates a counter container with the given name. Counters are special CRDTs that sum concurrent increments; see [Counter](/docs/tutorial/counter). **Parameters:** + - `name` - The container name **Returns:** A `LoroCounter` instance **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -6977,6 +20488,7 @@ const doc = new LoroDoc(); const counter = doc.getCounter("likes"); counter.increment(1); ``` + @@ -6988,11 +20500,13 @@ getMovableList(name: string): LoroMovableList Gets or creates a movable list container with the given name. MovableList is designed for reordering with concurrent moves. See [List and Movable List](/docs/tutorial/list) and [Choosing CRDT Types](/docs/concepts/choose_crdt_type). **Parameters:** + - `name` - The container name **Returns:** A `LoroMovableList` instance **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7003,6 +20517,7 @@ movableList.push("Task 2"); movableList.push("Task 3"); movableList.move(0, 2); // Move first item to third position ``` + @@ -7014,11 +20529,13 @@ getContainerById(id: ContainerID): Container | undefined Gets a container by its unique ID. Container IDs (CID) uniquely reference containers across updates; see [Container ID](/docs/advanced/cid) and [Container](/docs/concepts/container). **Parameters:** + - `id` - The container ID **Returns:** The container instance or undefined if not found **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7027,6 +20544,7 @@ const text = doc.getText("text"); const textId = text.id; const sameText = doc.getContainerById(textId); ``` + ### Import/Export Methods @@ -7040,25 +20558,29 @@ export(mode?: ExportMode): Uint8Array Exports the document in various formats for synchronization or persistence. For a walkthrough of export modesβ€”snapshot, update, shallow-snapshot, and updates-in-rangeβ€”see [Export Mode](/docs/tutorial/encoding). Shallow snapshots remove history while keeping current state; see [Shallow Snapshots](/docs/concepts/shallow_snapshots). VersionVector and Frontiers are two ways to represent versions; see [Version Vector](/docs/concepts/version_vector) and [Frontiers](/docs/concepts/frontiers). **Parameters:** + - `mode` - Export configuration (optional) **ExportMode Options:** + ```typescript no_run type ExportMode = | { mode: "snapshot" } - | { mode: "update", from?: VersionVector } - | { mode: "shallow-snapshot", frontiers: Frontiers } - | { mode: "updates-in-range", spans: { id: OpId; len: number }[] } + | { mode: "update"; from?: VersionVector } + | { mode: "shallow-snapshot"; frontiers: Frontiers } + | { mode: "updates-in-range"; spans: { id: OpId; len: number }[] }; ``` **Returns:** Encoded binary data **⚠️ Important Notes:** + - **Shallow snapshots**: Cannot import updates from before the shallow start point. Peers can only sync if they have versions after this point. - **Auto-commit**: The document automatically commits pending operations before export. - **Performance**: Export new snapshots periodically to reduce import times for new peers. **Examples:** + ```typescript no_run import { LoroDoc, VersionVector } from "loro-crdt"; @@ -7073,15 +20595,16 @@ const lastSyncVersion = doc.version(); // Get current version // ... make more changes ... const updates = doc.export({ mode: "update", - from: lastSyncVersion + from: lastSyncVersion, }); // Export shallow snapshot at current version const shallowSnapshot = doc.export({ mode: "shallow-snapshot", - frontiers: doc.frontiers() + frontiers: doc.frontiers(), }); ``` + @@ -7093,11 +20616,13 @@ import(data: Uint8Array): ImportStatus Imports updates or snapshots into the document. Returns an `ImportStatus` describing which peer ranges were applied or are pending. See [Sync](/docs/tutorial/sync) and [Import Status](/docs/concepts/import_status) for how Loro handles out-of-order and partial updates. **Parameters:** + - `data` - Binary data or another LoroDoc to import from **⚠️ Important:** LoroDoc will automatically commits pending operations before import. If the doc is in detached mode, the imported operations are recorded into OpLog but not applied to DocState until you call `attach()`, see [Attached vs Detached States](/docs/concepts/attached_detached) adn [OpLog and DocState](/docs/concepts/oplog_docstate). **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7111,6 +20636,7 @@ const updates: Uint8Array = otherDoc.export({ mode: "update" }); const status = doc.import(updates); console.log(status.success); ``` + @@ -7122,9 +20648,11 @@ importBatch(data: Uint8Array[]): ImportStatus Efficiently imports multiple updates in a single batch operation. See [Batch Import](/docs/advanced/import_batch) for performance considerations and usage. **Parameters:** + - `data` - Array of binary updates **Example:** + ```typescript no_run import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -7136,6 +20664,7 @@ declare const update3: Uint8Array; const updates = [update1, update2, update3]; const status = doc.importBatch(updates); ``` + @@ -7147,12 +20676,14 @@ exportJsonUpdates(start?: VersionVector, end?: VersionVector, withPeerCompressio Exports updates in JSON format for debugging or alternative storage. See [Export Mode](/docs/tutorial/encoding) for format details and trade-offs. **Parameters:** + - `start` - Starting version (optional) - `end` - Ending version (optional) **Returns:** JSON representation of updates **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7160,6 +20691,7 @@ const doc = new LoroDoc(); const jsonUpdates = doc.exportJsonUpdates(); console.log(JSON.stringify(jsonUpdates, null, 2)); ``` + @@ -7171,9 +20703,11 @@ importJsonUpdates(json: string | JsonSchema): void Imports updates from JSON format. Useful for debugging, migration, or custom storage layers; see [Export Mode](/docs/tutorial/encoding). **Parameters:** + - `json` - JSON string or object containing updates **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7183,6 +20717,7 @@ otherDoc.getText("text").insert(0, "Hello"); const jsonStr = otherDoc.exportJsonUpdates(); doc.importJsonUpdates(jsonStr); ``` + ## Versioning @@ -7200,9 +20735,11 @@ checkout(frontiers: Frontiers): void Checks out the document to a specific version, making it read-only at that point in history. This is the core of time travel; see [Time Travel](/docs/tutorial/time_travel) and [Version](/docs/tutorial/version). **Parameters:** + - `frontiers` - Array of OpIds representing the target version **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7211,12 +20748,14 @@ const frontiers = doc.frontiers(); // Make some changes... doc.checkout(frontiers); // Go back to previous version ``` + **⚠️ Important:** In Loro 1.0, `version()`/`frontiers()` include pending (uncommitted) local operations. **πŸ“ Note:** After `checkout()`, the document enters "detached" mode and becomes read-only by default. Use `attach()` or `checkoutToLatest()` to return to editing mode. See [Version Deep Dive](/docs/advanced/version_deep_dive) and [Attached vs Detached States](/docs/concepts/attached_detached). + @@ -7228,12 +20767,14 @@ checkoutToLatest(): void Returns the document to the latest version after a checkout. Related concepts: [Frontiers](/docs/concepts/frontiers) and [Version Vector](/docs/concepts/version_vector). **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.checkoutToLatest(); ``` + @@ -7245,12 +20786,14 @@ attach(): void Attaches the document to track latest changes after being detached. See [Attached vs Detached States](/docs/concepts/attached_detached) for how Loro separates current state from history. **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.attach(); ``` + @@ -7262,12 +20805,14 @@ detach(): void Detaches the document from tracking latest changes, freezing it at current version. See [Attached vs Detached States](/docs/concepts/attached_detached). **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.detach(); ``` + @@ -7281,12 +20826,14 @@ Creates a new document that is a fork of the current one with a new peer ID. For **Returns:** A new LoroDoc instance **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const forkedDoc = doc.fork(); ``` + @@ -7298,11 +20845,13 @@ forkAt(frontiers: Frontiers): LoroDoc Creates a fork at a specific version in history. Learn more about versions, DAG history, and heads in [Version Deep Dive](/docs/advanced/version_deep_dive). **Parameters:** + - `frontiers` - The version to fork from **Returns:** A new LoroDoc instance **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7310,11 +20859,12 @@ const doc = new LoroDoc(); const frontiers = doc.frontiers(); const forkedDoc = doc.forkAt(frontiers); ``` + ## Events & Transactions -React to changes and group local operations into transactions. Events are delivered asynchronously after a microtask. See [Event Handling](/docs/tutorial/event) and [Transaction Model](/docs/concepts/transaction_model). +React to changes and group local operations into transactions. Starting in v1.8, events are delivered synchronously; older releases require awaiting a microtask. See [Event Handling](/docs/tutorial/event) and [Transaction Model](/docs/concepts/transaction_model). ### Subscription Methods @@ -7327,30 +20877,33 @@ subscribe(listener: (event: LoroEventBatch) => void): () => void Subscribes to all document changes. See [Event Handling](/docs/tutorial/event) for the event model and best practices. **Parameters:** + - `listener` - Callback function that receives change events **Returns:** Unsubscribe function **Event Structure:** + ```typescript no_run interface LoroEventBatch { - by: "local" | "import" | "checkout" - origin?: string - currentTarget?: ContainerID - events: LoroEvent[] - from: Frontiers - to: Frontiers + by: "local" | "import" | "checkout"; + origin?: string; + currentTarget?: ContainerID; + events: LoroEvent[]; + from: Frontiers; + to: Frontiers; } ``` **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const unsubscribe = doc.subscribe((event) => { console.log("Change type:", event.by); - event.events.forEach(e => { + event.events.forEach((e) => { console.log("Container changed:", e.target); console.log("Diff:", e.diff); }); @@ -7359,14 +20912,16 @@ const unsubscribe = doc.subscribe((event) => { // Later: unsubscribe(); ``` -**⚠️ Important:** Events are emitted asynchronously after a microtask. +**⚠️ Important:** Events are emitted synchronously as of v1.8. If you are pinned to `<=1.7.x`, await a microtask before reading the batch. + ```ts no_run doc.commit(); -await Promise.resolve(); -// Now events have been emitted +// Events have already been delivered in v1.8+ +// await Promise.resolve(); // Only needed on <=1.7.x ``` **πŸ“ Note:** Multiple operations before a commit are batched into a single event. See [Event Handling](/docs/tutorial/event). + @@ -7378,11 +20933,13 @@ subscribeLocalUpdates(f: (bytes: Uint8Array) => void): () => void Subscribes only to local changes, useful for syncing with remote peers. This is typically wired to your transport layer; see [Sync](/docs/tutorial/sync). **Parameters:** + - `f` - Callback that receives binary updates **Returns:** Unsubscribe function **Example:** + ```ts no_run threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -7394,6 +20951,7 @@ const unsubscribe = doc.subscribeLocalUpdates((updates) => { websocket.send(updates); }); ``` + @@ -7405,11 +20963,13 @@ subscribeFirstCommitFromPeer(f: (e: { peer: PeerID }) => void): () => void Subscribes to the first commit from each peer, useful for tracking peer metadata. **Parameters:** + - `f` - Callback that receives peer information **Returns:** Unsubscribe function **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7418,13 +20978,12 @@ doc.subscribeFirstCommitFromPeer(({ peer }) => { // Store peer metadata doc.getMap("peers").set(peer, { joinedAt: Date.now(), - name: `User ${peer}` + name: `User ${peer}`, }); }); ``` - - + ### Transaction Methods @@ -7437,6 +20996,7 @@ commit(options?: { origin?: string, message?: string, timestamp?: number }): voi Commits pending changes as a single transaction. A transaction groups operations into a Change; see [Operations and Changes](/docs/concepts/operations_changes). **⚠️ Critical Distinction:** Loro transactions are NOT ACID database transactions: + - No rollback capability - No isolation guarantees - Purpose: Bundle local operations for event batching and history grouping @@ -7445,26 +21005,30 @@ Commits pending changes as a single transaction. A transaction groups operations See [Transaction Model](/docs/concepts/transaction_model). **Parameters:** + - `options` - Optional commit configuration - `message` - Commit message (persisted in the document like a git commit message, visible to all peers after sync) - `origin` - Origin identifier (local only - used for marking local events, remote peers won't see this) - `timestamp` - Unix timestamp in seconds (see [Storing Timestamps](/docs/advanced/timestamp)) **Important distinction:** + - `message` is persisted in the document's history and will be synchronized to all peers, similar to git commit messages - `origin` is only used locally for filtering events (e.g., excluding certain origins from undo) and is NOT synchronized to remote peers **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.commit({ message: "Updated document title", // Persisted & synced to all peers - origin: "user-action", // Local only, for event filtering - timestamp: Math.floor(Date.now() / 1000) + origin: "user-action", // Local only, for event filtering + timestamp: Math.floor(Date.now() / 1000), }); ``` + ### Query Methods @@ -7480,6 +21044,7 @@ Converts the entire document to a JSON-compatible value. If you prefer a structu **Returns:** JSON representation of the document **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7487,6 +21052,7 @@ const doc = new LoroDoc(); const json = doc.toJSON(); console.log(JSON.stringify(json, null, 2)); ``` + @@ -7500,6 +21066,7 @@ Gets a shallow representation where sub-containers are represented by their IDs. **Returns:** Shallow JSON value **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7507,6 +21074,7 @@ const doc = new LoroDoc(); const shallow = doc.getShallowValue(); // Sub-containers appear as: "cid:..." ``` + @@ -7520,12 +21088,14 @@ Gets the deep value of the document with container IDs preserved. This is useful **Returns:** Document value with container IDs **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const deepValue = doc.getDeepValueWithID(); ``` + @@ -7539,6 +21109,7 @@ Gets the current version vector of the document. Version vectors track how much **Returns:** Map from PeerID to counter **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7546,6 +21117,7 @@ const doc = new LoroDoc(); const vv = doc.version(); console.log(vv.toJSON()); ``` + @@ -7563,6 +21135,7 @@ Gets the current frontiers (heads) of the document. Frontiers are a compact repr **Returns:** Array of OpIds **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7570,6 +21143,7 @@ const doc = new LoroDoc(); const frontiers = doc.frontiers(); // Can be used for checkouts or shallow snapshots ``` + @@ -7581,6 +21155,7 @@ diff(from: Frontiers, to: Frontiers, for_json?: boolean): [ContainerID, Diff | J Calculates differences between two versions. Understanding how Loro computes diffs benefits from the history DAG model; see [Version Deep Dive](/docs/advanced/version_deep_dive). **Parameters:** + - `from` - Starting frontiers - `to` - Ending frontiers - `for_json` - If true, returns JsonDiff format (default: true) @@ -7588,6 +21163,7 @@ Calculates differences between two versions. Understanding how Loro computes dif **Returns:** Array of container IDs and their diffs **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7601,6 +21177,7 @@ diffs.forEach(([containerId, diff]) => { console.log(`Container ${containerId} changed:`, diff); }); ``` + ### Pre-Commit Hook @@ -7628,6 +21205,7 @@ doc.getText("text").insert(0, "Hello"); doc.commit(); unsubscribe(); ``` + ### Cursor Utilities @@ -7652,6 +21230,7 @@ const c0 = text.getCursor(1); const pos = doc.getCursorPos(c0!); console.log(pos.offset); // 1 ``` + ### Pending Operations @@ -7674,9 +21253,11 @@ const pending = doc.getUncommittedOpsAsJson(); doc.commit(); const none = doc.getUncommittedOpsAsJson(); // undefined after commit ``` + ### Change Graph & History + These APIs traverse the history DAG of changes (ancestors/descendants, spans). If this sounds unfamiliar, start with Loro's [Versioning Deep Dive](/docs/advanced/version_deep_dive) and the [Event Graph Walker](/docs/concepts/event_graph_walker). @@ -7699,16 +21280,14 @@ doc.travelChangeAncestors(head, (change) => { return true; // continue }); ``` + -```typescript no_run -findIdSpansBetween(from: Frontiers, to: Frontiers): VersionVectorDiff -``` + ```typescript no_run findIdSpansBetween(from: Frontiers, to: Frontiers): + VersionVectorDiff ``` - -Find the op id spans that lie between two versions. - +Find the op id spans that lie between two versions. ```typescript no_run @@ -7727,18 +21306,20 @@ a.commit(); const snapshot = a.export({ mode: "snapshot" }); let printed: any; b.subscribe((e) => { - const spans = b.findIdSpansBetween(e.from, e.to); - const changes = b.exportJsonInIdSpan(spans.forward[0]); - printed = changes; +const spans = b.findIdSpansBetween(e.from, e.to); +const changes = b.exportJsonInIdSpan(spans.forward[0]); +printed = changes; }); b.import(snapshot); -``` + +```` ```typescript no_run getChangedContainersIn(id: OpId, len: number): ContainerID[] -``` +```` + Get container IDs modified in the given ID range. @@ -7752,6 +21333,7 @@ doc.commit(); const head = doc.frontiers()[0]; const containers = doc.getChangedContainersIn(head, 1); ``` + ### Revert & Apply Diff @@ -7774,6 +21356,7 @@ t.update("Hello"); doc.commit(); doc.revertTo([{ peer: "1", counter: 1 }]); ``` + @@ -7794,6 +21377,36 @@ doc1.getText("text").insert(0, "Hello"); const diff = doc1.diff([], doc1.frontiers()); doc2.applyDiff(diff); ``` + +**Workflow example (squash-like diffs):** For PR-style reviews, combine `diff` and `applyDiff` to send a compact change set between a base version and a new version. Operations that cancel out (insert + delete) are compressed away. + +```ts threeslash +import { LoroDoc } from "loro-crdt"; + +const baseDoc = new LoroDoc(); +const baseText = baseDoc.getText("text"); +baseText.insert(0, "hello world"); + +// Fork to make isolated edits +const newDoc = baseDoc.fork(); +const newText = newDoc.getText("text"); +newText.insert(0, "abc"); +newText.delete(0, 4); + +const diff = newDoc.diff(baseDoc.frontiers(), newDoc.frontiers()); +console.log(diff); +// [ +// [ +// "cid:root-text:Text", +// { type: "text", diff: [ { delete: 1 } ] } +// ] +// ] + +baseDoc.applyDiff(diff); +console.log(baseDoc.toJSON()); +// { text: "ello world" } +``` + ### Detached Editing @@ -7807,15 +21420,18 @@ setDetachedEditing(enable: boolean): void Enables or disables detached editing mode. Detached editing lets you stage edits separate from the latest head; see [Attached vs Detached States](/docs/concepts/attached_detached). **Parameters:** + - `enable` - Whether to enable detached editing **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.setDetachedEditing(true); ``` + @@ -7829,12 +21445,14 @@ Checks if detached editing mode is enabled. **Returns:** True if detached editing is enabled **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const enabled = doc.isDetachedEditingEnabled(); ``` + @@ -7848,12 +21466,14 @@ Checks if the document is currently detached. **Returns:** True if document is detached **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); console.log(doc.isDetached()); ``` + ### Commit Options Helpers @@ -7867,15 +21487,18 @@ setNextCommitMessage(msg: string): void Sets the message for the next commit. **Parameters:** + - `msg` - Commit message **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.setNextCommitMessage("User action"); ``` + @@ -7887,15 +21510,18 @@ setNextCommitOrigin(origin: string): void Sets the origin for the next commit. **Parameters:** + - `origin` - Origin identifier **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.setNextCommitOrigin("ui"); ``` + @@ -7907,15 +21533,18 @@ setNextCommitTimestamp(timestamp: number): void Sets the timestamp for the next commit. **Parameters:** + - `timestamp` - Unix timestamp in seconds **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.setNextCommitTimestamp(Math.floor(Date.now() / 1000)); ``` + @@ -7927,9 +21556,11 @@ setNextCommitOptions(options: { origin?: string, timestamp?: number, message?: s Sets multiple options for the next commit. **Parameters:** + - `options` - Commit options object **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7938,6 +21569,7 @@ doc.setNextCommitOptions({ origin: "ui", message: "batch" }); doc.getText("text").insert(0, "Hi"); doc.commit(); ``` + @@ -7949,12 +21581,14 @@ clearNextCommitOptions(): void Clears all pending commit options. **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.clearNextCommitOptions(); ``` + ### Version & Frontier Utilities @@ -7968,11 +21602,13 @@ frontiersToVV(frontiers: Frontiers): VersionVector Converts frontiers to a version vector. **Parameters:** + - `frontiers` - Frontiers to convert **Returns:** Version vector representation **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -7980,6 +21616,7 @@ const doc = new LoroDoc(); const frontiers = doc.frontiers(); const vv = doc.frontiersToVV(frontiers); ``` + @@ -7991,11 +21628,13 @@ vvToFrontiers(vv: VersionVector): Frontiers Converts a version vector to frontiers. **Parameters:** + - `vv` - Version vector to convert **Returns:** Frontiers representation **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -8003,6 +21642,7 @@ const doc = new LoroDoc(); const vv = doc.version(); const frontiers = doc.vvToFrontiers(vv); ``` + @@ -8016,12 +21656,14 @@ Gets the oplog version vector. **Returns:** Oplog version vector **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const vv = doc.oplogVersion(); ``` + @@ -8035,12 +21677,14 @@ Gets the oplog frontiers. **Returns:** Oplog frontiers **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const frontiers = doc.oplogFrontiers(); ``` + @@ -8052,11 +21696,13 @@ cmpWithFrontiers(frontiers: Frontiers): -1 | 0 | 1 Compares current document state with given frontiers. **Parameters:** + - `frontiers` - Frontiers to compare with **Returns:** -1 if behind, 0 if equal, 1 if ahead **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -8064,6 +21710,7 @@ const doc = new LoroDoc(); const frontiers = doc.frontiers(); const cmp = doc.cmpWithFrontiers(frontiers); ``` + @@ -8075,12 +21722,14 @@ cmpFrontiers(a: Frontiers, b: Frontiers): -1 | 0 | 1 | undefined Compares two frontiers. **Parameters:** + - `a` - First frontiers - `b` - Second frontiers **Returns:** -1 if a < b, 0 if equal, 1 if a > b, undefined if incomparable **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -8089,9 +21738,11 @@ const f1 = doc.frontiers(); const f2 = doc.frontiers(); const cmp = doc.cmpFrontiers(f1, f2); ``` + ### JSONPath & Path Queries + Use simple path strings and JSONPath to fetch nested values and containers. Paths are formed from root container names and keys (e.g., map/key or list/0). For container IDs, see [Container ID](/docs/advanced/cid). @@ -8103,11 +21754,13 @@ getByPath(path: string): Value | Container | undefined Gets a value or container by its path. **Parameters:** + - `path` - Path string (e.g., "map/key") **Returns:** Value or container at the path, or undefined **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8117,6 +21770,7 @@ map.set("key", 1); // Usage example: const value = doc.getByPath("map/key"); ``` + @@ -8128,11 +21782,13 @@ getPathToContainer(id: ContainerID): (string | number)[] | undefined Gets the path to a container by its ID. **Parameters:** + - `id` - Container ID **Returns:** Array representing the path, or undefined **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8140,6 +21796,7 @@ const map = doc.getMap("map"); const path = doc.getPathToContainer(map.id); ``` + @@ -8151,11 +21808,13 @@ JSONPath(jsonpath: string): any[] Queries the document using JSONPath syntax. **Parameters:** + - `jsonpath` - JSONPath query string **Returns:** Array of matching values **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8165,9 +21824,11 @@ map.set("key", 1); // Usage example: const results = doc.JSONPath("$.map"); ``` + ### Shallow Doc Utilities + These helpers relate to shallow snapshots and redaction. If you need a refresher on what β€œshallow” means, see [Shallow Snapshots](/docs/concepts/shallow_snapshots). @@ -8181,12 +21842,14 @@ Gets the version vector since which the document is shallow. **Returns:** Version vector **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const vv = doc.shallowSinceVV(); ``` + @@ -8200,12 +21863,14 @@ Gets the frontiers since which the document is shallow. **Returns:** Frontiers **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const frontiers = doc.shallowSinceFrontiers(); ``` + @@ -8219,12 +21884,14 @@ Checks if the document is shallow. **Returns:** True if document is shallow **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const shallow = doc.isShallow(); ``` + @@ -8236,9 +21903,11 @@ setHideEmptyRootContainers(hide: boolean): void Controls whether empty root containers are hidden in JSON output. **Parameters:** + - `hide` - Whether to hide empty root containers **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -8246,6 +21915,7 @@ const doc = new LoroDoc(); doc.setHideEmptyRootContainers(true); // Now empty roots are hidden in toJSON() ``` + @@ -8257,9 +21927,11 @@ deleteRootContainer(cid: ContainerID): void Deletes a root container. **Parameters:** + - `cid` - Container ID to delete **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8268,6 +21940,7 @@ const map = doc.getMap("map"); // Usage example: doc.deleteRootContainer(map.id); ``` + @@ -8279,11 +21952,13 @@ hasContainer(id: ContainerID): boolean Checks if a container exists in the document. **Parameters:** + - `id` - Container ID to check **Returns:** True if container exists **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8291,6 +21966,7 @@ const map = doc.getMap("map"); const exists = doc.hasContainer(map.id); ``` + ### JSON Serialization with Replacer @@ -8304,11 +21980,13 @@ toJsonWithReplacer(replacer: (key: string | number, value: Value | Container) => Customize JSON serialization of containers and values. **Parameters:** + - `replacer` - Function to transform values during serialization **Returns:** Customized JSON value **Example:** + ```ts threeslash import { LoroDoc, LoroText } from "loro-crdt"; const doc = new LoroDoc(); @@ -8323,6 +22001,7 @@ const json = doc.toJsonWithReplacer((key, value) => { return value; }); ``` + ### Stats & Introspection @@ -8336,12 +22015,14 @@ debugHistory(): void Prints debug information about the document history. **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.debugHistory(); ``` + @@ -8355,12 +22036,14 @@ Gets the total number of changes in the document. **Returns:** Number of changes **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const changes = doc.changeCount(); ``` + @@ -8374,12 +22057,14 @@ Gets the total number of operations in the document. **Returns:** Number of operations **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const ops = doc.opCount(); ``` + @@ -8393,12 +22078,14 @@ Gets all changes grouped by peer ID. **Returns:** Map of peer ID to changes **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const changes = doc.getAllChanges(); ``` + @@ -8410,20 +22097,23 @@ getChangeAt(id: OpId): Change Gets a specific change by operation ID. **Parameters:** + - `id` - Operation ID **Returns:** Change object **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); doc.getText("text").insert(0, "hello"); -doc.commit() +doc.commit(); const changes = doc.getAllChanges(); const change = changes.get(doc.peerIdStr)?.[0]; ``` + @@ -8435,12 +22125,14 @@ getChangeAtLamport(peer_id: string, lamport: number): Change | undefined Gets a change by peer ID and Lamport timestamp. **Parameters:** + - `peer_id` - Peer ID - `lamport` - Lamport timestamp **Returns:** Change object or undefined **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -8448,6 +22140,7 @@ const doc = new LoroDoc(); doc.getText("text").insert(0, "hello"); const change = doc.getChangeAtLamport(doc.peerIdStr, 1); ``` + @@ -8459,11 +22152,13 @@ getOpsInChange(id: OpId): any[] Gets all operations in a specific change. **Parameters:** + - `id` - Operation ID **Returns:** Array of operations **Example:** + ```typescript no_run import { LoroDoc } from "loro-crdt"; @@ -8472,6 +22167,7 @@ doc.getText("text").insert(0, "hello"); const changes = doc.getAllChanges(); const ops = doc.getOpsInChange(changes[0].id); ``` + @@ -8485,6 +22181,7 @@ Gets the number of pending operations in the current transaction. **Returns:** Number of pending operations **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -8493,6 +22190,7 @@ doc.getText("text").insert(0, "x"); console.log(doc.getPendingTxnLength()); doc.commit(); ``` + ### Import/Export Utilities @@ -8506,12 +22204,14 @@ decodeImportBlobMeta(blob: Uint8Array, check_checksum: boolean): ImportBlobMetad Decodes metadata from an import blob. **Parameters:** + - `blob` - Binary data to decode - `check_checksum` - Whether to verify checksum **Returns:** Import blob metadata **Example:** + ```ts threeslash import { LoroDoc, decodeImportBlobMeta } from "loro-crdt"; @@ -8519,6 +22219,7 @@ const doc = new LoroDoc(); const updates = doc.export({ mode: "update" }); const meta = decodeImportBlobMeta(updates, true); ``` + @@ -8532,12 +22233,14 @@ Redacts JSON updates within a specified version range. Use this to safely remove accidentally leaked sensitive content from history while preserving structure. See [Tips: Redaction](/docs/tutorial/tips). **Parameters:** + - `json` - JSON updates to redact - `version_range` - Version range for redaction **Returns:** Redacted JSON schema **Example:** + ```ts threeslash import { LoroDoc, redactJsonUpdates } from "loro-crdt"; @@ -8545,6 +22248,7 @@ const doc = new LoroDoc(); const json = doc.exportJsonUpdates(); const redacted = redactJsonUpdates(json, { [doc.peerIdStr]: [0, 999999] }); ``` + --- @@ -8563,22 +22267,25 @@ See [Text](/docs/tutorial/text), [Eg-walker](/docs/advanced/event_graph_walker), **⚠️ Critical: UTF-16 String Encoding** LoroText uses **UTF-16** encoding, matching JavaScript's native string encoding: + - All standard methods (`insert()`, `delete()`, `mark()`, `slice()`, `charAt()`) use UTF-16 code unit indices - `length` returns UTF-16 code units (same as JavaScript `string.length`) - Use `insertUtf8()` and `deleteUtf8()` for UTF-8 byte-based operations when integrating with UTF-8 systems **⚠️ Common Pitfalls:** + 1. **Index Misalignment**: UTF-16 indices differ from visual character count 2. **Performance**: Cursor queries on deleted positions require history traversal - in that case, it will return a refreshed Cursor object that does not point to the deleted text **Example with emoji:** + ```typescript no_run const text = doc.getText("text"); text.insert(0, "Hello πŸ˜€ World"); -console.log(text.length); // 13 (emoji counts as 2) +console.log(text.length); // 13 (emoji counts as 2) console.log(text.toString()[6]); // ⚠️ Invalid - splits the emoji -text.delete(6, 2); // βœ… Correct - deletes entire emoji -text.delete(6, 1); // ❌ Wrong - corrupts the emoji +text.delete(6, 2); // βœ… Correct - deletes entire emoji +text.delete(6, 1); // ❌ Wrong - corrupts the emoji // Safe iteration text.iter((char) => { @@ -8588,25 +22295,25 @@ text.iter((char) => { ``` **πŸ“ Text vs String in Maps:** + - Use `LoroText` for collaborative text editing where all concurrent edits must be preserved - Use regular strings in `LoroMap` for atomic values (URLs, IDs, hashes) where Last-Write-Wins is preferred - Example: URLs should be strings in maps, not LoroText. Otherwise, the automatically merged result may be an invalid URL - -```typescript no_run -insert(index: number, text: string): void -``` + ```typescript no_run insert(index: number, text: string): void ``` Inserts text at the specified position using UTF-16 code unit indices (same as JavaScript string indices). **Parameters:** + - `index` - UTF-16 code unit position to insert at (0-based, same as JavaScript string index) - `text` - Text to insert **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8616,22 +22323,23 @@ const text = doc.getText("text"); text.insert(0, "Hello "); text.insert(6, "World"); ``` + -```typescript no_run -delete(index: number, len: number): void -``` + ```typescript no_run delete(index: number, len: number): void ``` Deletes text from the specified position using UTF-16 code units. **Parameters:** + - `index` - Starting UTF-16 code unit position (same as JavaScript string index) - `len` - Number of UTF-16 code units to delete **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8640,6 +22348,7 @@ text.insert(0, "Hello πŸ˜€ World"); text.delete(6, 2); // Delete emoji (2 UTF-16 units) text.delete(5, 1); // Delete space before World ``` + @@ -8649,67 +22358,146 @@ mark(range: { start: number, end: number }, key: string, value: Value): void -Applies formatting to a text range. Marks can be configured to expand/stop at edges via configTextStyle(); see [Text](/docs/tutorial/text) for mark behavior. +Applies formatting to a text range. Marks can be configured to expand/stop at edges via configTextStyle(); see [Text](/docs/tutorial/text) for mark behavior. + +**Parameters:** + +- `range` - The range to format +- `key` - Style attribute name +- `value` - Style value + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); +doc.configTextStyle({ bold: { expand: "after" } }); +text.mark({ start: 0, end: 5 }, "bold", true); +``` + + + + +```typescript no_run +unmark(range: { start: number, end: number }, key: string): void +``` + + +Removes formatting from a text range. For how conflicting edits on marks resolve, see [Text](/docs/tutorial/text). + +**Parameters:** + +- `range` - The range to unformat +- `key` - Style attribute to remove + +**Example:** + +```ts threeslash +import { LoroDoc } from "loro-crdt"; +const doc = new LoroDoc(); +const text = doc.getText("text"); +text.insert(0, "Hello World"); +text.mark({ start: 0, end: 5 }, "bold", true); +text.unmark({ start: 0, end: 5 }, "bold"); +``` + + + + +```typescript no_run +toDelta(): Delta[] +``` + + +Converts text to Delta format (Quill-compatible). -**Parameters:** -- `range` - The range to format -- `key` - Style attribute name -- `value` - Style value +**Returns:** Array of Delta operations **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const text = doc.getText("text"); text.insert(0, "Hello World"); -doc.configTextStyle({ bold: { expand: "after" } }); text.mark({ start: 0, end: 5 }, "bold", true); +const delta = text.toDelta(); +// [{ insert: "Hello", attributes: { bold: true } }, { insert: " World" }] ``` + - + ```typescript no_run -unmark(range: { start: number, end: number }, key: string): void +sliceDelta(start: number, end: number): Delta[] ``` -Removes formatting from a text range. For how conflicting edits on marks resolve, see [Text](/docs/tutorial/text). +Returns a Quill-style Delta for a subsection of the text, using UTF-16 indices. Useful for copying a styled span. Use `sliceDeltaUtf8` if you need UTF-8 byte offsets instead. **Parameters:** -- `range` - The range to unformat -- `key` - Style attribute to remove + +- `start` - Start UTF-16 code unit index (inclusive) +- `end` - End UTF-16 code unit index (exclusive) **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; +import { expect } from "expect"; const doc = new LoroDoc(); +doc.configTextStyle({ + bold: { expand: "after" }, + comment: { expand: "none" }, +}); const text = doc.getText("text"); -text.insert(0, "Hello World"); + +text.insert(0, "Hello World!"); text.mark({ start: 0, end: 5 }, "bold", true); -text.unmark({ start: 0, end: 5 }, "bold"); +text.mark({ start: 6, end: 11 }, "comment", "greeting"); + +const snippet = text.sliceDelta(1, 8); +expect(snippet).toStrictEqual([ + { insert: "ello", attributes: { bold: true } }, + { insert: " " }, + { insert: "Wo", attributes: { comment: "greeting" } }, +]); ``` + - + ```typescript no_run -toDelta(): Delta[] +sliceDeltaUtf8(start: number, end: number): Delta[] ``` -Converts text to Delta format (Quill-compatible). +Returns a Quill-style Delta for a subsection of the text using **UTF-8 byte offsets**. Choose this when your offsets come from UTF-8 encoded buffers. -**Returns:** Array of Delta operations +**Parameters:** + +- `start` - Start byte offset (inclusive) +- `end` - End byte offset (exclusive) **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; +import { expect } from "expect"; const doc = new LoroDoc(); const text = doc.getText("text"); -text.insert(0, "Hello World"); -text.mark({ start: 0, end: 5 }, "bold", true); -const delta = text.toDelta(); -// [{ insert: "Hello", attributes: { bold: true } }, { insert: " World" }] +text.insert(0, "Hi πŸ‘‹"); + +const enc = new TextEncoder(); +const start = enc.encode("Hi ").length; // 3 bytes +const end = enc.encode("Hi πŸ‘‹").length; // 7 bytes + +const delta = text.sliceDeltaUtf8(start, end); +expect(delta).toStrictEqual([{ insert: "πŸ‘‹" }]); ``` + @@ -8721,18 +22509,21 @@ applyDelta(delta: Delta[]): void Applies Delta operations to the text. **Parameters:** + - `delta` - Array of Delta operations **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const text = doc.getText("text"); text.applyDelta([ { insert: "Hello", attributes: { bold: true } }, - { insert: " World" } + { insert: " World" }, ]); ``` + @@ -8744,12 +22535,14 @@ update(text: string, options?: { timeoutMs?: number; useRefinedDiff?: boolean }) Updates the current text to the target text using Myers' diff algorithm. **Parameters:** + - `text` - New text content - `options` - Update options - `timeoutMs` - Optional timeout for the diff computation - `useRefinedDiff` - Use refined diff for better quality on long texts **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8758,6 +22551,7 @@ const text = doc.getText("text"); text.insert(0, "Hello"); text.update("Hello World", { timeoutMs: 100 }); ``` + @@ -8776,6 +22570,7 @@ const text = doc.getText("text"); text.insert(0, "Line A\nLine C"); text.updateByLine("Line A\nLine B\nLine C"); ``` + @@ -8787,12 +22582,14 @@ getCursor(pos: number, side?: Side): Cursor | undefined Gets a stable cursor position that survives edits. **Parameters:** + - `pos` - Position in the text - `side` - Cursor affinity (-1, 0, or 1) **Returns:** Cursor object or undefined **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8802,6 +22599,7 @@ text.insert(0, "Hello World"); const cursor = text.getCursor(5); // Cursor remains valid even after edits ``` + @@ -8815,6 +22613,7 @@ Converts to plain text string. **Returns:** Plain text content **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8822,6 +22621,7 @@ const text = doc.getText("text"); text.insert(0, "Hello World"); const plainText = text.toString(); ``` + @@ -8833,11 +22633,13 @@ charAt(pos: number): string Gets the character at a specific UTF-16 code unit position. **Parameters:** + - `pos` - UTF-16 code unit position **Returns:** Character at position **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8845,6 +22647,7 @@ const text = doc.getText("text"); text.insert(0, "Hello"); const char = text.charAt(1); // "e" ``` + @@ -8856,22 +22659,25 @@ slice(start: number, end: number): string Extracts a section of the text using UTF-16 code unit positions. **Parameters:** + - `start` - Start UTF-16 code unit index - `end` - End UTF-16 code unit index (exclusive) **Returns:** Sliced text **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const text = doc.getText("text"); text.insert(0, "Hello πŸ˜€ World"); -const slice1 = text.slice(0, 5); // "Hello" -const slice2 = text.slice(6, 8); // "πŸ˜€" (emoji spans 6-8) +const slice1 = text.slice(0, 5); // "Hello" +const slice2 = text.slice(6, 8); // "πŸ˜€" (emoji spans 6-8) const slice3 = text.slice(9, 14); // "World" ``` + @@ -8883,6 +22689,7 @@ splice(pos: number, len: number, s: string): string Replaces text at a position with new content. **Parameters:** + - `pos` - Start position - `len` - Length to delete - `s` - String to insert @@ -8890,6 +22697,7 @@ Replaces text at a position with new content. **Returns:** Deleted text **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8899,6 +22707,7 @@ text.insert(0, "Hello World"); // Usage example: const deleted = text.splice(6, 5, "Loro"); // returns "World" ``` + @@ -8910,9 +22719,11 @@ push(s: string): void Appends text to the end of the document. **Parameters:** + - `s` - String to append **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8921,6 +22732,7 @@ const text = doc.getText("text"); text.push("Hello"); text.push(" World"); ``` + @@ -8932,9 +22744,11 @@ iter(callback: (char: string) => boolean): void Iterates over each character in the text. **Parameters:** + - `callback` - Function called for each character. Return false to stop iteration. **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8947,6 +22761,7 @@ text.iter((char) => { return true; // continue iteration }); ``` + @@ -8958,10 +22773,12 @@ insertUtf8(index: number, content: string): void Inserts text at a UTF-8 byte index position. **Parameters:** + - `index` - UTF-8 byte index - `content` - Text to insert **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8969,6 +22786,7 @@ const text = doc.getText("text"); text.insertUtf8(0, "Hello"); ``` + @@ -8980,10 +22798,12 @@ deleteUtf8(index: number, len: number): void Deletes text at a UTF-8 byte index position. **Parameters:** + - `index` - UTF-8 byte index - `len` - Number of UTF-8 bytes to delete **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -8993,6 +22813,7 @@ text.insert(0, "Hello World"); // Usage example: text.deleteUtf8(6, 5); // Delete "World" ``` + @@ -9004,11 +22825,13 @@ getEditorOf(pos: number): PeerID | undefined Gets the peer ID of who last edited the character at a position. **Parameters:** + - `pos` - Character position **Returns:** PeerID of last editor or undefined **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9018,6 +22841,7 @@ text.insert(0, "Hello"); // Usage example: const editor = text.getEditorOf(0); ``` + @@ -9031,6 +22855,7 @@ Returns the container type. **Returns:** "Text" **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9038,6 +22863,7 @@ const text = doc.getText("text"); const type = text.kind(); // "Text" ``` + @@ -9051,6 +22877,7 @@ Gets the parent container if this text is nested. **Returns:** Parent container or undefined **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9060,6 +22887,7 @@ const text = list.insertContainer(0, doc.getText("nested")); // Usage example: const parent = text.parent(); // Returns the list ``` + @@ -9073,6 +22901,7 @@ Checks if the container is attached to a document. **Returns:** True if attached **Example:** + ```ts threeslash import { LoroDoc, LoroText } from "loro-crdt"; const doc = new LoroDoc(); @@ -9080,6 +22909,7 @@ const text = new LoroText(); const attached = text.isAttached(); // false until attached to doc ``` + @@ -9093,6 +22923,7 @@ Gets the attached version of this container. **Returns:** Attached container or undefined **Example:** + ```ts threeslash import { LoroDoc, LoroText } from "loro-crdt"; const doc = new LoroDoc(); @@ -9100,6 +22931,7 @@ const text = new LoroText(); const attached = text.getAttached(); ``` + @@ -9113,6 +22945,7 @@ Checks if the container has been deleted. **Returns:** True if deleted **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9120,6 +22953,7 @@ const text = doc.getText("text"); const deleted = text.isDeleted(); ``` + @@ -9133,6 +22967,7 @@ Gets the text content without marks. **Returns:** Plain text string **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9142,6 +22977,7 @@ text.insert(0, "Hello"); // Usage example: const value = text.getShallowValue(); // "Hello" ``` + @@ -9155,6 +22991,7 @@ Converts the text to JSON representation. **Returns:** JSON value **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9164,6 +23001,7 @@ text.insert(0, "Hello"); // Usage example: const json = text.toJSON(); // "Hello" ``` + @@ -9175,6 +23013,7 @@ readonly id: ContainerID Gets the unique container ID. **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9182,6 +23021,7 @@ const text = doc.getText("text"); const containerId = text.id; ``` + @@ -9193,12 +23033,14 @@ readonly length: number Gets the length of the text in UTF-16 code units (same as JavaScript's `string.length`). **⚠️ Important:** Emoji and other characters outside the Basic Multilingual Plane count as 2 UTF-16 units. This affects all index-based operations: + ```typescript no_run text.insert(0, "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦"); // Family emoji console.log(text.length); // 11 (not 1!) - complex emoji with ZWJ sequences ``` **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9209,6 +23051,7 @@ console.log(text.length); // 5 text.insert(5, " πŸ˜€"); console.log(text.length); // 8 (space + emoji which counts as 2) ``` + ### LoroList @@ -9218,6 +23061,7 @@ by replaying only the necessary portion of history (Eg-walker-inspired). See [Li [Choosing CRDT Types](/docs/concepts/choose_crdt_type), and [Eg-walker](/docs/advanced/event_graph_walker). **⚠️ Important: List vs Map for Coordinates** + ```typescript no_run // ❌ WRONG - Don't use List for coordinates const coord = doc.getList("coord"); @@ -9241,10 +23085,12 @@ insert(pos: number, value: Value | Container): void Inserts a value at the specified position. **Parameters:** + - `pos` - Insert position - `value` - Value to insert **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9252,6 +23098,7 @@ const list = doc.getList("list"); list.insert(0, "First"); list.insert(1, { type: "object" }); ``` + @@ -9263,12 +23110,14 @@ insertContainer(pos: number, container: T): T Inserts a new container at the position. **Parameters:** + - `pos` - Insert position - `container` - Container instance **Returns:** The inserted container **Example:** + ```ts threeslash import { LoroDoc, LoroText } from "loro-crdt"; const doc = new LoroDoc(); @@ -9276,6 +23125,7 @@ const list = doc.getList("list"); const subText = list.insertContainer(0, new LoroText()); subText.insert(0, "Nested text"); ``` + @@ -9287,10 +23137,12 @@ delete(pos: number, len: number): void Deletes elements from the list. **Parameters:** + - `pos` - Starting position - `len` - Number of elements to delete **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9302,6 +23154,7 @@ list.push("d"); list.delete(1, 2); // Delete 2 elements starting at index 1 ``` + @@ -9313,15 +23166,18 @@ push(value: Value | Container): void Appends a value to the end of the list. **Parameters:** + - `value` - Value to append **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const list = doc.getList("list"); list.push("Last item"); ``` + @@ -9337,23 +23193,27 @@ const list = doc.getList("list"); list.insert(0, 1); const id0 = list.getIdAt(0); -``` + +```` ```typescript no_run pushContainer(container: T): T -``` +```` + Appends a container to the end of the list. **Parameters:** + - `container` - Container to append **Returns:** The appended container **Example:** + ```ts threeslash import { LoroDoc, LoroMap } from "loro-crdt"; const doc = new LoroDoc(); @@ -9361,6 +23221,7 @@ const list = doc.getList("list"); const map = list.pushContainer(new LoroMap()); map.set("key", "value"); ``` + @@ -9374,12 +23235,14 @@ Removes and returns the last element. **Returns:** The removed element or undefined **Example:** + ```ts no_run threeslash import { LoroList } from "loro-crdt"; declare const list: LoroList; // ---cut--- const lastItem = list.pop(); ``` + @@ -9391,11 +23254,13 @@ get(index: number): Value | Container | undefined Gets the value at the specified index. **Parameters:** + - `index` - Element index **Returns:** The value or undefined **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -9405,6 +23270,7 @@ list.push("first", "second"); const item = list.get(0); // "first" ``` + @@ -9416,12 +23282,14 @@ getCursor(pos: number, side?: Side): Cursor | undefined Gets a stable cursor for the position. **Parameters:** + - `pos` - Position in the list - `side` - Cursor affinity **Returns:** Cursor object or undefined **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; @@ -9431,6 +23299,7 @@ list.push("a", "b", "c"); const cursor = list.getCursor(2); ``` + @@ -9444,6 +23313,7 @@ Converts the list to a JavaScript array. **Returns:** Array of values **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9451,6 +23321,7 @@ const list = doc.getList("list"); list.push("a", "b", "c"); const array = list.toArray(); ``` + @@ -9462,6 +23333,7 @@ clear(): void Removes all elements from the list. **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9469,6 +23341,7 @@ const list = doc.getList("list"); list.push("a", "b", "c"); list.clear(); ``` + @@ -9482,6 +23355,7 @@ Gets the number of elements in the list. **Returns:** List length **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9491,6 +23365,7 @@ list.push("b"); list.push("c"); console.log(`List has ${list.length} items`); ``` + @@ -9504,6 +23379,7 @@ Returns the container type. **Returns:** "List" **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9511,6 +23387,7 @@ const list = doc.getList("list"); const type = list.kind(); // "List" ``` + @@ -9524,6 +23401,7 @@ Converts the list to JSON representation. **Returns:** JSON array **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9533,6 +23411,7 @@ list.push(1, 2, 3); // Usage example: const json = list.toJSON(); // [1, 2, 3] ``` + @@ -9546,6 +23425,7 @@ Gets the parent container if this list is nested. **Returns:** Parent container or undefined **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9555,6 +23435,7 @@ const list = map.setContainer("nested", doc.getList("list")); // Usage example: const parent = list.parent(); // Returns the map ``` + @@ -9568,6 +23449,7 @@ Checks if the container is attached to a document. **Returns:** True if attached **Example:** + ```ts threeslash import { LoroDoc, LoroList } from "loro-crdt"; const doc = new LoroDoc(); @@ -9575,6 +23457,7 @@ const list = new LoroList(); const attached = list.isAttached(); // false until attached to doc ``` + @@ -9588,6 +23471,7 @@ Gets the attached version of this container. **Returns:** Attached container or undefined **Example:** + ```ts threeslash import { LoroDoc, LoroList } from "loro-crdt"; const doc = new LoroDoc(); @@ -9595,6 +23479,7 @@ const list = new LoroList(); const attached = list.getAttached(); ``` + @@ -9608,6 +23493,7 @@ Checks if the container has been deleted. **Returns:** True if deleted **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9615,6 +23501,7 @@ const list = doc.getList("list"); const deleted = list.isDeleted(); ``` + @@ -9628,6 +23515,7 @@ Gets the list values with sub-containers as IDs. **Returns:** Array of values **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9637,6 +23525,7 @@ list.push(1, 2); // Usage example: const values = list.getShallowValue(); // [1, 2] ``` + @@ -9648,6 +23537,7 @@ readonly id: ContainerID Gets the unique container ID. **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9655,6 +23545,7 @@ const list = doc.getList("list"); const containerId = list.id; ``` + ### LoroMap @@ -9672,10 +23563,12 @@ Sets a key-value pair. Note: Setting a key to the same value is a no-op (no operation recorded). See [Map basics](/docs/tutorial/map). **Parameters:** + - `key` - The key - `value` - The value **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9684,6 +23577,7 @@ const map = doc.getMap("map"); map.set("name", "Alice"); map.set("age", 30); ``` + @@ -9694,15 +23588,17 @@ setContainer(key: string, container: T): T Sets a container as the value for a key. -**⚠️ Pitfall:** Concurrent child container creation at the same key on a LoroMap from multiple peers causes overwrites (different container IDs cannot auto-merge). See [Container initialization](/docs/tutorial/tips). +**⚠️ Pitfall:** Concurrent regular child container creation at the same key on a LoroMap from multiple peers causes overwrites (different container IDs cannot auto-merge). If the child should be shared by logical key, use the `ensureMergeable*` methods instead. See [Container initialization](/docs/tutorial/tips). **Parameters:** + - `key` - The key - `container` - Container instance **Returns:** The set container **Example:** + ```ts threeslash import { LoroDoc, LoroList } from "loro-crdt"; const doc = new LoroDoc(); @@ -9711,6 +23607,126 @@ const map = doc.getMap("map"); const list = map.setContainer("items", new LoroList()); list.push("item1"); ``` + + + + +```typescript no_run +ensureMergeableText(key: string): LoroText +``` + + +Ensures that `key` points to a mergeable Text child container. Peers that call this method concurrently with the same parent Map and key will address the same logical Text container instead of creating competing child container IDs. + +Use this for lazily-created text fields that should behave like one shared document for all peers. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Text container + +**Example:** + +```ts no_run +const body = doc.getMap("note").ensureMergeableText("body"); +body.insert(0, "Hello"); +``` + + + + +```typescript no_run +ensureMergeableMap(key: string): LoroMap +``` + + +Ensures that `key` points to a mergeable Map child container. Use this when a nested record, profile, settings object, or migration-created child Map may be initialized by multiple peers. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Map container + +**Example:** + +```ts no_run +const record = doc.getMap("records").ensureMergeableMap(recordId); +record.set("updated", true); +``` + + + + +```typescript no_run +ensureMergeableList(key: string): LoroList +``` + + +Ensures that `key` points to a mergeable List child container. Use this for dynamic child lists, such as one list per day, where concurrent first creation should produce one shared List. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable List container + +**Example:** + +```ts no_run +const entries = doc.getMap("days").ensureMergeableList("2026-06-08"); +entries.insert(0, "meeting notes"); +``` + + + + +```typescript no_run +ensureMergeableMovableList(key: string): LoroMovableList +``` + + +Ensures that `key` points to a mergeable MovableList child container. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable MovableList container + + + + +```typescript no_run +ensureMergeableTree(key: string): LoroTree +``` + + +Ensures that `key` points to a mergeable Tree child container. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Tree container + + + + +```typescript no_run +ensureMergeableCounter(key: string): LoroCounter +``` + + +Ensures that `key` points to a mergeable Counter child container. + +**Parameters:** + +- `key` - The Map key for the mergeable child container + +**Returns:** The mergeable Counter container + @@ -9722,11 +23738,13 @@ get(key: string): Value | Container | undefined Gets the value for a key. **Parameters:** + - `key` - The key **Returns:** The value or undefined **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9734,25 +23752,33 @@ const map = doc.getMap("map"); const name = map.get("name"); ``` + ```typescript no_run +/** + * @deprecated Prefer ensureMergeableText/Map/List/... for lazily-created Map child containers. + */ getOrCreateContainer(key: string, container: T): T ``` -Gets an existing container or creates a new one. +**Deprecated:** Prefer the `ensureMergeable*` methods for Map child containers that may be created lazily or concurrently by multiple peers. + +Gets an existing container or creates a new regular child container. -**⚠️ Pitfall:** Parallel container creation for the same key across peers causes overwrites. See [Container initialization](/docs/tutorial/tips). +**⚠️ Pitfall:** Parallel container creation for the same key across peers creates different child container IDs and can cause overwrites. This method is kept for compatibility and for cases where you explicitly want a regular child container. For shared dynamic children, use `ensureMergeableText`, `ensureMergeableMap`, `ensureMergeableList`, `ensureMergeableMovableList`, `ensureMergeableTree`, or `ensureMergeableCounter`. See [Container initialization](/docs/tutorial/tips). **Parameters:** + - `key` - The key - `container` - Container to create if not exists **Returns:** The container **Example:** + ```ts threeslash import { LoroDoc, LoroText } from "loro-crdt"; const doc = new LoroDoc(); @@ -9760,6 +23786,7 @@ const map = doc.getMap("map"); const text = map.getOrCreateContainer("description", new LoroText()); ``` + @@ -9771,9 +23798,11 @@ delete(key: string): void Removes a key-value pair. **Parameters:** + - `key` - The key to remove **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9781,6 +23810,7 @@ const map = doc.getMap("map"); map.delete("obsoleteKey"); ``` + @@ -9792,6 +23822,7 @@ clear(): void Removes all key-value pairs. **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9799,6 +23830,7 @@ const map = doc.getMap("map"); map.clear(); ``` + @@ -9812,6 +23844,7 @@ Gets all keys in the map. **Returns:** Array of keys **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9819,6 +23852,7 @@ const map = doc.getMap("map"); const allKeys = map.keys(); ``` + @@ -9832,6 +23866,7 @@ Gets all values in the map. **Returns:** Array of values **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9839,6 +23874,7 @@ const map = doc.getMap("map"); const allValues = map.values(); ``` + @@ -9852,6 +23888,7 @@ Gets all key-value pairs. **Returns:** Array of entries **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9861,6 +23898,7 @@ for (const [key, value] of map.entries()) { console.log(`${key}: ${value}`); } ``` + @@ -9869,7 +23907,7 @@ getLastEditor(key: string): PeerID | undefined ``` -```ts threeslash +```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const map = doc.getMap("map"); @@ -9878,13 +23916,15 @@ map.set("k", 1); doc.commit(); const who = map.getLastEditor("k"); // who = doc.peerIdStr -``` + +```` ```typescript no_run size: number -``` +```` + Gets the number of key-value pairs. @@ -9892,6 +23932,7 @@ Gets the number of key-value pairs. **Returns:** Map size **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9899,6 +23940,7 @@ const map = doc.getMap("map"); console.log(`Map has ${map.size} entries`); ``` + @@ -9912,6 +23954,7 @@ Returns the container type. **Returns:** "Map" **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9919,6 +23962,7 @@ const map = doc.getMap("map"); const type = map.kind(); // "Map" ``` + @@ -9932,6 +23976,7 @@ Converts the map to JSON representation. **Returns:** JSON object **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9941,6 +23986,7 @@ map.set("name", "Alice"); // Usage example: const json = map.toJSON(); // { name: "Alice" } ``` + @@ -9954,6 +24000,7 @@ Gets the parent container if this map is nested. **Returns:** Parent container or undefined **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -9963,6 +24010,7 @@ const map = list.insertContainer(0, doc.getMap("nested")); // Usage example: const parent = map.parent(); // Returns the list ``` + @@ -9976,6 +24024,7 @@ Checks if the container is attached to a document. **Returns:** True if attached **Example:** + ```ts threeslash import { LoroDoc, LoroMap } from "loro-crdt"; const doc = new LoroDoc(); @@ -9983,6 +24032,7 @@ const map = new LoroMap(); const attached = map.isAttached(); // false until attached to doc ``` + @@ -9996,6 +24046,7 @@ Gets the attached version of this container. **Returns:** Attached container or undefined **Example:** + ```ts threeslash import { LoroDoc, LoroMap } from "loro-crdt"; const doc = new LoroDoc(); @@ -10003,6 +24054,7 @@ const map = new LoroMap(); const attached = map.getAttached(); ``` + @@ -10016,6 +24068,7 @@ Checks if the container has been deleted. **Returns:** True if deleted **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10023,6 +24076,7 @@ const map = doc.getMap("map"); const deleted = map.isDeleted(); ``` + @@ -10036,6 +24090,7 @@ Gets the map values with sub-containers as IDs. **Returns:** Object with values **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10045,6 +24100,7 @@ map.set("key", "value"); // Usage example: const values = map.getShallowValue(); // { key: "value" } ``` + @@ -10056,6 +24112,7 @@ readonly id: ContainerID Gets the unique container ID. **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10063,6 +24120,7 @@ const map = doc.getMap("map"); const containerId = map.id; ``` + ### LoroTree @@ -10070,6 +24128,7 @@ const containerId = map.id; A hierarchical tree container for nested structures. Supports moving subtrees while handling concurrent edits. See [Tree](/docs/tutorial/tree). **⚠️ Important Tree Operation Notes:** + - **Concurrent moves can create cycles**: Loro detects and prevents these automatically - **Fractional indexing**: Has interleaving issues but maintains relative ordering - **Don't disable fractional index** if you need siblings to be sorted. See [Tree](/docs/tutorial/tree). @@ -10083,12 +24142,14 @@ createNode(parent?: TreeID, index?: number): LoroTreeNode Creates a new tree node. **Parameters:** + - `parent` - Parent node ID (optional, creates root if omitted) - `index` - Position among siblings (optional) **Returns:** The new node's handler **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10097,6 +24158,7 @@ const tree = doc.getTree("tree"); const root = tree.createNode(); const child = root.createNode(0); ``` + @@ -10108,11 +24170,13 @@ move(target: TreeID, parent?: TreeID, index?: number): void Moves a node to a new position. **Parameters:** + - `target` - Node to move - `parent` - New parent (undefined for root) - `index` - Position among siblings **Example:** + ```typescript no_run threeslash import { LoroDoc, TreeID } from "loro-crdt"; const doc = new LoroDoc(); @@ -10123,6 +24187,7 @@ declare const newParentId: TreeID; // Usage example: tree.move(nodeId, newParentId, 0); ``` + @@ -10134,9 +24199,11 @@ delete(target: TreeID): void Deletes a node and its descendants. **Parameters:** + - `target` - Node to delete **Example:** + ```ts threeslash import { LoroDoc, TreeID } from "loro-crdt"; const doc = new LoroDoc(); @@ -10147,6 +24214,7 @@ const nodeId = node.id; // Usage example: tree.delete(nodeId); ``` + @@ -10158,11 +24226,13 @@ getNodeByID(id: TreeID): LoroTreeNode | undefined Gets a node handler by its ID. **Parameters:** + - `id` - Node ID **Returns:** Node handler or undefined **Example:** + ```ts threeslash import { LoroDoc, TreeID } from "loro-crdt"; const doc = new LoroDoc(); @@ -10176,6 +24246,7 @@ if (node) { node.data.set("label", "New Label"); } ``` + @@ -10189,6 +24260,7 @@ Gets all nodes in the tree. **Returns:** Array of all nodes **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10196,6 +24268,7 @@ const tree = doc.getTree("tree"); const allNodes = tree.nodes(); ``` + @@ -10209,6 +24282,7 @@ Gets all root nodes. **Returns:** Array of root nodes **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10216,6 +24290,7 @@ const tree = doc.getTree("tree"); const rootNodes = tree.roots(); ``` + @@ -10223,15 +24298,17 @@ const rootNodes = tree.roots(); has(target: TreeID): boolean ``` - + Checks if a node exists. **Parameters:** + - `target` - Node ID to check **Returns:** Boolean indicating existence **Example:** + ```typescript no_run threeslash import { LoroDoc, TreeID } from "loro-crdt"; const doc = new LoroDoc(); @@ -10243,6 +24320,7 @@ if (tree.has(nodeId)) { console.log("Node exists"); } ``` + @@ -10254,11 +24332,13 @@ isNodeDeleted(target: TreeID): boolean Checks if a node has been deleted. **Parameters:** + - `target` - Node ID to check **Returns:** Boolean indicating deletion status **Example:** + ```typescript no_run threeslash import { LoroDoc, TreeID } from "loro-crdt"; const doc = new LoroDoc(); @@ -10270,6 +24350,7 @@ if (tree.isNodeDeleted(nodeId)) { console.log("Node was deleted"); } ``` + ```typescript no_run @@ -10280,9 +24361,11 @@ enableFractionalIndex(jitter: number): void Enables fractional indexing for better concurrent move performance. **Parameters:** + - `jitter` - Jitter amount for fractional indices **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10290,6 +24373,7 @@ const tree = doc.getTree("tree"); tree.enableFractionalIndex(0.001); ``` + @@ -10301,6 +24385,7 @@ disableFractionalIndex(): void Disables fractional indexing. **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10308,6 +24393,7 @@ const tree = doc.getTree("tree"); tree.disableFractionalIndex(); ``` + @@ -10321,6 +24407,7 @@ Checks if fractional indexing is enabled. **Returns:** True if enabled **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10328,6 +24415,7 @@ const tree = doc.getTree("tree"); const enabled = tree.isFractionalIndexEnabled(); ``` + @@ -10341,6 +24429,7 @@ Returns the container type. **Returns:** "Tree" **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10348,6 +24437,7 @@ const tree = doc.getTree("tree"); const type = tree.kind(); // "Tree" ``` + @@ -10361,6 +24451,7 @@ Converts the tree to JSON representation. **Returns:** JSON tree structure **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10368,6 +24459,7 @@ const tree = doc.getTree("tree"); const json = tree.toJSON(); ``` + @@ -10381,6 +24473,7 @@ Gets the parent container if this tree is nested. **Returns:** Parent container or undefined **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10390,6 +24483,7 @@ const tree = map.setContainer("tree", doc.getTree("nested")); // Usage example: const parent = tree.parent(); ``` + @@ -10403,6 +24497,7 @@ Checks if the container is attached to a document. **Returns:** True if attached **Example:** + ```ts threeslash import { LoroDoc, LoroTree } from "loro-crdt"; const doc = new LoroDoc(); @@ -10410,6 +24505,7 @@ const tree = new LoroTree(); const attached = tree.isAttached(); ``` + @@ -10423,6 +24519,7 @@ Gets the attached version of this container. **Returns:** Attached container or undefined **Example:** + ```ts threeslash import { LoroDoc, LoroTree } from "loro-crdt"; const doc = new LoroDoc(); @@ -10430,6 +24527,7 @@ const tree = new LoroTree(); const attached = tree.getAttached(); ``` + @@ -10443,6 +24541,7 @@ Checks if the container has been deleted. **Returns:** True if deleted **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10450,6 +24549,7 @@ const tree = doc.getTree("tree"); const deleted = tree.isDeleted(); ``` + @@ -10463,6 +24563,7 @@ Gets the tree values with sub-containers as IDs. **Returns:** Array of tree node values **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10470,6 +24571,7 @@ const tree = doc.getTree("tree"); const values = tree.getShallowValue(); ``` + @@ -10481,6 +24583,7 @@ readonly id: ContainerID Gets the unique container ID. **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10488,6 +24591,7 @@ const tree = doc.getTree("tree"); const containerId = tree.id; ``` + ### LoroTreeNode @@ -10503,6 +24607,7 @@ data: LoroMap A map container for storing node metadata. **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10511,6 +24616,7 @@ const node = tree.createNode(); node.data.set("title", "Node Title"); node.data.set("expanded", true); ``` + @@ -10522,11 +24628,13 @@ createNode(index?: number): LoroTreeNode Creates a child node. **Parameters:** + - `index` - Position among siblings **Returns:** New node's handler **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10534,6 +24642,7 @@ const tree = doc.getTree("tree"); const node = tree.createNode(); const childId = node.createNode(0); ``` + @@ -10545,10 +24654,12 @@ move(parent?: LoroTreeNode, index?: number): void Moves this node to a new parent. **Parameters:** + - `parent` - New parent node - `index` - Position among siblings **Example:** + ```ts threeslash import { LoroDoc, LoroTreeNode } from "loro-crdt"; const doc = new LoroDoc(); @@ -10559,6 +24670,7 @@ const parent = tree.createNode(); // Usage example: node.move(parent, 0); ``` + @@ -10570,9 +24682,11 @@ moveAfter(target: LoroTreeNode): void Moves this node after a sibling. **Parameters:** + - `target` - Sibling node **Example:** + ```ts threeslash import { LoroDoc, LoroTreeNode } from "loro-crdt"; const doc = new LoroDoc(); @@ -10583,6 +24697,7 @@ const sibling = tree.createNode(); // Usage example: node.moveAfter(sibling); ``` + @@ -10594,9 +24709,11 @@ moveBefore(target: LoroTreeNode): void Moves this node before a sibling. **Parameters:** + - `target` - Sibling node **Example:** + ```ts threeslash import { LoroDoc, LoroTreeNode } from "loro-crdt"; const doc = new LoroDoc(); @@ -10607,6 +24724,7 @@ const sibling = tree.createNode(); // Usage example: node.moveBefore(sibling); ``` + @@ -10620,6 +24738,7 @@ Gets the parent node. **Returns:** Parent node or undefined **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10629,6 +24748,7 @@ const node = tree.createNode(); // Usage example: const parentNode = node.parent(); ``` + @@ -10642,6 +24762,7 @@ Gets all child nodes. **Returns:** Array of children **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10651,6 +24772,7 @@ const node = tree.createNode(); // Usage example: const childNodes = node.children(); ``` + @@ -10664,6 +24786,7 @@ Gets the position among siblings. **Returns:** Index or undefined if root **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10673,6 +24796,7 @@ const node = tree.createNode(); // Usage example: const position = node.index(); ``` + @@ -10688,6 +24812,7 @@ Returns `undefined` for the root node. Note: the tree must be attached to the do **Returns:** Hex string or `undefined` for root **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10700,6 +24825,7 @@ const aFi = a.fractionalIndex(); const bFi = b.fractionalIndex(); // aFi < bFi, because b is inserted after a ``` + @@ -10713,6 +24839,7 @@ Returns the OpID that created this node. **Returns:** `{ peer: PeerID, counter: number }` creation identifier **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10721,6 +24848,7 @@ const node = tree.createNode(); const { peer, counter } = node.creationId(); ``` + @@ -10734,6 +24862,7 @@ Returns the peer ID that created this node (equivalent to `creationId().peer`). **Returns:** `PeerID` **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10743,6 +24872,7 @@ const node = tree.createNode(); const author = node.creator(); // author == doc.peerIdStr ``` + @@ -10756,6 +24886,7 @@ Returns the OpID of the most recent move operation for this node, or `undefined` **Returns:** Creation/move OpID or `undefined` **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10764,6 +24895,7 @@ const node = tree.createNode(); const lastMove = node.getLastMoveId(); ``` + @@ -10771,12 +24903,13 @@ const lastMove = node.getLastMoveId(); isDeleted(): boolean ``` - + Checks if this node has been deleted. **Returns:** Boolean deletion status **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10788,6 +24921,7 @@ if (node.isDeleted()) { console.log("Node is deleted"); } ``` + ### LoroCounter @@ -10803,16 +24937,19 @@ increment(value: number): void Increments the counter. **Parameters:** + - `value` - Amount to increment (default: 1) **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const counter = doc.getCounter("counter"); -counter.increment(5); // +5 +counter.increment(5); // +5 ``` + @@ -10824,16 +24961,19 @@ decrement(value: number): void Decrements the counter. **Parameters:** + - `value` - Amount to decrement (default: 1) **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); const counter = doc.getCounter("counter"); -counter.decrement(3); // -3 +counter.decrement(3); // -3 ``` + @@ -10847,6 +24987,7 @@ Gets the current counter value. **Returns:** Current numeric value **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10854,6 +24995,7 @@ const counter = doc.getCounter("counter"); console.log(`Counter value: ${counter.value}`); ``` + ### LoroMovableList @@ -10862,6 +25004,7 @@ A list optimized for move operations. Designed for frequent reordering (drag-and concurrent moves (concurrent moves resolve to one final position). See [List and Movable List](/docs/tutorial/list). **πŸ“ MovableList vs List:** + - **Use MovableList** for: Drag-and-drop UIs, sortable lists, kanban boards - **Use List** for: scenarios where the list items don't need to be moved - **Key difference**: MovableList handles concurrent moves better (no duplicates) and supports set operations, List is more efficient in general. @@ -10875,10 +25018,12 @@ move(from: number, to: number): void Moves an element from one position to another. **Parameters:** + - `from` - Source index - `to` - Target index **Example:** + ```typescript import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10891,6 +25036,7 @@ movableList.push("e"); movableList.move(0, 3); // Move first element to fourth position ``` + @@ -10898,14 +25044,16 @@ movableList.move(0, 3); // Move first element to fourth position set(pos: number, value: Value | Container): void ``` - + Replaces the value at a position. **Parameters:** + - `pos` - Position to update - `value` - New value **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -10914,6 +25062,7 @@ movableList.push("a", "b", "c"); movableList.set(0, "Updated value"); ``` + @@ -10921,16 +25070,18 @@ movableList.set(0, "Updated value"); setContainer(pos: number, container: T): T ``` - + Replaces the value with a container. **Parameters:** + - `pos` - Position to update - `container` - New container **Returns:** The set container **Example:** + ```ts threeslash import { LoroDoc, LoroText } from "loro-crdt"; const doc = new LoroDoc(); @@ -10939,6 +25090,7 @@ movableList.push("placeholder"); const text = movableList.setContainer(0, new LoroText()); ``` + ## Synchronization @@ -10985,6 +25137,7 @@ doc2.subscribeLocalUpdates((updates) => { ``` Performance tips: + - Prefer `mode: "update"` with a `VersionVector` to sync incrementally. - Use `mode: "shallow-snapshot"` when you only need current state; it strips history for faster import/load. - Loro’s LSM-based encoding and Eg-walker-inspired merge keep import/export fast, even for large histories. @@ -11010,7 +25163,7 @@ doc.subscribeLocalUpdates((updates) => { }); // Receive updates from server -ws.on('message', (data) => { +ws.on("message", (data) => { doc.import(new Uint8Array(data)); }); ``` @@ -11027,7 +25180,7 @@ const doc = new LoroDoc(); const frontiers = doc.frontiers(); const shallowSnapshot = doc.export({ mode: "shallow-snapshot", - frontiers: frontiers + frontiers: frontiers, }); // Import shallow snapshot @@ -11107,18 +25260,18 @@ console.log(`Document has ${vv.length()} peers`); ```typescript no_run interface LoroEventBatch { - by: "local" | "import" | "checkout" - origin?: string - currentTarget?: ContainerID - events: LoroEvent[] - from: Frontiers - to: Frontiers + by: "local" | "import" | "checkout"; + origin?: string; + currentTarget?: ContainerID; + events: LoroEvent[]; + from: Frontiers; + to: Frontiers; } interface LoroEvent { - target: ContainerID - diff: Diff - path: Path + target: ContainerID; + diff: Diff; + path: Path; } ``` @@ -11138,10 +25291,12 @@ type TextDiff = { Represents changes to text content using Delta format. **Properties:** + - `type` - Always "text" for text diffs - `diff` - Array of Delta operations (insert, delete, retain) **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -11151,7 +25306,7 @@ const text = doc.getText("text"); text.subscribe((e) => { for (const event of e.events) { if (event.diff.type === "text") { - event.diff.diff.forEach(delta => { + event.diff.diff.forEach((delta) => { if (delta.insert) { console.log(`Inserted: "${delta.insert}"`); } @@ -11163,6 +25318,7 @@ text.subscribe((e) => { } }); ``` + #### ListDiff @@ -11177,10 +25333,12 @@ type ListDiff = { Represents changes to list content using Delta format. **Properties:** + - `type` - Always "list" for list diffs - `diff` - Array of Delta operations on list items **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -11190,7 +25348,7 @@ const list = doc.getList("list"); list.subscribe((e) => { for (const event of e.events) { if (event.diff.type === "list") { - event.diff.diff.forEach(delta => { + event.diff.diff.forEach((delta) => { if (delta.insert) { console.log(`Inserted items:`, delta.insert); } @@ -11199,6 +25357,7 @@ list.subscribe((e) => { } }); ``` + #### MapDiff @@ -11213,10 +25372,12 @@ type MapDiff = { Represents changes to map content. **Properties:** + - `type` - Always "map" for map diffs - `updated` - Record of key-value changes (undefined means deleted) **Example:** + ```ts threeslash import { LoroDoc } from "loro-crdt"; const doc = new LoroDoc(); @@ -11237,6 +25398,7 @@ map.subscribe((e) => { } }); ``` + #### TreeDiff @@ -11249,29 +25411,30 @@ type TreeDiff = { } type TreeDiffItem = - | { - target: TreeID - action: "create" - parent: TreeID | undefined - index: number - fractionalIndex: string - } - | { - target: TreeID - action: "delete" - oldParent: TreeID | undefined - oldIndex: number - } - | { - target: TreeID - action: "move" - parent: TreeID | undefined - index: number - fractionalIndex: string - oldParent: TreeID | undefined - oldIndex: number - } -``` +| { +target: TreeID +action: "create" +parent: TreeID | undefined +index: number +fractionalIndex: string +} +| { +target: TreeID +action: "delete" +oldParent: TreeID | undefined +oldIndex: number +} +| { +target: TreeID +action: "move" +parent: TreeID | undefined +index: number +fractionalIndex: string +oldParent: TreeID | undefined +oldIndex: number +} + +```` Represents changes to tree structure. **Properties:** @@ -11309,7 +25472,8 @@ tree.subscribe((e) => { } } }); -``` +```` + ### Deep Subscription @@ -11329,7 +25493,7 @@ text.subscribe((event) => { // Subscribe with deep observation doc.subscribe((event) => { // Path shows the location of the change - event.events.forEach(e => { + event.events.forEach((e) => { console.log("Change path:", e.path); console.log("Container:", e.target); console.log("Diff:", e.diff); @@ -11348,6 +25512,7 @@ Local undo operates on your own changes without breaking collaboration. See [Und Provides local undo/redo functionality. **⚠️ Important Notes:** + - **Local-only**: UndoManager only undoes the local user's operations, not remote operations - **Origin filtering**: Use `excludeOriginPrefixes` to exclude certain operations (e.g., sync operations) from undo - **Cursor restoration**: Use `onPush`/`onPop` callbacks to save and restore cursor positions @@ -11361,6 +25526,7 @@ constructor(doc: LoroDoc, config: UndoConfig) Creates a new UndoManager instance. **Parameters:** + - `doc` - The LoroDoc to manage undo/redo for - `config` - Configuration options - `mergeInterval?` - Time in ms to merge consecutive operations (default: 1000) @@ -11370,6 +25536,7 @@ Creates a new UndoManager instance. - `onPop?` - Callback when undoing/redoing **Example:** + ```ts threeslash import { LoroDoc, UndoManager } from "loro-crdt"; @@ -11377,9 +25544,10 @@ const doc = new LoroDoc(); const undo = new UndoManager(doc, { mergeInterval: 1000, maxUndoSteps: 100, - excludeOriginPrefixes: ["sync-"] + excludeOriginPrefixes: ["sync-"], }); ``` + @@ -11393,6 +25561,7 @@ Undo the last operation. **Returns:** True if undo was successful **Example:** + ```ts threeslash import { LoroDoc, UndoManager } from "loro-crdt"; const doc = new LoroDoc(); @@ -11405,6 +25574,7 @@ doc.commit(); const success = undo.undo(); console.log(success); // true ``` + @@ -11418,6 +25588,7 @@ Redo the last undone operation. **Returns:** True if redo was successful **Example:** + ```ts threeslash import { LoroDoc, UndoManager } from "loro-crdt"; const doc = new LoroDoc(); @@ -11431,6 +25602,7 @@ undo.undo(); const success = undo.redo(); console.log(success); // true ``` + @@ -11444,6 +25616,7 @@ Check if undo is available. **Returns:** True if can undo **Example:** + ```ts threeslash import { LoroDoc, UndoManager } from "loro-crdt"; const doc = new LoroDoc(); @@ -11454,6 +25627,7 @@ if (undo.canUndo()) { undo.undo(); } ``` + @@ -11467,6 +25641,7 @@ Check if redo is available. **Returns:** True if can redo **Example:** + ```ts threeslash import { LoroDoc, UndoManager } from "loro-crdt"; const doc = new LoroDoc(); @@ -11477,6 +25652,7 @@ if (undo.canRedo()) { undo.redo(); } ``` + @@ -11490,6 +25666,7 @@ Get the peer ID of the undo manager. **Returns:** The peer ID **Example:** + ```ts threeslash import { LoroDoc, UndoManager } from "loro-crdt"; const doc = new LoroDoc(); @@ -11499,6 +25676,7 @@ const undo = new UndoManager(doc, {}); const peerId = undo.peer(); console.log(peerId); // e.g., "123456" ``` + @@ -11510,9 +25688,11 @@ setMaxUndoSteps(steps: number): void Set the maximum number of undo steps. **Parameters:** + - `steps` - Maximum number of undo steps **Example:** + ```ts threeslash import { LoroDoc, UndoManager } from "loro-crdt"; const doc = new LoroDoc(); @@ -11521,6 +25701,7 @@ const undo = new UndoManager(doc, {}); // Usage example: undo.setMaxUndoSteps(50); ``` + @@ -11532,9 +25713,11 @@ setMergeInterval(interval: number): void Set the merge interval for grouping operations. **Parameters:** + - `interval` - Merge interval in milliseconds **Example:** + ```ts threeslash import { LoroDoc, UndoManager } from "loro-crdt"; const doc = new LoroDoc(); @@ -11543,6 +25726,7 @@ const undo = new UndoManager(doc, {}); // Usage example: undo.setMergeInterval(2000); // 2 seconds ``` + @@ -11554,11 +25738,13 @@ groupStart(): void Begin a manual grouping of subsequent commits into a single undo step. **Behavior:** + - Wrap consecutive `doc.commit()` calls so they undo together - Calling `groupStart` again before `groupEnd` throws and leaves the current group unchanged - Conflicting remote imports may automatically end the group and split the undo item **Example:** + ```ts threeslash import { LoroDoc, UndoManager } from "loro-crdt"; @@ -11580,6 +25766,7 @@ undo.undo(); console.log(text.toString()); // "" ``` + @@ -11591,11 +25778,13 @@ groupEnd(): void Close the active manual group and enqueue the grouped operations as a single undo item. **Behavior:** + - Must be paired with a prior `groupStart` - Safe to call after the group was auto-closed by a conflicting remote import (becomes a no-op) - Non-conflicting remote updates from other peers remain outside the undo item but do not break the group **Example:** + ```ts threeslash import { LoroDoc, UndoManager } from "loro-crdt"; @@ -11628,6 +25817,7 @@ undo.undo(); console.log(text.toString()); // "" ``` + @@ -11639,9 +25829,11 @@ addExcludeOriginPrefix(prefix: string): void Add a prefix to exclude from undo stack. **Parameters:** + - `prefix` - Origin prefix to exclude **Example:** + ```ts threeslash import { LoroDoc, UndoManager } from "loro-crdt"; const doc = new LoroDoc(); @@ -11651,6 +25843,7 @@ const undo = new UndoManager(doc, {}); undo.addExcludeOriginPrefix("sync-"); undo.addExcludeOriginPrefix("import-"); ``` + @@ -11662,6 +25855,7 @@ clear(): void Clear the undo and redo stacks. **Example:** + ```ts threeslash import { LoroDoc, UndoManager } from "loro-crdt"; const doc = new LoroDoc(); @@ -11670,6 +25864,7 @@ const undo = new UndoManager(doc, {}); // Usage example: undo.clear(); ``` + ### Custom Undo Handlers @@ -11689,14 +25884,14 @@ const undo = new UndoManager(doc, { const cursors = saveCursorPositions(); return { value: doc.toJSON(), - cursors: cursors + cursors: cursors, }; }, onPop: (isUndo, { value, cursors }, counterRange) => { // Restore cursor positions when undoing restoreCursorPositions(cursors); - } + }, }); ``` @@ -11710,24 +25905,30 @@ Reference for core types used across the API. For conceptual background, see [Co ```typescript no_run // Peer identifier -type PeerID = `${number}` +type PeerID = `${number}`; // Container identifier type ContainerID = | `cid:root-${string}:${ContainerType}` - | `cid:${number}@${PeerID}:${ContainerType}` + | `cid:${number}@${PeerID}:${ContainerType}`; // Tree node identifier -type TreeID = `${number}@${PeerID}` +type TreeID = `${number}@${PeerID}`; // Operation identifier type OpId = { - peer: PeerID - counter: number -} + peer: PeerID; + counter: number; +}; // Container types -type ContainerType = "Text" | "Map" | "List" | "Tree" | "MovableList" | "Counter" +type ContainerType = + | "Text" + | "Map" + | "List" + | "Tree" + | "MovableList" + | "Counter"; // Value types type Value = @@ -11739,37 +25940,40 @@ type Value = | { [key: string]: Value } | Uint8Array | Value[] - | undefined + | undefined; ``` ### Version Types + Loro uses two complementary version representations: Version Vectors (per-peer counters) and Frontiers (a compact set of heads). See [Version Vector](/docs/concepts/version_vector) and [Frontiers](/docs/concepts/frontiers). For the full DAG model, see [Version Deep Dive](/docs/advanced/version_deep_dive). ```typescript no_run // Version vector class class VersionVector { - constructor(value: Map | Uint8Array | VersionVector | undefined | null) - static parseJSON(version: Map): VersionVector - toJSON(): Map - encode(): Uint8Array - static decode(bytes: Uint8Array): VersionVector - get(peer_id: number | bigint | `${number}`): number | undefined - compare(other: VersionVector): number | undefined - setEnd(id: { peer: PeerID, counter: number }): void - setLast(id: { peer: PeerID, counter: number }): void - remove(peer: PeerID): void - length(): number + constructor( + value: Map | Uint8Array | VersionVector | undefined | null, + ); + static parseJSON(version: Map): VersionVector; + toJSON(): Map; + encode(): Uint8Array; + static decode(bytes: Uint8Array): VersionVector; + get(peer_id: number | bigint | `${number}`): number | undefined; + compare(other: VersionVector): number | undefined; + setEnd(id: { peer: PeerID; counter: number }): void; + setLast(id: { peer: PeerID; counter: number }): void; + remove(peer: PeerID): void; + length(): number; } // Frontiers represent a specific version -type Frontiers = OpId[] +type Frontiers = OpId[]; // ID span for range queries type IdSpan = { - peer: PeerID - counter: number - length: number -} + peer: PeerID; + counter: number; + length: number; +}; ``` ### Change Types @@ -11777,63 +25981,65 @@ type IdSpan = { ```typescript no_run // Change metadata interface Change { - peer: PeerID - counter: number - lamport: number - length: number - timestamp: number // Unix timestamp in seconds - deps: OpId[] - message: string | undefined + peer: PeerID; + counter: number; + lamport: number; + length: number; + timestamp: number; // Unix timestamp in seconds + deps: OpId[]; + message: string | undefined; } // Change modifier for pre-commit hooks interface ChangeModifier { - setMessage(message: string): this - setTimestamp(timestamp: number): this + setMessage(message: string): this; + setTimestamp(timestamp: number): this; } ``` ### Cursor Types + Stable cursors survive concurrent edits and resolve to absolute positions on demand. See [Cursor and Stable Positions](/docs/concepts/cursor_stable_positions) and [Cursor tutorial](/docs/tutorial/cursor). ```typescript no_run // Stable position in containers class Cursor { - containerId(): ContainerID - pos(): OpId | undefined - side(): Side // -1 | 0 | 1 - encode(): Uint8Array - static decode(data: Uint8Array): Cursor + containerId(): ContainerID; + pos(): OpId | undefined; + side(): Side; // -1 | 0 | 1 + encode(): Uint8Array; + static decode(data: Uint8Array): Cursor; } // Cursor side affinity -type Side = -1 | 0 | 1 +type Side = -1 | 0 | 1; ``` ### Delta Type + Delta is a popular rich-text operation format (e.g., Quill). LoroText can export/import Delta; see [Text](/docs/tutorial/text). ```typescript no_run // Rich text delta operations type Delta = | { - insert: T - attributes?: { [key in string]: {} } - retain?: undefined - delete?: undefined + insert: T; + attributes?: { [key in string]: {} }; + retain?: undefined; + delete?: undefined; } | { - delete: number - attributes?: undefined - retain?: undefined - insert?: undefined + delete: number; + attributes?: undefined; + retain?: undefined; + insert?: undefined; } | { - retain: number - attributes?: { [key in string]: {} } - delete?: undefined - insert?: undefined - } + retain: number; + attributes?: { [key in string]: {} }; + delete?: undefined; + insert?: undefined; + }; ``` --- @@ -11853,11 +26059,13 @@ encodeFrontiers(frontiers: OpId[]): Uint8Array Encode frontiers for efficient transmission. **Parameters:** + - `frontiers` - Array of operation IDs representing frontiers **Returns:** Encoded bytes **Example:** + ```ts threeslash import { LoroDoc, encodeFrontiers } from "loro-crdt"; @@ -11866,6 +26074,7 @@ const frontiers = doc.frontiers(); const encoded = encodeFrontiers(frontiers); // Send encoded to remote peers ``` + @@ -11877,11 +26086,13 @@ decodeFrontiers(bytes: Uint8Array): OpId[] Decode frontiers from bytes. **Parameters:** + - `bytes` - Encoded frontier bytes **Returns:** Array of operation IDs **Example:** + ```typescript no_run threeslash import { decodeFrontiers } from "loro-crdt"; @@ -11889,6 +26100,7 @@ declare const encodedData: Uint8Array; const frontiers = decodeFrontiers(encodedData); console.log(frontiers); // [{ peer: "1", counter: 10 }, ...] ``` + ### Debugging @@ -11902,12 +26114,14 @@ setDebug(): void Enable debug mode for detailed logging. **Example:** + ```ts threeslash import { setDebug } from "loro-crdt"; // Enable debug logging setDebug(); ``` + @@ -11921,12 +26135,14 @@ Get the current Loro version. **Returns:** Version string **Example:** + ```ts threeslash import { LORO_VERSION } from "loro-crdt"; const version = LORO_VERSION(); console.log("Loro version:", version); ``` + ### Import Blob Metadata @@ -11940,12 +26156,14 @@ decodeImportBlobMeta(blob: Uint8Array, check_checksum: boolean): ImportBlobMetad Decode metadata from an import blob. **Parameters:** + - `blob` - The import blob bytes - `check_checksum` - Whether to verify checksum **Returns:** Import blob metadata **Example:** + ```typescript no_run threeslash import { decodeImportBlobMeta } from "loro-crdt"; @@ -11953,6 +26171,7 @@ declare const blob: Uint8Array; const metadata = decodeImportBlobMeta(blob, true); console.log("Blob metadata:", metadata); ``` + --- @@ -11962,6 +26181,7 @@ console.log("Blob metadata:", metadata); Manages ephemeral state like cursor positions and user presence. See [Ephemeral Store](/docs/tutorial/ephemeral) for concepts and usage patterns. Each entry uses timestamp-based LWW (Last-Write-Wins) for conflict resolution. **⚠️ Important:** + - EphemeralStore is a separate CRDT without history - history/operations are NOT persisted - Perfect for temporary state: cursor positions, selections, typing indicators - Each peer's state auto-expires after the timeout period @@ -11978,15 +26198,18 @@ constructor(timeout?: number) Creates a new EphemeralStore instance. **Parameters:** + - `timeout` - Duration in milliseconds. A peer's state is considered outdated if its last update is older than this timeout. Default is 30000ms (30 seconds). **Example:** + ```ts threeslash import { EphemeralStore } from "loro-crdt"; // Create ephemeral store with 30 second timeout const store = new EphemeralStore(30000); ``` + @@ -11998,10 +26221,12 @@ set(key: K, value: T[K]): void Set an ephemeral value. **Parameters:** + - `key` - The key to set - `value` - The value to store **Example:** + ```ts threeslash import { EphemeralStore } from "loro-crdt"; const store = new EphemeralStore(30000); @@ -12011,6 +26236,7 @@ store.set("cursor", { line: 10, column: 5 }); store.set("selection", { start: 0, end: 10 }); store.set("user", { name: "Alice", color: "#ff0000" }); ``` + @@ -12022,11 +26248,13 @@ get(key: K): T[K] | undefined Get an ephemeral value. **Parameters:** + - `key` - The key to get **Returns:** The stored value or undefined **Example:** + ```ts threeslash import { EphemeralStore } from "loro-crdt"; const store = new EphemeralStore(30000); @@ -12036,6 +26264,7 @@ store.set("cursor", { line: 10 }); const cursor = store.get("cursor"); console.log(cursor); // { line: 10 } ``` + @@ -12047,9 +26276,11 @@ delete(key: K): void Delete an ephemeral value. **Parameters:** + - `key` - The key to delete **Example:** + ```ts threeslash import { EphemeralStore } from "loro-crdt"; const store = new EphemeralStore(30000); @@ -12058,6 +26289,7 @@ store.set("cursor", { line: 10 }); // Usage example: store.delete("cursor"); ``` + @@ -12071,6 +26303,7 @@ Get all ephemeral states. **Returns:** All stored states **Example:** + ```ts threeslash import { EphemeralStore } from "loro-crdt"; const store = new EphemeralStore(30000); @@ -12081,6 +26314,7 @@ store.set("user", { name: "Alice" }); const allStates = store.getAllStates(); console.log(allStates); ``` + @@ -12092,11 +26326,13 @@ encode(key: K): Uint8Array Encode a specific key's state for transmission. **Parameters:** + - `key` - The key to encode **Returns:** Encoded bytes **Example:** + ```ts threeslash import { EphemeralStore } from "loro-crdt"; const store = new EphemeralStore(30000); @@ -12106,6 +26342,7 @@ store.set("cursor", { line: 10 }); const encoded = store.encode("cursor"); // Send encoded to remote peers ``` + @@ -12119,6 +26356,7 @@ Encode all states for transmission. **Returns:** Encoded bytes **Example:** + ```ts threeslash import { EphemeralStore } from "loro-crdt"; const store = new EphemeralStore(30000); @@ -12129,6 +26367,7 @@ store.set("user", { name: "Alice" }); const encoded = store.encodeAll(); // Send encoded to remote peers ``` + @@ -12140,9 +26379,11 @@ apply(bytes: Uint8Array): void Apply remote updates. **Parameters:** + - `bytes` - Encoded updates from remote peer **Example:** + ```typescript no_run threeslash import { EphemeralStore } from "loro-crdt"; const store = new EphemeralStore(30000); @@ -12151,6 +26392,7 @@ const store = new EphemeralStore(30000); declare const remoteData: Uint8Array; store.apply(remoteData); ``` + @@ -12164,6 +26406,7 @@ Get all keys in the store. **Returns:** Array of keys **Example:** + ```ts threeslash import { EphemeralStore } from "loro-crdt"; const store = new EphemeralStore(30000); @@ -12174,6 +26417,7 @@ store.set("user", { name: "Alice" }); const allKeys = store.keys(); console.log(allKeys); // ["cursor", "user"] ``` + @@ -12185,11 +26429,13 @@ subscribe(listener: EphemeralListener): () => void Subscribe to all ephemeral state changes. **Parameters:** + - `listener` - Callback function for state changes **Returns:** Unsubscribe function **Example:** + ```ts threeslash import { EphemeralStore } from "loro-crdt"; const store = new EphemeralStore(30000); @@ -12202,6 +26448,7 @@ const unsubscribe = store.subscribe((event) => { // Later, unsubscribe unsubscribe(); ``` + @@ -12213,11 +26460,13 @@ subscribeLocalUpdates(listener: EphemeralLocalListener): () => void Subscribe to local ephemeral updates for syncing to remote peers. **Parameters:** + - `listener` - Callback function that receives encoded updates **Returns:** Unsubscribe function **Example:** + ```typescript no_run threeslash import { EphemeralStore } from "loro-crdt"; const store = new EphemeralStore(30000); @@ -12233,6 +26482,7 @@ const unsubscribe = store.subscribeLocalUpdates((data) => { // Later, unsubscribe unsubscribe(); ``` + @@ -12244,6 +26494,7 @@ destroy(): void Clean up and destroy the ephemeral store. **Example:** + ```ts threeslash import { EphemeralStore } from "loro-crdt"; const store = new EphemeralStore(30000); @@ -12251,6 +26502,7 @@ const store = new EphemeralStore(30000); // Usage example: store.destroy(); ``` + ### Complete Example @@ -12345,7 +26597,7 @@ If truncated from the latest version, the result will be: # Native Benchmarks -[This native benchmark](https://github.com/https://twitter.com/zx_loro/crdt-bench-native) is based on +[This native benchmark](https://github.com/zxch3n/crdt-bench-native) is based on the Rust implementation of each crate. - Conducted on a M2 Max CPU, dated 2024-10-18. @@ -12374,7 +26626,7 @@ description: "CRDT benchmarks, comparing the performance of Loro and popular CRD > The primary role of these benchmarks should be to serve as indicators of the absence of performance pitfalls rather than as measures of which project is superior. This is because different projects consistently make different trade-offs. It is inaccurate to claim that Project A is superior to Project B simply because A performs better in certain benchmarks, while Project B may excel in other areas by a significant margin. -The benchmark can be reproduced using the [crdt-benchmarks](https://github.com/https://twitter.com/zx_loro/crdt-benchmarks) repo. +The benchmark can be reproduced using the [crdt-benchmarks](https://github.com/zxch3n/crdt-benchmarks) repo. - The benchmarks were performed on MacBook Pro M1 2020 with 16GB RAM - loro-old is the version of loro on 2023-11-10, it's compiled from @@ -12728,13 +26980,13 @@ To globally sort events, we first look at the Lamport Timestamps: smaller number #### Apply a Remote Operation -An op's safety depends on the tree's state when applied, avoiding cycles. Insertion requires evaluating the state formed by all preceding ops. For remote updates, we may need to: +An op's safety depends on the tree's state when applied, avoiding cycles. Insertion requires evaluating the state formed by all preceding ops. For remote updates, we may need to: 1. Undo recent ops -2. Insert the new op -3. Reapply undone ops +2. Insert the new op +3. Reapply undone ops -This ensures proper integration of new ops into the existing sequence. +This ensures proper integration of new ops into the existing sequence. ##### Undo Recent Ops @@ -13098,6 +27350,10 @@ console.log(docA.getText("text").toString()); // Hello!Hi! console.log(docB.getText("text").toString()); // Hello!Hi! ``` +If a nested child container may be created lazily by multiple peers under the +same Map key, use [Mergeable Containers](/blog/mergeable-containers) instead of +regular `setContainer`. +
@@ -13955,6 +28211,578 @@ By using loro-mirror to bridge CRDTs and application state consistency, and by e If this work helps you build collaborative, local‑first experiences, we’d be grateful for your sponsorship. You can support us via [GitHub Sponsors](https://github.com/sponsors/loro-dev). +# FILE: pages/blog/loro-protocol.mdx + +--- +title: "Loro Protocol" +date: 2025/10/30 +description: "The Loro Protocol multiplexes CRDT sync workloads over one WebSocket connection and ships the open-source loro-websocket, loro-adaptors, plus Rust client and server implementations that speak the same protocol." +image: "/images/blog-loro-protocol.png" +--- + +## Loro Protocol + +import Authors, { Author } from "../../components/authors"; + + + + + +![](/images/blog-loro-protocol.png) + +The open-source Loro Protocol project includes the `loro-websocket` package, the adaptor suite in `loro-adaptors`, and matching Rust client and server implementations that all interoperate on the same wire format. + +The [**Loro Protocol**](https://github.com/loro-dev/protocol) is a wire protocol designed for real-time CRDT synchronization. Learn about the design in detail [here](https://github.com/loro-dev/protocol/blob/main/protocol.md). + +It efficiently runs multiple, independent "rooms" over a single WebSocket connection. + +This allows you to synchronize your application state, such as a Loro document, ephemeral cursor positions, and end-to-end encrypted documents, over one connection. It is also compatible with Yjs. + +### Quick Start: Server & Client Example + +The protocol is implemented by the `loro-websocket` client and a minimal `SimpleServer` for testing. These components are bridged to your CRDT state using `loro-adaptors`. + +**Server** + +For development, you can run the `SimpleServer` (from `loro-websocket`) in a Node.js environment. + +```tsx +// server.ts +import { SimpleServer } from "loro-websocket/server"; + +const server = new SimpleServer({ + port: 8787, + // SimpleServer accepts hooks for authentication and data persistence: + // authenticate: async (roomId, crdt, auth) => { ... }, + // onLoadDocument: async (roomId, crdt) => { ... }, + // onSaveDocument: async (roomId, crdt, data) => { ... }, +}); + +server.start().then(() => { + console.log("SimpleServer listening on ws://localhost:8787"); +}); +``` + +**Client** + +On the client side, you connect once and then join multiple rooms using different adaptors. + +```tsx +// client.ts +import { LoroWebsocketClient } from "loro-websocket"; +import { LoroAdaptor, LoroEphemeralAdaptor } from "loro-adaptors"; + +// 1. Create and connect the client +const client = new LoroWebsocketClient({ url: "ws://localhost:8787" }); +await client.waitConnected(); +console.log("Client connected!"); + +// --- Room 1: A Loro Document (%LOR) --- +const docAdaptor = new LoroAdaptor(); +const docRoom = await client.join({ + roomId: "doc:123", + crdtAdaptor: docAdaptor, +}); + +// Local edits are now automatically synced +const text = docAdaptor.getDoc().getText("content"); +text.insert(0, "Hello, Loro!"); +docAdaptor.getDoc().commit(); + +// --- Room 2: Ephemeral Presence (%EPH) on the SAME socket --- +const ephAdaptor = new LoroEphemeralAdaptor(); +const presenceRoom = await client.join({ + roomId: "doc:123", // Can be the same room ID, but different magic bytes + crdtAdaptor: ephAdaptor, +}); + +// Ephemeral state syncs, but is not persisted by the server +ephAdaptor.getStore().set("cursor", { x: 100, y: 100 }); +``` + +--- + +### Features + +#### Multiplexing + +Each binary message is prefixed with four magic bytes that identify the data type, followed by the `roomId`. This structure allows the server to route messages to the correct handler. A single client can join: + +- `%LOR` (Loro Document) +- `%EPH` (Loro Ephemeral Store, for cursors and presence) +- `%ELO` (End-to-End Encrypted Loro Document) +- `%YJS` and `%YAW` (for Yjs Document and Awareness interoperability) + +All traffic runs on the same socket. + +#### Compatibility + +The Loro Protocol is designed to accommodate environments like Cloudflare: + +- Fragmentation: Large updates are automatically split into fragments under 256 KiB and reassembled by the receiver. This addresses platforms that enforce WebSocket message size limits. +- Application-level keepalive: The protocol defines simple `"ping"` and `"pong"` text frames. These bypass the binary envelope and allow the client to check connection liveness, which is useful in browser or serverless environments where transport-level TCP keepalives are not exposed. + +This repository also ships Rust clients and servers that mirror the TypeScript packages. + +#### Experimental E2E Encryption + +End-to-end encrypted Loro is included in `loro-protocol`, but the feature is currently experimental: expect wire formats and key-management APIs to change, and do not rely on it for production-grade security audits yet. When paired with `EloLoroAdaptor` on the client, the server relays encrypted records without decrypting them. + +### Status and Licensing + +The Loro Protocol is mostly stable. We welcome community feedback and contributions, especially regarding use cases that are difficult to satisfy with the current design. + +All the packages in inside https://github.com/loro-dev/protocol are open-sourced under the permissive MIT license. + + +# FILE: pages/blog/mergeable-containers.mdx + +--- +title: "Mergeable Containers: Fixing Concurrent Child Creation" +date: 2026/06/09 +description: "Mergeable Containers let Loro peers concurrently create the same child container under a Map key and still merge into one shared child, by deriving identity from the logical parent/key/type instead of the creation OpID." +image: "/images/blog-mergeable-containers.png" +--- + +# Mergeable Containers: Fixing Concurrent Child Creation + +import Authors, { Author } from "../../components/authors"; + + + + + +![Mergeable Containers overview](/images/blog-mergeable-containers.png) + +Two users are offline. Both add content to the same empty note. They come back online, sync finishes, and one user's edits seem to disappear. + +There is no error, and the data is not actually gone from history. But `note.get("body")` can only return one Text container. The other container was created concurrently and still exists in history, but it is no longer visible in the current document state. From the application's point of view, this looks like data loss. + +This is a classic problem in JSON-like CRDTs. Users have run into versions of it in the Loro, Yjs, and Automerge communities. The [Appendix](#appendix-runnable-reproductions) has short scripts that reproduce it in all three. + +Loro now solves this with Mergeable Containers. They make a child container's identity come from its logical position in the `Map`, not from the ID of the operation that happened to create it. + +Special thanks to [Alexis Williams](https://github.com/typedrat) from [Synapdeck](https://synapdeck.com/) for the substantial implementation work and design discussion behind this feature. + +From the user's point of view, the API change is small. Instead of creating an on-demand child container like this: + +```ts no_run +// Peer A +doc.getMap("days").setContainer("2026-06-08", new LoroList()).insert(0, "A"); + +// Peer B, offline +doc.getMap("days").setContainer("2026-06-08", new LoroList()).insert(0, "B"); + +// after sync: only one List is visible at "2026-06-08" +``` + +you can use a mergeable child: + +```ts no_run +// Peer A +doc.getMap("days").ensureMergeableList("2026-06-08").insert(0, "A"); + +// Peer B, offline +doc.getMap("days").ensureMergeableList("2026-06-08").insert(0, "B"); + +// after sync: both peers edit the same List +``` + +As a rule of thumb, use `ensureMergeable*` when a child container should be identified by its logical position: + +```ts no_run +map.ensureMergeableText(key); +map.ensureMergeableMap(key); +map.ensureMergeableList(key); +map.ensureMergeableMovableList(key); +map.ensureMergeableTree(key); +map.ensureMergeableCounter(key); +``` + +Use them for fields that should behave like one shared child container for everyone: one shared Text, one shared List, one shared Map, and so on. It should not matter which peer creates that child first. The rest of this post walks through why the problem exists and how the new encoding works. + +## Why This Happens + +CRDTs are usually good at cases like "multiple users editing the same text at the same time" or "multiple users inserting into the same list concurrently." This issue happens one layer earlier: before the peers can edit the same List, Text, or Map, they first need to agree on which child container that key refers to. + +Before Mergeable Containers, the recommended workaround was to initialize all required child containers as soon as the parent `LoroMap` was created. For example, if every note always needs a `body` text, creating that `body` together with the note avoids the first-creation race. + +That workaround is useful, but it has limits. Some applications cannot know every child container ahead of time. A schema migration may add a new child container to existing documents. A calendar-like document may create child containers by date. A dynamic index may create one child container per user-defined key. In these cases, on-demand creation is natural, and concurrent first creation is hard to avoid. + +The root cause is the way regular child Container IDs are represented. A normal child Container ID includes the `OpID` that created it. Concurrent first creation therefore creates different Container IDs, and the Map conflict-resolution rule decides which one is visible. + +The issue is not that List insertion cannot merge. Once both peers are editing the same List, List edits merge normally. The issue is that the two peers created two different Lists at the same Map key. + +## Why Root Containers Are Naturally Mergeable + +In Loro and Yjs, top-level Root Containers are usually accessed by name: + +```ts no_run +doc.getMap("state"); +doc.getText("content"); +``` + +Here, `"state"` or `"content"` is already a stable identity. It does not depend on which peer created it or which operation created it. As long as multiple peers access the same root name, they naturally refer to the same logical Container. + +> Automerge has a different object identity model, so this root-container comparison is specifically about Loro and Yjs. The broader issue is still similar: when composite values are created concurrently at the same key, the system needs a rule for which object identity becomes visible. + +Regular child Containers are different. Their identity is tied to the operation that created them, so two concurrent "first creations" become two different objects. + +Mergeable Containers bring the useful part of Root Container identity to selected child Containers: the child identity comes from a deterministic name, not from the creation operation. + +## API: Explicitly Ensuring a Mergeable Child + +This feature does not change the existing `setContainer` / `insertContainer` behavior. It adds explicit `ensureMergeable*` APIs for the mergeable case. In Rust, the same methods use snake case: + +```rust +map.ensure_mergeable_text("body")?; +map.ensure_mergeable_map("profile")?; +``` + +The word `ensure` is intentional. It returns the child and, if needed, writes the marker that makes it visible at that key. Calling the same method again for the same type is idempotent. + +If the key already holds a regular scalar value or a regular child Container, the API returns an error instead of silently overwriting it. + +One subtle case is type changes. If one peer asks for a mergeable Text at `"field"` while another peer asks for a mergeable Map at the same key, Loro still needs one visible value at that key. The Map's normal conflict rule decides which type is visible. The non-visible mergeable child's state is still preserved under its deterministic ID, so switching back to that type can resurface it later. + +## Core Design: Deterministic CID + Map Slot Marker + +Mergeable Containers have two separate layers of representation: + +1. The child Container ID derived from the parent Container ID, key, and type. This decides whether peers address the same CRDT object. +2. The parent Map slot. This decides whether that object is currently visible at a key, and which mergeable child type is active there. + +Keeping these two layers separate makes the behavior easier to reason about. + +## 1. CID: A Synthetic Root Container ID + +A Mergeable Container uses a synthetic `ContainerID::Root` under an internal namespace. User-created root names cannot use this prefix, so ordinary roots cannot collide with mergeable CIDs: + +```text +🀝: +``` + +The payload is derived from the parent Map and the key. The Container type stays in `ContainerID::Root.container_type`, just like ordinary Root Containers. This lets all peers derive the same child ID without using the creation `OpID`. + +The current encoding keeps nested mergeable Map IDs linear in the logical path length. This change was made before release to avoid recursive CID growth for deeply nested mergeable maps. + +
+More details: the flattened CID encoding + +After [PR #1002](https://github.com/loro-dev/loro/pull/1002), the payload no longer recursively embeds the full parent CID. Instead, it uses a flattened path: + +```text +payload = base-parent ">" key-1 ">" key-2 ... +``` + +The `base-parent` is the nearest non-mergeable Map ancestor: + +```text +$ +@: +``` + +For example: + +```text +Root map "state", key "note-1", child map: +🀝:$state>note-1 type = Map + +Nested key "body" under that mergeable map, child text: +🀝:$state>note-1>body type = Text +``` + +Parsing the second CID gives: + +```text +parent = Root("🀝:$state>note-1", Map) +key = "body" +type = Text +``` + +
+ +## 2. Map Slot: A Binary Marker Controls Visibility + +A deterministic CID alone is not enough because Loro has multiple Container types. If one peer calls `ensureMergeableText("field")` while another peer concurrently calls `ensureMergeableMap("field")`, both deterministic child CIDs can exist. The parent Map still needs to decide which type is currently visible at `"field"`. That decision needs to be deterministic and reversible: switching the visible type should not destroy the state of the other mergeable child. + +So Loro stores a small activation marker in the parent Map slot. Its meaning is: + +```text +At this key of this parent Map, activate a mergeable child of this type. +``` + +When a new Loro client reads the slot, it uses the current `parent id + key + kind` to derive the deterministic mergeable CID, then presents it through the public API as a normal Container: + +```ts no_run +const body = map.get("body"); +// body is a LoroText, not the internal binary marker +``` + +When the key is deleted, only the marker is removed. The mergeable child state is not immediately destroyed, because the parent slot controls visibility rather than the child's stored history. Calling this again: + +```ts no_run +map.ensureMergeableText("body"); +``` + +resurfaces the same deterministic Text Container. + +The marker is also bound to its exact parent, key, and type. That keeps it from accidentally activating a mergeable child if the same binary value is copied somewhere else. + +
+More details: the binary marker format + +The marker is a compact binary value: + +```text +MAGIC[4] + KIND[1] + DIGEST[3] +``` + +`DIGEST` is the low 24 bits of CRC32 over `(parent_id, key, kind)`. So the marker is not a magic value that can be copied anywhere. + +If a user copies the marker binary from one key to another key, or from one parent Map to another, new Loro clients will not recognize it as a valid mergeable child marker. It remains an ordinary binary value. + +This matters because `LoroValue::Binary` is still valid user data. Without binding the marker to parent, key, and type, copying a binary value could accidentally activate a mergeable Container somewhere else. + +### Why Not Use a Reserved Keyword? + +One possible approach would be to store a special string or JSON object: + +```json +{ "__loro_mergeable_container__": "Text" } +``` + +or: + +```text +"__loro_mergeable_text__" +``` + +But that would take over part of the user data space. `LoroMap` is a general-purpose Map, and users may legitimately store such strings or objects. Reserved keywords would make ordinary user values suddenly have special meaning. + +They are also hard to bind safely to parent, key, and type. If a string marker is copied somewhere else, it still looks like a marker. Avoiding accidental activation would require extra validation fields, which would make the format longer and more fragile. + +A binary marker fits this role better: it is low-level structural metadata, not business data. Older clients that do not understand Mergeable Containers see it as an ordinary binary value, rather than misinterpreting it as a child Container reference. + +### Why Not Store the Full ContainerID in the Slot? + +Another possible design would be to store the full deterministic ContainerID directly in the parent Map slot. + +The problem is that older clients may interpret it as a regular child Container edge. That would give them the wrong view of the document structure. + +Mergeable Containers need more than "a pointer to a Container." The design also needs to preserve these rules: + +- The same `(parent, key, type)` deterministically produces the same CID. +- Deleting the key hides the child, but does not delete the child state. +- Conflicts between different mergeable child types still use the Map's normal LWW rule. +- The marker must only activate at the correct parent/key/type. +- Older clients must not mistake it for a normal child Container edge. + +The marker is better understood as an activation marker. New clients derive the actual child CID from the surrounding context. + +
+ +## What This Solves for Users + +Mergeable Containers are especially useful when eager initialization is not practical. + +For example, suppose an application stores one child List per date: + +```ts no_run +const days = doc.getMap("days"); +const entries = days.ensureMergeableList("2026-06-08"); +entries.insert(0, "meeting notes"); +``` + +Or suppose a schema migration lazily adds a new child Map to existing records: + +```ts no_run +const record = doc.getMap("records").ensureMergeableMap(recordId); +const metadata = record.ensureMergeableMap("metadata_v2"); +metadata.set("migrated", true); +``` + +In both cases, the child container identity no longer depends on which peer created it first. It depends on the logical position in the document structure. + +This makes Mergeable Containers especially useful for: + +- date-keyed child lists or maps +- schema migrations that add new child containers lazily +- dynamic per-user or per-entity subdocuments +- revision counters +- settings maps whose keys are discovered over time + +## Cost and Compatibility + +Mergeable Containers have some metadata cost. Their CIDs carry logical path information, so deeper paths and longer keys produce larger IDs. [PR #1002](https://github.com/loro-dev/loro/pull/1002) changed the encoding so nested mergeable Map IDs grow linearly instead of recursively, but very deep mergeable Map chains are still better to avoid. + +The compatibility story is intentionally conservative: + +- Existing `setContainer` / `insertContainer` behavior is unchanged. +- Existing documents can be read normally by new versions. +- Mergeable Containers are introduced through new APIs, without changing existing method signatures. +- Older clients that do not understand this feature see the parent slot marker as an ordinary binary value, not as a fake child Container edge. They can preserve and sync the data, but they will not display the mergeable child with the new semantics. +- User-created root names that start with the internal `🀝:` prefix are rejected by Loro's root-name validator, so they cannot collide with mergeable CIDs. + +## Summary + +Mergeable Containers are for child Containers whose identity should come from their logical position, not from whichever peer created them first. + +Use `ensureMergeable*` when: + +- the key is dynamic or lazily created +- different peers may initialize the same child while offline +- the child should behave like one shared Text, List, Map, Tree, or Counter +- deleting the key should hide the child without treating its internal history as immediately destroyed + +Keep using `setContainer` / `insertContainer` when: + +- each creation should produce a distinct child object +- the parent slot should point to exactly the Container created by that operation +- you are modeling replacement rather than shared initialization + +The short version: if two peers creating the same child at the same Map key should mean "we both found the same child," use a Mergeable Container. + +References: + +- Loro background: [issue #759](https://github.com/loro-dev/loro/issues/759) +- Loro implementation: [PR #991](https://github.com/loro-dev/loro/pull/991), [PR #1002](https://github.com/loro-dev/loro/pull/1002) +- Related Yjs discussions: [complex diagram page](https://discuss.yjs.dev/t/how-would-you-model-a-complex-diagram-page/2114), [losing data](https://discuss.yjs.dev/t/why-am-i-losing-data/2734), [nested `Y.Map`](https://discuss.yjs.dev/t/create-y-map-is-empty/1701) +- Related Automerge discussions: [#528: failing merge for text values](https://github.com/automerge/automerge/issues/528) is the closest match; [#526: conflict resolution for replaced arrays and objects](https://github.com/automerge/automerge/issues/526) is useful background on object identity and conflict handling; the historical [automerge-classic #4](https://github.com/automerge/automerge-classic/issues/4) also covers concurrently created objects under the same key. + +## Appendix: Runnable Reproductions + +The snippets below are self-contained and run directly on Node (tested on Node 24; any Node 18+ with ESM works). Install the three libraries once: + +```bash +npm install loro-crdt@^1.13 yjs @automerge/automerge +``` + +Save each block as a `.mjs` file and run it with `node file.mjs`. They all model the same scenario from this post: two offline peers concurrently create a child container under the same `Map` key, then sync. The Loro example also shows the `ensureMergeable*` fix. + +### Loro β€” the bug, and the fix + +```js no_run +// loro.mjs β€” node loro.mjs +// In plain Node import from "loro-crdt/nodejs"; the bare "loro-crdt" entry +// targets a bundler. With Vite/webpack, import from "loro-crdt" instead. +import { LoroDoc, LoroList } from "loro-crdt/nodejs"; + +function sync(a, b) { + const va = a.export({ mode: "update" }); + const vb = b.export({ mode: "update" }); + a.import(vb); + b.import(va); +} + +// 1. The bug: concurrent setContainer at the same key +{ + const a = new LoroDoc(); + const b = new LoroDoc(); + a.getMap("days").setContainer("2026-06-08", new LoroList()).insert(0, "A"); + b.getMap("days").setContainer("2026-06-08", new LoroList()).insert(0, "B"); + sync(a, b); + console.log( + "setContainer ->", + JSON.stringify(a.getMap("days").get("2026-06-08").toArray()), + ); + // -> ["A"] or ["B"], never both: only one peer's List survives. + // (which one wins depends on the randomly-assigned peer IDs) +} + +// 2. The fix: concurrent ensureMergeableList at the same key +{ + const a = new LoroDoc(); + const b = new LoroDoc(); + a.getMap("days").ensureMergeableList("2026-06-08").insert(0, "A"); + b.getMap("days").ensureMergeableList("2026-06-08").insert(0, "B"); + sync(a, b); + console.log( + "ensureMergeable ->", + JSON.stringify(a.getMap("days").get("2026-06-08").toArray()), + ); + // -> both entries, e.g. ["A","B"] (order may vary): both peers share one List. +} +``` + +### Yjs β€” the same problem + +```js no_run +// yjs.mjs β€” node yjs.mjs +import * as Y from "yjs"; + +const a = new Y.Doc(); +const b = new Y.Doc(); + +// Peer A and Peer B each create a Y.Array at the same key, offline. +{ + const l = new Y.Array(); + a.getMap("days").set("2026-06-08", l); + l.insert(0, ["A"]); +} +{ + const l = new Y.Array(); + b.getMap("days").set("2026-06-08", l); + l.insert(0, ["B"]); +} + +// Sync both ways. +Y.applyUpdate(a, Y.encodeStateAsUpdate(b)); +Y.applyUpdate(b, Y.encodeStateAsUpdate(a)); + +console.log( + "yjs ->", + JSON.stringify(a.getMap("days").get("2026-06-08").toArray()), +); +// -> ["A"] or ["B"], never both: one peer's child Y.Array wins, the other is dropped. +``` + +### Automerge β€” the same problem + +```js no_run +// automerge.mjs β€” node automerge.mjs +import * as A from "@automerge/automerge"; + +let base = A.from({ days: {} }); +let a = A.clone(base); +let b = A.clone(base); + +// Peer A and Peer B each create a list at the same key, offline. +a = A.change(a, (d) => { + d.days["2026-06-08"] = ["A"]; +}); +b = A.change(b, (d) => { + d.days["2026-06-08"] = ["B"]; +}); + +let merged = A.merge(A.clone(a), b); +console.log("automerge visible ->", JSON.stringify(merged.days["2026-06-08"])); +// -> ["A"] or ["B"], never both: one list wins. +console.log( + "automerge conflicts ->", + JSON.stringify(A.getConflicts(merged.days, "2026-06-08")), +); +// -> both lists keyed by op id: the losing list is retained but hidden, +// reachable only via getConflicts(). + +// Control: when the child is created ONCE up front, concurrent edits merge. +let shared = A.from({ days: { "2026-06-08": [] } }); +let c = A.clone(shared), + d = A.clone(shared); +c = A.change(c, (x) => { + x.days["2026-06-08"].push("A"); +}); +d = A.change(d, (x) => { + x.days["2026-06-08"].push("B"); +}); +let ok = A.merge(A.clone(c), d); +console.log("automerge pre-created ->", JSON.stringify(ok.days["2026-06-08"])); +// -> ["A","B"] (order may vary): both survive β€” this is the eager-init workaround. +``` + +Note one difference worth calling out: in Automerge the losing child is retained and can be recovered through `getConflicts()`, while Yjs overwrites the map key and drops the losing child outright. Either way, from the application's point of view it looks like data loss β€” which is exactly what Mergeable Containers avoid. + + # FILE: pages/blog/loro-now-open-source.mdx --- diff --git a/public/llms.txt b/public/llms.txt index e712276..8212179 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -18,6 +18,7 @@ Loro provides JSON-compatible data structures with built-in version control, tim - **Automatic Conflict Resolution**: All concurrent edits converge to the same state using CRDTs - **Data Types**: Map, List, MovableList, Text (rich/plain), Tree, Counter +- **Mergeable Containers**: `ensureMergeable*` APIs let peers lazily create the same child container under a Map key and still edit one shared child - **Version Control**: Git-like checkout, fork, and merge operations - **Time Travel**: Navigate to any point in document history - **Rich Text**: Full support for overlapping styles and editor bindings (ProseMirror, CodeMirror, Lexical) @@ -58,6 +59,7 @@ Loro provides JSON-compatible data structures with built-in version control, tim - [CRDT Introduction](https://loro.dev/docs/concepts/crdt): Understanding Conflict-free Replicated Data Types - [When Not to Use CRDTs](https://loro.dev/docs/concepts/when_not_crdt): Understanding CRDT limitations - [Choosing CRDT Types](https://loro.dev/docs/concepts/choose_crdt_type): Selecting the right container for your use case +- [Mergeable Containers](https://loro.dev/blog/mergeable-containers): Avoid hidden child containers when peers concurrently initialize the same Map child ### Architecture - [Containers](https://loro.dev/docs/concepts/container): Building blocks for organizing data