DocsAPI ReferenceJavaScript API

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 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) 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 & Transactions

  • Events emit asynchronously after a microtask in JS API
  • 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 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

Real-time Collaboration

  • Sync between peers: export with mode: "update" + import/importBatch - Exchange incremental updates
  • Stream updates: subscribeLocalUpdates - Send changes over WebSocket/WebRTC
  • Set unique peer ID: setPeerId - Ensure each client has a unique identifier
  • Handle conflicts: Automatic - All Loro data types are CRDTs that merge concurrent edits

Rich Text Editing

Data Structures

Ephemeral State & Presence

  • User presence: EphemeralStore - Share cursor positions, selections, user status (not persisted)
  • Cursor syncing: Use EphemeralStore.set with cursor data from 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

Performance & Storage

  • Incremental updates: export from specific version - Send only changes
  • Compact history: export with mode: "snapshot" - Full state with compressed history
  • Shallow snapshots: export with mode: "shallow-snapshot" - State without partial history (see Shallow Snapshots)

Basic Usage

import {  } from "loro-crdt";
 
const  = new ();
const  = .("text");
.(0, "Hello World");
 
// Subscribe to changes
const  = .(() => {
  .("Document changed:", );
});
 
// Export updates for synchronization
const  = .({ : "update" });

LoroDoc

The LoroDoc class manages containers, sync, versions, and events.

Constructor

new LoroDoc()

Creates a new Loro document with a randomly generated peer ID.

Example:

import {  } from "loro-crdt";
 
const  = new ();

Static Methods

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:

import {  } from "loro-crdt";
 
// Assume we have a snapshot from a previous export
const  = new ();
.("text").(0, "Hello");
const :  = .({ : "snapshot" });
 
const  = .();

Properties

readonly peerId: bigint

Gets the peer ID of the current writer as a bigint.

See Also: PeerID Management

Example:

import {  } from "loro-crdt";
 
const  = new ();
const  = .;
readonly peerIdStr: `${number}`

Gets the peer ID as a decimal string.

Example:

import {  } from "loro-crdt";
 
const  = new ();
const  = .;

Configuration Methods

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 for why uniqueness matters in distributed systems.

Parameters:

  • peer - Peer ID as number, bigint, or decimal string

Example:

import {  } from "loro-crdt";
 
const  = new ();
.("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 for safe reuse patterns.

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.

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:

import {  } from "loro-crdt";
 
const  = new ();
.(true);
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.

Parameters:

  • interval - Merge interval in milliseconds

Example:

import {  } from "loro-crdt";
 
const  = new ();
.(1000); // Merge changes within 1 second
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.

Parameters:

  • styles - Configuration object mapping style names to their config

StyleConfig Type:

type StyleConfig = Record<string, {
  expand?: "after" | "before" | "both" | "none"
}>

Example:

import {  } from "loro-crdt";
 
const  = new ();
.({
  : { : "after" },
  : { : "none" },
  : { : "none" }
});
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:

import {  } from "loro-crdt";
 
const  = new ();
.({ : "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:

// Dangerous - can cause overwrites
doc.getMap("user").getOrCreateContainer(userId, new LoroMap())

Use:

// Safe - unique root container per user
doc.getMap("user." + userId)
getText(name: string): LoroText

Gets or creates a text container with the given name. New to LoroText and marks? See Text.

Parameters:

  • name - The container name

Returns: A LoroText instance

Example:

import {  } from "loro-crdt";
 
const  = new ();
const  = .("content");
.(0, "Hello");
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 and the type selection guide Choosing CRDT Types.

Parameters:

  • name - The container name

Returns: A LoroList instance

Example:

import {  } from "loro-crdt";
 
const  = new ();
const  = .("items");
.("Item 1");
getMap(name: string): LoroMap

Gets or creates a map container with the given name. See Map for basics and patterns.

Parameters:

  • name - The container name

Returns: A LoroMap instance

Example:

import {  } from "loro-crdt";
 
const  = new ();
const  = .("settings");
.("theme", "dark");
getTree(name: string): LoroTree

Gets or creates a tree container with the given name. Learn about hierarchical editing and moves in Tree.

Parameters:

  • name - The container name

Returns: A LoroTree instance

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const tree = doc.getTree("fileSystem");
const root = tree.createNode();
getCounter(name: string): LoroCounter

Gets or creates a counter container with the given name. Counters are special CRDTs that sum concurrent increments; see Counter.

Parameters:

  • name - The container name

Returns: A LoroCounter instance

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const counter = doc.getCounter("likes");
counter.increment(1);
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 and Choosing CRDT Types.

Parameters:

  • name - The container name

Returns: A LoroMovableList instance

Example:

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
getContainerById(id: ContainerID): Container | undefined

Gets a container by its unique ID. Container IDs (CID) uniquely reference containers across updates; see Container ID and Container.

Parameters:

  • id - The container ID

Returns: The container instance or undefined if not found

Example:

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

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. Shallow snapshots remove history while keeping current state; see Shallow Snapshots. VersionVector and Frontiers are two ways to represent versions; see Version Vector and Frontiers.

Parameters:

  • mode - Export configuration (optional)

ExportMode Options:

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:

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()
});
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 and 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 adn OpLog and DocState.

Example:

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);
importBatch(data: Uint8Array[]): ImportStatus

Efficiently imports multiple updates in a single batch operation. See Batch Import for performance considerations and usage.

Parameters:

  • data - Array of binary updates

Example:

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);
exportJsonUpdates(start?: VersionVector, end?: VersionVector, withPeerCompression?: boolean): JsonSchema

Exports updates in JSON format for debugging or alternative storage. See Export Mode for format details and trade-offs.

Parameters:

  • start - Starting version (optional)
  • end - Ending version (optional)

Returns: JSON representation of updates

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const jsonUpdates = doc.exportJsonUpdates();
console.log(JSON.stringify(jsonUpdates, null, 2));
importJsonUpdates(json: string | JsonSchema): void

Imports updates from JSON format. Useful for debugging, migration, or custom storage layers; see Export Mode.

Parameters:

  • json - JSON string or object containing updates

Example:

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 and Attached vs Detached States.

Version Control Methods

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 and Version.

Parameters:

  • frontiers - Array of OpIds representing the target version

Example:

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 and Attached vs Detached States.

checkoutToLatest(): void

Returns the document to the latest version after a checkout. Related concepts: Frontiers and Version Vector.

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
doc.checkoutToLatest();
attach(): void

Attaches the document to track latest changes after being detached. See Attached vs Detached States for how Loro separates current state from history.

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
doc.attach();
detach(): void

Detaches the document from tracking latest changes, freezing it at current version. See Attached vs Detached States.

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
doc.detach();
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.

Returns: A new LoroDoc instance

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const forkedDoc = doc.fork();
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.

Parameters:

  • frontiers - The version to fork from

Returns: A new LoroDoc instance

Example:

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. Events are delivered asynchronously after a microtask. See Event Handling and Transaction Model.

Subscription Methods

subscribe(listener: (event: LoroEventBatch) => void): () => void

Subscribes to all document changes. See Event Handling for the event model and best practices.

Parameters:

  • listener - Callback function that receives change events

Returns: Unsubscribe function

Event Structure:

interface LoroEventBatch {
  by: "local" | "import" | "checkout"
  origin?: string
  currentTarget?: ContainerID
  events: LoroEvent[]
  from: Frontiers
  to: Frontiers
}

Example:

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 asynchronously after a microtask.

doc.commit();
await Promise.resolve();
// Now events have been emitted

📝 Note: Multiple operations before a commit are batched into a single event. See Event Handling.

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.

Parameters:

  • f - Callback that receives binary updates

Returns: Unsubscribe function

Example:

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);
});
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:

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

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.

⚠️ 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.

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)

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:

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

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.

Returns: JSON representation of the document

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const json = doc.toJSON();
console.log(JSON.stringify(json, null, 2));
getShallowValue(): Record<string, ContainerID>

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.

Returns: Shallow JSON value

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const shallow = doc.getShallowValue();
// Sub-containers appear as: "cid:..."
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:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const deepValue = doc.getDeepValueWithID();
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.

Returns: Map from PeerID to counter

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const vv = doc.version();
console.log(vv.toJSON());
frontiers(): Frontiers

Gets the current frontiers (heads) of the document. Frontiers are a compact representation of a version; see 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 for trade-offs.

Returns: Array of OpIds

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const frontiers = doc.frontiers();
// Can be used for checkouts or shallow snapshots
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.

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:

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

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 and Operations and Changes.

Pitfall: commit() can be triggered implicitly by import, export, and checkout. Use this hook to attach metadata even for those implicit commits.

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

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 and Cursor tutorial. The side controls affinity when the cursor sits at an insertion boundary.

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

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.

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 and the Event Graph Walker.

travelChangeAncestors(ids: OpId[], f: (change: Change) => boolean): void

Visit ancestors of the given changes in causal order.

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
});
findIdSpansBetween(from: Frontiers, to: Frontiers): VersionVectorDiff

Find the op id spans that lie between two versions.

exportJsonInIdSpan(idSpan: { peer: PeerID, counter: number, length: number }): JsonChange[]
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);
getChangedContainersIn(id: OpId, len: number): ContainerID[]

Get container IDs modified in the given ID range.

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

revertTo(frontiers: Frontiers): void

Revert the document to a given version by generating inverse operations.

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 }]);
applyDiff(diff: [ContainerID, Diff | JsonDiff][]): void

Apply a batch of diffs to the document.

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);

Detached Editing

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.

Parameters:

  • enable - Whether to enable detached editing

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
doc.setDetachedEditing(true);
isDetachedEditingEnabled(): boolean

Checks if detached editing mode is enabled.

Returns: True if detached editing is enabled

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const enabled = doc.isDetachedEditingEnabled();
isDetached(): boolean

Checks if the document is currently detached.

Returns: True if document is detached

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
console.log(doc.isDetached());

Commit Options Helpers

setNextCommitMessage(msg: string): void

Sets the message for the next commit.

Parameters:

  • msg - Commit message

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
doc.setNextCommitMessage("User action");
setNextCommitOrigin(origin: string): void

Sets the origin for the next commit.

Parameters:

  • origin - Origin identifier

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
doc.setNextCommitOrigin("ui");
setNextCommitTimestamp(timestamp: number): void

Sets the timestamp for the next commit.

Parameters:

  • timestamp - Unix timestamp in seconds

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
doc.setNextCommitTimestamp(Math.floor(Date.now() / 1000));
setNextCommitOptions(options: { origin?: string, timestamp?: number, message?: string }): void

Sets multiple options for the next commit.

Parameters:

  • options - Commit options object

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
doc.setNextCommitOptions({ origin: "ui", message: "batch" });
doc.getText("text").insert(0, "Hi");
doc.commit();
clearNextCommitOptions(): void

Clears all pending commit options.

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
doc.clearNextCommitOptions();

Version & Frontier Utilities

frontiersToVV(frontiers: Frontiers): VersionVector

Converts frontiers to a version vector.

Parameters:

  • frontiers - Frontiers to convert

Returns: Version vector representation

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const frontiers = doc.frontiers();
const vv = doc.frontiersToVV(frontiers);
vvToFrontiers(vv: VersionVector): Frontiers

Converts a version vector to frontiers.

Parameters:

  • vv - Version vector to convert

Returns: Frontiers representation

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const vv = doc.version();
const frontiers = doc.vvToFrontiers(vv);
oplogVersion(): VersionVector

Gets the oplog version vector.

Returns: Oplog version vector

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const vv = doc.oplogVersion();
oplogFrontiers(): Frontiers

Gets the oplog frontiers.

Returns: Oplog frontiers

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const frontiers = doc.oplogFrontiers();
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:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const frontiers = doc.frontiers();
const cmp = doc.cmpWithFrontiers(frontiers);
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:

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.

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:

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");
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:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
 
const path = doc.getPathToContainer(map.id);
JSONPath(jsonpath: string): any[]

Queries the document using JSONPath syntax.

Parameters:

  • jsonpath - JSONPath query string

Returns: Array of matching values

Example:

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.

shallowSinceVV(): VersionVector

Gets the version vector since which the document is shallow.

Returns: Version vector

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const vv = doc.shallowSinceVV();
shallowSinceFrontiers(): Frontiers

Gets the frontiers since which the document is shallow.

Returns: Frontiers

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const frontiers = doc.shallowSinceFrontiers();
isShallow(): boolean

Checks if the document is shallow.

Returns: True if document is shallow

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const shallow = doc.isShallow();
setHideEmptyRootContainers(hide: boolean): void

Controls whether empty root containers are hidden in JSON output.

Parameters:

  • hide - Whether to hide empty root containers

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
doc.setHideEmptyRootContainers(true);
// Now empty roots are hidden in toJSON()
deleteRootContainer(cid: ContainerID): void

Deletes a root container.

Parameters:

  • cid - Container ID to delete

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
 
// Usage example:
doc.deleteRootContainer(map.id);
hasContainer(id: ContainerID): boolean

Checks if a container exists in the document.

Parameters:

  • id - Container ID to check

Returns: True if container exists

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
 
const exists = doc.hasContainer(map.id);

JSON Serialization with Replacer

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:

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

debugHistory(): void

Prints debug information about the document history.

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
doc.debugHistory();
changeCount(): number

Gets the total number of changes in the document.

Returns: Number of changes

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const changes = doc.changeCount();
opCount(): number

Gets the total number of operations in the document.

Returns: Number of operations

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const ops = doc.opCount();
getAllChanges(): Map<PeerID, Change[]>

Gets all changes grouped by peer ID.

Returns: Map of peer ID to changes

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const changes = doc.getAllChanges();
getChangeAt(id: OpId): Change

Gets a specific change by operation ID.

Parameters:

  • id - Operation ID

Returns: Change object

Example:

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];
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:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
doc.getText("text").insert(0, "hello");
const change = doc.getChangeAtLamport(doc.peerIdStr, 1);
getOpsInChange(id: OpId): any[]

Gets all operations in a specific change.

Parameters:

  • id - Operation ID

Returns: Array of operations

Example:

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);
getPendingTxnLength(): number

Gets the number of pending operations in the current transaction.

Returns: Number of pending operations

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
doc.getText("text").insert(0, "x");
console.log(doc.getPendingTxnLength());
doc.commit();

Import/Export Utilities

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:

import { LoroDoc, decodeImportBlobMeta } from "loro-crdt";
 
const doc = new LoroDoc();
const updates = doc.export({ mode: "update" });
const meta = decodeImportBlobMeta(updates, true);
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.

Parameters:

  • json - JSON updates to redact
  • version_range - Version range for redaction

Returns: Redacted JSON schema

Example:

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 and Composing CRDTs 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, Eg-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:

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
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:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
 
// Usage example:
text.insert(0, "Hello ");
text.insert(6, "World");
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:

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
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 for mark behavior.

Parameters:

  • range - The range to format
  • key - Style attribute name
  • value - Style value

Example:

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);
unmark(range: { start: number, end: number }, key: string): void

Removes formatting from a text range. For how conflicting edits on marks resolve, see Text.

Parameters:

  • range - The range to unformat
  • key - Style attribute to remove

Example:

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");
toDelta(): Delta<string>[]

Converts text to Delta format (Quill-compatible).

Returns: Array of Delta operations

Example:

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" }]
applyDelta(delta: Delta<string>[]): void

Applies Delta operations to the text.

Parameters:

  • delta - Array of Delta operations

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.applyDelta([
  { insert: "Hello", attributes: { bold: true } },
  { insert: " World" }
]);
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:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
 
text.insert(0, "Hello");
text.update("Hello World", { timeoutMs: 100 });
updateByLine(text: string, options?: { timeoutMs?: number; useRefinedDiff?: boolean }): void

Line-based update that’s faster for large texts (less precise than update).

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");
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:

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
toString(): string

Converts to plain text string.

Returns: Plain text content

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello World");
const plainText = text.toString();
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:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello");
const char = text.charAt(1); // "e"
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:

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"
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:

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"
push(s: string): void

Appends text to the end of the document.

Parameters:

  • s - String to append

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
 
text.push("Hello");
text.push(" World");
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:

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
});
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:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
 
text.insertUtf8(0, "Hello");
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:

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"
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:

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);
kind(): "Text"

Returns the container type.

Returns: “Text”

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
 
const type = text.kind(); // "Text"
parent(): Container | undefined

Gets the parent container if this text is nested.

Returns: Parent container or undefined

Example:

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
isAttached(): boolean

Checks if the container is attached to a document.

Returns: True if attached

Example:

import { LoroDoc, LoroText } from "loro-crdt";
const doc = new LoroDoc();
const text = new LoroText();
 
const attached = text.isAttached(); // false until attached to doc
getAttached(): LoroText | undefined

Gets the attached version of this container.

Returns: Attached container or undefined

Example:

import { LoroDoc, LoroText } from "loro-crdt";
const doc = new LoroDoc();
const text = new LoroText();
 
const attached = text.getAttached();
isDeleted(): boolean

Checks if the container has been deleted.

Returns: True if deleted

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
 
const deleted = text.isDeleted();
getShallowValue(): string

Gets the text content without marks.

Returns: Plain text string

Example:

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"
toJSON(): any

Converts the text to JSON representation.

Returns: JSON value

Example:

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"
readonly id: ContainerID

Gets the unique container ID.

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
 
const containerId = text.id;
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:

text.insert(0, "👨‍👩‍👧‍👦"); // Family emoji
console.log(text.length); // 11 (not 1!) - complex emoji with ZWJ sequences

Example:

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, Choosing CRDT Types, and Eg-walker.

⚠️ Important: List vs Map for Coordinates

// ❌ 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}
insert(pos: number, value: Value | Container): void

Inserts a value at the specified position.

Parameters:

  • pos - Insert position
  • value - Value to insert

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
list.insert(0, "First");
list.insert(1, { type: "object" });
insertContainer<T extends Container>(pos: number, container: T): T

Inserts a new container at the position.

Parameters:

  • pos - Insert position
  • container - Container instance

Returns: The inserted container

Example:

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");
delete(pos: number, len: number): void

Deletes elements from the list.

Parameters:

  • pos - Starting position
  • len - Number of elements to delete

Example:

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
push(value: Value | Container): void

Appends a value to the end of the list.

Parameters:

  • value - Value to append

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
list.push("Last item");
getIdAt(pos: number): { peer: PeerID, counter: number } | undefined
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
 
list.insert(0, 1);
const id0 = list.getIdAt(0);
pushContainer<T extends Container>(container: T): T

Appends a container to the end of the list.

Parameters:

  • container - Container to append

Returns: The appended container

Example:

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");
pop(): Value | Container | undefined

Removes and returns the last element.

Returns: The removed element or undefined

Example:

import { LoroList } from "loro-crdt";
declare const list: LoroList;
// ---cut---
const lastItem = list.pop();
get(index: number): Value | Container | undefined

Gets the value at the specified index.

Parameters:

  • index - Element index

Returns: The value or undefined

Example:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const list = doc.getList("items");
list.push("first", "second");
 
const item = list.get(0); // "first"
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:

import { LoroDoc } from "loro-crdt";
 
const doc = new LoroDoc();
const list = doc.getList("list");
list.push("a", "b", "c");
 
const cursor = list.getCursor(2);
toArray(): (Value | Container)[]

Converts the list to a JavaScript array.

Returns: Array of values

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
list.push("a", "b", "c");
const array = list.toArray();
clear(): void

Removes all elements from the list.

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
list.push("a", "b", "c");
list.clear();
length: number

Gets the number of elements in the list.

Returns: List length

Example:

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`);
kind(): "List"

Returns the container type.

Returns: “List”

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
 
const type = list.kind(); // "List"
toJSON(): any

Converts the list to JSON representation.

Returns: JSON array

Example:

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]
parent(): Container | undefined

Gets the parent container if this list is nested.

Returns: Parent container or undefined

Example:

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
isAttached(): boolean

Checks if the container is attached to a document.

Returns: True if attached

Example:

import { LoroDoc, LoroList } from "loro-crdt";
const doc = new LoroDoc();
const list = new LoroList();
 
const attached = list.isAttached(); // false until attached to doc
getAttached(): LoroList | undefined

Gets the attached version of this container.

Returns: Attached container or undefined

Example:

import { LoroDoc, LoroList } from "loro-crdt";
const doc = new LoroDoc();
const list = new LoroList();
 
const attached = list.getAttached();
isDeleted(): boolean

Checks if the container has been deleted.

Returns: True if deleted

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
 
const deleted = list.isDeleted();
getShallowValue(): Value[]

Gets the list values with sub-containers as IDs.

Returns: Array of values

Example:

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]
readonly id: ContainerID

Gets the unique container ID.

Example:

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.

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.

Parameters:

  • key - The key
  • value - The value

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
 
map.set("name", "Alice");
map.set("age", 30);
setContainer<T extends Container>(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.

Parameters:

  • key - The key
  • container - Container instance

Returns: The set container

Example:

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");
get(key: string): Value | Container | undefined

Gets the value for a key.

Parameters:

  • key - The key

Returns: The value or undefined

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
 
const name = map.get("name");
getOrCreateContainer<T extends Container>(key: string, container: T): T

Gets an existing container or creates a new one.

⚠️ Pitfall: Parallel container creation for the same key across peers causes overwrites. See Container initialization.

Parameters:

  • key - The key
  • container - Container to create if not exists

Returns: The container

Example:

import { LoroDoc, LoroText } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
 
const text = map.getOrCreateContainer("description", new LoroText());
delete(key: string): void

Removes a key-value pair.

Parameters:

  • key - The key to remove

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
 
map.delete("obsoleteKey");
clear(): void

Removes all key-value pairs.

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
 
map.clear();
keys(): string[]

Gets all keys in the map.

Returns: Array of keys

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
 
const allKeys = map.keys();
values(): (Value | Container)[]

Gets all values in the map.

Returns: Array of values

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
 
const allValues = map.values();
entries(): [string, Value | Container][]

Gets all key-value pairs.

Returns: Array of entries

Example:

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}`);
}
getLastEditor(key: string): PeerID | undefined
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
size: number

Gets the number of key-value pairs.

Returns: Map size

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
 
console.log(`Map has ${map.size} entries`);
kind(): "Map"

Returns the container type.

Returns: “Map”

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
 
const type = map.kind(); // "Map"
toJSON(): any

Converts the map to JSON representation.

Returns: JSON object

Example:

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" }
parent(): Container | undefined

Gets the parent container if this map is nested.

Returns: Parent container or undefined

Example:

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
isAttached(): boolean

Checks if the container is attached to a document.

Returns: True if attached

Example:

import { LoroDoc, LoroMap } from "loro-crdt";
const doc = new LoroDoc();
const map = new LoroMap();
 
const attached = map.isAttached(); // false until attached to doc
getAttached(): LoroMap | undefined

Gets the attached version of this container.

Returns: Attached container or undefined

Example:

import { LoroDoc, LoroMap } from "loro-crdt";
const doc = new LoroDoc();
const map = new LoroMap();
 
const attached = map.getAttached();
isDeleted(): boolean

Checks if the container has been deleted.

Returns: True if deleted

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
 
const deleted = map.isDeleted();
getShallowValue(): Record<string, Value>

Gets the map values with sub-containers as IDs.

Returns: Object with values

Example:

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" }
readonly id: ContainerID

Gets the unique container ID.

Example:

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.

⚠️ 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.
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:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
 
const root = tree.createNode();
const child = root.createNode(0);
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:

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);
delete(target: TreeID): void

Deletes a node and its descendants.

Parameters:

  • target - Node to delete

Example:

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);
getNodeByID(id: TreeID): LoroTreeNode | undefined

Gets a node handler by its ID.

Parameters:

  • id - Node ID

Returns: Node handler or undefined

Example:

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");
}
nodes(): LoroTreeNode[]

Gets all nodes in the tree.

Returns: Array of all nodes

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
 
const allNodes = tree.nodes();
roots(): LoroTreeNode[]

Gets all root nodes.

Returns: Array of root nodes

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
 
const rootNodes = tree.roots();
has(target: TreeID): boolean

Checks if a node exists.

Parameters:

  • target - Node ID to check

Returns: Boolean indicating existence

Example:

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");
}
isNodeDeleted(target: TreeID): boolean

Checks if a node has been deleted.

Parameters:

  • target - Node ID to check

Returns: Boolean indicating deletion status

Example:

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");
}
enableFractionalIndex(jitter: number): void

Enables fractional indexing for better concurrent move performance.

Parameters:

  • jitter - Jitter amount for fractional indices

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
 
tree.enableFractionalIndex(0.001);
disableFractionalIndex(): void

Disables fractional indexing.

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
 
tree.disableFractionalIndex();
isFractionalIndexEnabled(): boolean

Checks if fractional indexing is enabled.

Returns: True if enabled

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
 
const enabled = tree.isFractionalIndexEnabled();
kind(): "Tree"

Returns the container type.

Returns: “Tree”

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
 
const type = tree.kind(); // "Tree"
toJSON(): any

Converts the tree to JSON representation.

Returns: JSON tree structure

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
 
const json = tree.toJSON();
parent(): Container | undefined

Gets the parent container if this tree is nested.

Returns: Parent container or undefined

Example:

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();
isAttached(): boolean

Checks if the container is attached to a document.

Returns: True if attached

Example:

import { LoroDoc, LoroTree } from "loro-crdt";
const doc = new LoroDoc();
const tree = new LoroTree();
 
const attached = tree.isAttached();
getAttached(): LoroTree | undefined

Gets the attached version of this container.

Returns: Attached container or undefined

Example:

import { LoroDoc, LoroTree } from "loro-crdt";
const doc = new LoroDoc();
const tree = new LoroTree();
 
const attached = tree.getAttached();
isDeleted(): boolean

Checks if the container has been deleted.

Returns: True if deleted

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
 
const deleted = tree.isDeleted();
getShallowValue(): TreeNodeShallowValue[]

Gets the tree values with sub-containers as IDs.

Returns: Array of tree node values

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
 
const values = tree.getShallowValue();
readonly id: ContainerID

Gets the unique container ID.

Example:

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.

data: LoroMap

A map container for storing node metadata.

Example:

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);
createNode(index?: number): LoroTreeNode

Creates a child node.

Parameters:

  • index - Position among siblings

Returns: New node’s handler

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
const childId = node.createNode(0);
move(parent?: LoroTreeNode, index?: number): void

Moves this node to a new parent.

Parameters:

  • parent - New parent node
  • index - Position among siblings

Example:

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);
moveAfter(target: LoroTreeNode): void

Moves this node after a sibling.

Parameters:

  • target - Sibling node

Example:

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);
moveBefore(target: LoroTreeNode): void

Moves this node before a sibling.

Parameters:

  • target - Sibling node

Example:

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);
parent(): LoroTreeNode | undefined

Gets the parent node.

Returns: Parent node or undefined

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
 
// Usage example:
const parentNode = node.parent();
children(): LoroTreeNode[]

Gets all child nodes.

Returns: Array of children

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
 
// Usage example:
const childNodes = node.children();
index(): number | undefined

Gets the position among siblings.

Returns: Index or undefined if root

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
 
// Usage example:
const position = node.index();
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:

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
creationId(): { peer: PeerID, counter: number }

Returns the OpID that created this node.

Returns: { peer: PeerID, counter: number } creation identifier

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
 
const { peer, counter } = node.creationId();
creator(): PeerID

Returns the peer ID that created this node (equivalent to creationId().peer).

Returns: PeerID

Example:

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
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:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
 
const lastMove = node.getLastMoveId();
isDeleted(): boolean

Checks if this node has been deleted.

Returns: Boolean deletion status

Example:

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.

increment(value: number): void

Increments the counter.

Parameters:

  • value - Amount to increment (default: 1)

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const counter = doc.getCounter("counter");
 
counter.increment(5);   // +5
decrement(value: number): void

Decrements the counter.

Parameters:

  • value - Amount to decrement (default: 1)

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const counter = doc.getCounter("counter");
 
counter.decrement(3);   // -3
value: number

Gets the current counter value.

Returns: Current numeric value

Example:

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.

📝 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.
move(from: number, to: number): void

Moves an element from one position to another.

Parameters:

  • from - Source index
  • to - Target index

Example:

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
set(pos: number, value: Value | Container): void

Replaces the value at a position.

Parameters:

  • pos - Position to update
  • value - New value

Example:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const movableList = doc.getMovableList("list");
movableList.push("a", "b", "c");
 
movableList.set(0, "Updated value");
setContainer<T extends Container>(pos: number, container: T): T

Replaces the value with a container.

Parameters:

  • pos - Position to update
  • container - New container

Returns: The set container

Example:

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 and Encoding & Export Modes.

  • Import order: Loro handles out-of-order updates automatically
  • Auto-commit: Import and export operations trigger automatic commits

Import/Export Patterns

Basic Synchronization

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

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 and v1.0 performance notes: https://loro.dev/blog/v1.0

Network Sync with WebSocket

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.

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:

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:

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:

import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const vv = doc.version();
console.log(`Document has ${vv.length()} peers`);

Events & Subscriptions

Event Structure

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

type TextDiff = {
  type: "text"
  diff: Delta<string>[]
}

Represents changes to text content using Delta format.

Properties:

  • type - Always “text” for text diffs
  • diff - Array of Delta operations (insert, delete, retain)

Example:

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

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:

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

type MapDiff = {
  type: "map"
  updated: Record<string, Value | Container | undefined>
}

Represents changes to map content.

Properties:

  • type - Always “map” for map diffs
  • updated - Record of key-value changes (undefined means deleted)

Example:

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

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:

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:

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 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
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:

import { LoroDoc, UndoManager } from "loro-crdt";
 
const doc = new LoroDoc();
const undo = new UndoManager(doc, {
  mergeInterval: 1000,
  maxUndoSteps: 100,
  excludeOriginPrefixes: ["sync-"]
});
undo(): boolean

Undo the last operation.

Returns: True if undo was successful

Example:

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
redo(): boolean

Redo the last undone operation.

Returns: True if redo was successful

Example:

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
canUndo(): boolean

Check if undo is available.

Returns: True if can undo

Example:

import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
 
// Usage example:
if (undo.canUndo()) {
  undo.undo();
}
canRedo(): boolean

Check if redo is available.

Returns: True if can redo

Example:

import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
 
// Usage example:
if (undo.canRedo()) {
  undo.redo();
}
peer(): PeerID

Get the peer ID of the undo manager.

Returns: The peer ID

Example:

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"
setMaxUndoSteps(steps: number): void

Set the maximum number of undo steps.

Parameters:

  • steps - Maximum number of undo steps

Example:

import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
 
// Usage example:
undo.setMaxUndoSteps(50);
setMergeInterval(interval: number): void

Set the merge interval for grouping operations.

Parameters:

  • interval - Merge interval in milliseconds

Example:

import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
 
// Usage example:
undo.setMergeInterval(2000); // 2 seconds
addExcludeOriginPrefix(prefix: string): void

Add a prefix to exclude from undo stack.

Parameters:

  • prefix - Origin prefix to exclude

Example:

import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
 
// Usage example:
undo.addExcludeOriginPrefix("sync-");
undo.addExcludeOriginPrefix("import-");
clear(): void

Clear the undo and redo stacks.

Example:

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:

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, Version Vector, Frontiers, and the Versioning Deep Dive.

Core Types

// 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 and Frontiers. For the full DAG model, see Version Deep Dive.

// Version vector class
class VersionVector {
  constructor(value: Map<PeerID, number> | Uint8Array | VersionVector | undefined | null)
  static parseJSON(version: Map<PeerID, number>): VersionVector
  toJSON(): Map<PeerID, number>
  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

// 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 and Cursor tutorial.

// 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.

// Rich text delta operations
type Delta<T> =
  | {
      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 and Versioning Deep Dive for frontiers/version encoding.

Frontier Encoding

encodeFrontiers(frontiers: OpId[]): Uint8Array

Encode frontiers for efficient transmission.

Parameters:

  • frontiers - Array of operation IDs representing frontiers

Returns: Encoded bytes

Example:

import { LoroDoc, encodeFrontiers } from "loro-crdt";
 
const doc = new LoroDoc();
const frontiers = doc.frontiers();
const encoded = encodeFrontiers(frontiers);
// Send encoded to remote peers
decodeFrontiers(bytes: Uint8Array): OpId[]

Decode frontiers from bytes.

Parameters:

  • bytes - Encoded frontier bytes

Returns: Array of operation IDs

Example:

import { decodeFrontiers } from "loro-crdt";
 
declare const encodedData: Uint8Array;
const frontiers = decodeFrontiers(encodedData);
console.log(frontiers); // [{ peer: "1", counter: 10 }, ...]

Debugging

setDebug(): void

Enable debug mode for detailed logging.

Example:

import { setDebug } from "loro-crdt";
 
// Enable debug logging
setDebug();
LORO_VERSION(): string

Get the current Loro version.

Returns: Version string

Example:

import { LORO_VERSION } from "loro-crdt";
 
const version = LORO_VERSION();
console.log("Loro version:", version);

Import Blob Metadata

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:

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 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

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:

import { EphemeralStore } from "loro-crdt";
 
// Create ephemeral store with 30 second timeout
const store = new EphemeralStore(30000);
set<K extends keyof T>(key: K, value: T[K]): void

Set an ephemeral value.

Parameters:

  • key - The key to set
  • value - The value to store

Example:

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" });
get<K extends keyof T>(key: K): T[K] | undefined

Get an ephemeral value.

Parameters:

  • key - The key to get

Returns: The stored value or undefined

Example:

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 }
delete<K extends keyof T>(key: K): void

Delete an ephemeral value.

Parameters:

  • key - The key to delete

Example:

import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
store.set("cursor", { line: 10 });
 
// Usage example:
store.delete("cursor");
getAllStates(): Partial<T>

Get all ephemeral states.

Returns: All stored states

Example:

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);
encode<K extends keyof T>(key: K): Uint8Array

Encode a specific key’s state for transmission.

Parameters:

  • key - The key to encode

Returns: Encoded bytes

Example:

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
encodeAll(): Uint8Array

Encode all states for transmission.

Returns: Encoded bytes

Example:

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
apply(bytes: Uint8Array): void

Apply remote updates.

Parameters:

  • bytes - Encoded updates from remote peer

Example:

import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
 
// Usage example:
declare const remoteData: Uint8Array;
store.apply(remoteData);
keys(): string[]

Get all keys in the store.

Returns: Array of keys

Example:

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"]
subscribe(listener: EphemeralListener): () => void

Subscribe to all ephemeral state changes.

Parameters:

  • listener - Callback function for state changes

Returns: Unsubscribe function

Example:

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();
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:

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();
destroy(): void

Clean up and destroy the ephemeral store.

Example:

import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
 
// Usage example:
store.destroy();

Complete Example

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);