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, unlesssetDetachedEditing(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
- Create a document:
new LoroDoc()
- Initialize a new collaborative document - Add containers:
getText
,getList
,getMap
,getTree
- Listen to changes:
subscribe
- React to document modifications - Export/Import state:
export
andimport
- Save and load documents
Real-time Collaboration
- Sync between peers:
export
withmode: "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
- Create rich text:
getText
- Initialize a collaborative text container - Edit text:
insert
,delete
,applyDelta
- Apply formatting:
mark
- Add bold, italic, links, custom styles - Track cursor positions:
getCursor
+getCursorPos
- Stable positions across edits - Configure styles:
configTextStyle
- Define expand behavior for marks
Data Structures
- Ordered lists:
getList
- Arrays withpush
,insert
,delete
- Key-value maps:
getMap
- Objects withset
,get
,delete
- Hierarchical trees:
getTree
- File systems, nested comments withcreateNode
,move
- Reorderable lists:
getMovableList
- Drag-and-drop withmove
,set
- Counters:
getCounter
- Distributed counters withincrement
Ephemeral State & Presence
- User presence:
EphemeralStore
- Share cursor positions, selections, user status (not persisted) - Cursor syncing: Use
EphemeralStore.set
with cursor data fromgetCursor
- Live indicators: Track who’s online, typing indicators, mouse positions
- Important: EphemeralStore is a separate CRDT without history - perfect for temporary state that shouldn’t persist
Version Control & History
- Undo/redo:
UndoManager
- Local undo of user’s own edits - Time travel:
checkout
to anyfrontiers
- Debug or review history - Version tracking:
version
,frontiers
,versionVector
- Fork documents:
fork
orforkAt
- Create branches for experimentation - Merge branches:
import
- Combine changes from forked documents
Performance & Storage
- Incremental updates:
export
from specificversion
- Send only changes - Compact history:
export
withmode: "snapshot"
- Full state with compressed history - Shallow snapshots:
export
withmode: "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 configurationmessage
- 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 messagesorigin
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 frontiersto
- Ending frontiersfor_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 frontiersb
- 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 IDlamport
- 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 decodecheck_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 redactversion_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 JavaScriptstring.length
)- Use
insertUtf8()
anddeleteUtf8()
for UTF-8 byte-based operations when integrating with UTF-8 systems
⚠️ Common Pitfalls:
- Index Misalignment: UTF-16 indices differ from visual character count
- 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 formatkey
- Style attribute namevalue
- 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 unformatkey
- 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 contentoptions
- Update optionstimeoutMs
- Optional timeout for the diff computationuseRefinedDiff
- 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 textside
- 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 indexend
- 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 positionlen
- Length to deletes
- 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 indexcontent
- 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 indexlen
- 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 positionvalue
- 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 positioncontainer
- 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 positionlen
- 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 listside
- 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 keyvalue
- 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 keycontainer
- 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 keycontainer
- 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 moveparent
- 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 nodeindex
- 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 indexto
- 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 updatevalue
- 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 updatecontainer
- 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 aVersionVector
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 diffsdiff
- 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 diffsdiff
- 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 diffsupdated
- 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 diffsdiff
- Array of TreeDiffItem operations (create, delete, move)
TreeDiffItem Actions:
create
- Node creation with parent and positiondelete
- Node deletion with old parent and positionmove
- 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 forconfig
- Configuration optionsmergeInterval?
- 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 undoonPush?
- Callback when adding to undo stackonPop?
- 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 bytescheck_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 setvalue
- 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);