Getting Started with LoroDoc
LoroDoc is the main entry point for almost all Loro functionality. It serves as a container manager and coordinator that provides:
- Container Management: Create and manage different types of CRDT containers (Text, List, Map, Tree, MovableList)
- Version Control: Track document history, checkout versions, and manage branches
- Event System: Subscribe to changes at both document and container levels
- Import/Export: Save and load documents/updates in various formats
Basic Usage
First, let’s create a new LoroDoc instance:
// Create a new document with a random peer ID
const = new ();
// Or set a specific peer ID
.("1");
// Create containers
const = .("text");
const = .("list");
const = .("map");
const = .("tree");
const = .("tasks");
To model a document with the following format:
{
"meta": {
"title": "Document Title",
"createdBy": "Author"
},
"content": "Article",
"comments": [
{
"user": "userId",
"comment": "comment"
}
]
}
const = new ();
const = .("meta");
.("title", "Document Title");
.("createdBy", "Author");
.("content").(0, "Article");
const = .("comments");
const = .(0, new ());
.("user", "userId");
.("comment", "comment");
Supported Data Types (LoroValue)
Loro supports storing various data types in its containers. The LoroValue
type represents all possible values that can be stored in a LoroDoc:
type LoroValue =
| null // Null values
| boolean // true or false
| number // Numeric values (both integer and floating point)
| string // Text values
| Uint8Array // Binary data
| LoroValue[] // Arrays (nested)
| { [key: string]: LoroValue } // Objects/Maps (nested)
Examples of Storing Different Data Types
const = new ();
const = .("data");
// Primitive types
.("name", "Alice"); // string
.("age", 30); // integer number
.("score", 95.5); // floating point number
.("isActive", true); // boolean
.("metadata", null); // null
// Binary data
const = new ([1, 2, 3, 4, 5]);
.("bytes", ); // Uint8Array
// Nested objects (plain JavaScript objects)
.("profile", {
: "alice@example.com",
: {
: "dark",
: true
}
});
// Arrays
.("tags", ["javascript", "typescript", "loro"]);
// Mixed nested structures
.("complex", {
: [1, 2, { : true }],
: new ([255, 128]),
: false
});
Note: When you need to store a CRDT container (like LoroText, LoroList, etc.) within another container, use setContainer()
or insertContainer()
methods instead of regular set()
or insert()
. This creates a proper sub-container relationship that maintains CRDT properties.
Container Types
LoroDoc supports several container types:
- Text - For rich text editing
- List - For ordered collections
- Map - For key-value pairs
- Tree - For hierarchical data structures
- MovableList - For lists with movable items
Let’s look at how to use each type:
Text Container
const = new ();
const = .("text");
.(0, "Hello");
.(5, " World!");
.(.()); // "Hello World!"
// Rich text support
.({
: { : "after" },
: { : "none" },
});
.({ : 0, : 5 }, "bold", true);
List Container
const = new ();
const = .("list");
.(0, "first");
.(1, "second");
.(.()); // ["first", "second"]
// Nested containers
const = .(2, new ());
.(0, "nested text");
Map Container
const = new ();
const = .("map");
.("name", "John");
.("age", 30);
.(.("name")); // "John"
// Nested containers
const = .("bio", new ());
.(0, "Software Engineer");
Tree Container
const = new ();
const = .("tree");
const = .();
..("name", "Root");
const = .();
..("name", "Child 1");
const = .();
..("name", "Child 2");
MovableList Container
const = new ();
const = .("tasks");
.(0, "Task 1");
.(1, "Task 2");
.(0, 1); // Move Task 1 after Task 2
Collaboration Features
LoroDoc can be used for real-time collaboration. Here’s how to sync changes between peers:
// First peer
const = new ();
.("1");
const = .("text");
// Second peer
const = new ();
.("2");
const = .("text");
// Set up two-way sync
.(() => {
.();
});
.(() => {
.();
});
// Now changes in doc1 will be reflected in doc2 and vice versa
.(0, "Hello");
.();
await .(); // await for the event to be emitted
.(5, " World!");
.();
Undo/Redo Support
Loro provides built-in undo/redo functionality:
const = new ();
const = new (, {
: 100,
: 1000,
});
const = .("text");
// Make some changes
.(0, "Hello");
.();
// Undo the changes
if (.()) {
.();
}
// Redo the changes
if (.()) {
.();
}
Exporting and Importing
You can save and load the document state:
const = new ();
// Export the document
const = .({ : "snapshot" });
// Create a new document from the snapshot
const = .();
const = new ();
// Or import into an existing document
.();
Shallow Import/Export
Shallow import/export is a feature that allows you to create and share document snapshots without including the complete history. This is particularly useful for:
- Reducing the size of exported data
- Sharing the document with others without revealing the complete history
- Speedup the import/export process
Here’s how to use shallow export:
const = new ();
// Export a shallow snapshot that only include the history since `doc.oplogFrontiers()`
// It works like `git clone --depth=1`, where the exported data only contain the most recent ops.
const = .({
: "shallow-snapshot",
: .(),
});
// Check if a document is shallow
const = .();
// Get the version since which the history is available
const = .();
// Or get it in frontiers format
const = .();
Note: A shallow document only contains history after a certain version point. Operations before the shallow start point are not included, but the document remains fully functional for collaboration.
Redacting Sensitive Content
Loro allows you to redact specific segments of document history while preserving the rest. This is particularly useful when:
- A user accidentally pastes sensitive information (like passwords or API keys) into the document
- You need to remove just the sensitive part of the history while keeping older and newer edits intact
- You want to share document history with sensitive segments sanitized
Here’s how to use the redaction functionality:
const = new ();
.("1");
// Create some content to be redacted
const = .("text");
.(0, "Sensitive information");
.();
const = .("map");
.("password", "secret123");
.("public", "public information");
.();
// Export JSON updates
const = .();
// Define version range to redact (redact the text content)
const = {
"1": [0, 21], // Redact the "Sensitive information"
};
// Apply redaction
const = (, );
// Create a new document with redacted content
const = new ();
.();
// The text content is now redacted with replacement characters
.(.("text").());
// Outputs: "���������������������"
// Map operations after the redacted range remain intact
.(.("map").("password")); // "secret123"
.(.("map").("public")); // "public information"
Redaction applies these rules to preserve document structure while removing sensitive content:
- Preserves delete and move operations
- Replaces text insertion content with Unicode replacement characters ’�’
- Substitutes list and map insert values with null
- Maintains structure of nested containers
- Replaces text mark values with null
- Preserves map keys and text annotation keys
Note that redaction doesn’t remove the operations completely - it just replaces the sensitive content with placeholders. If you need to completely remove portions of history, see the section on shallow snapshots in the Tips section.
Important: Synchronization Considerations
Both redaction and shallow snapshots maintain future synchronization consistency, but your application is responsible for ensuring all peers get the sanitized version. Otherwise, old instances of the document with sensitive information will still exist on other peers.
Event Subscription
Subscribe to changes in the document:
const = new ();
.(() => {
.("Document changed:", );
});
const = .("text");
// Container-specific subscription
.(() => {
.("Text changed:", );
});
Event Emission
Events in LoroDoc are emitted only after a transaction is committed, and importantly, the events are emitted after a microtask. This means you need to await a microtask if you want to handle the events immediately after a commit.
- Explicitly calling
doc.commit()
:
const = new ();
const = .("text");
// Subscribe to changes
.(() => {
.("Change event:", );
});
.(0, "Hello"); // No event emitted yet
.(); // Event will be emitted after a microtask
// If you need to wait for the event:
await .(); // Now the event has been emitted
- Implicitly through certain operations:
const = new ();
const = .("text");
// These operations trigger implicit commits:
.({ : "snapshot" }); // Implicit commit
.(); // Implicit commit
.(); // Implicit commit
You can also specify additional information when committing:
const = new ();
.({
: "user-edit", // Mark the event source
: "Add greeting", // Like a git commit message
: .(), // Custom timestamp
});
await .(); // Wait for event if needed
Note: Multiple operations before a commit
are batched into a single event. This helps reduce event overhead and provides atomic changes. The event will still be emitted after a microtask, regardless of whether the commit was explicit or implicit.
Version Control and History
LoroDoc provides powerful version control features that allow you to track and manage document history:
Version Representation
Loro uses two ways to represent versions:
- Version Vector: A map from peer ID to counter
const = new ();
// Get current version vector
const = .();
// Get oplog version vector (latest known version)
const = .();
- Frontiers: A list of operation IDs that represent the latest operations from each peer. This is compacter than version vector. In most of the cases, it only has 1 element.
const = new ();
.("0");
.("map").("text", "Hello");
// Get current frontiers
const = .();
// Get oplog frontiers (latest known version)
const = .(); // { "0": 0 }
Checkout and Time Travel
You can navigate through document history using checkout:
const = new ();
// Save current version
const = .();
const = .("text");
// Make some changes
.(0, "Hello World!");
// Checkout to previous version
.();
// Return to latest version
.();
// or
.();
Note: After checkout, the document enters “detached” mode. In this mode:
- The document is not editable by default
- Import operations are recorded but not applied to the document state
- You need to call
attach()
orcheckoutToLatest()
to go back to the latest version and make it editable again
Detached Mode
The document enters “detached” mode after a checkout
operation or when explicitly calling doc.detach()
. In detached mode, the document state is not synchronized with the latest version in the OpLog.
const = new ();
// Check if document is in detached mode
.(.()); // false
// Explicitly detach the document
.();
.(.()); // true
// Return to attached mode
.();
.(.()); // false
By default, editing is disabled in detached mode. However, you can enable it:
const = new ();
// Enable editing in detached mode
.(true);
.(.()); // true
Key Behaviors in Detached Mode
- Import Operations
- Operations imported via
doc.import()
are recorded in the OpLog - These operations are not applied to the document state until checkout
- Operations imported via
const = new ();
.("map").("name", "John");
const = .({ : "update" });
const = new ();
// In detached mode
.(); // Updates are stored but not applied
.(); // Now updates are applied
- Version Management
- Each checkout uses a different PeerID to prevent conflicts
- The document maintains two version states:
const = new ();
// Current state version
const = .();
// Latest known version in OpLog
const = .();
- Forking
- You can create a new document at a specific version:
const = new ();
.("0");
.("text").(0, "Hello");
// Fork at current frontiers
const = .();
// Or fork at specific frontiers
const = .([{ : "0", : 1 }]);
.(.("text").()); // "He"
Common Use Cases
- Time Travel and History Review
const = new ();
// Save current version
const = .();
// Make changes
.insert(0, "New content");
// Review previous version
.();
// Return to latest version
.();
- Branching
const = new ();
// Enable detached editing
.(true);
// Create a branch
const = .();
// Make changes in branch
const = .("text");
.(0, "Branch changes");
Subscription and Sync
Local Updates Subscription
Subscribe to local changes for syncing between peers:
const = new ();
// Subscribe to local updates
const = .(() => {
// Send updates to other peers
.();
});
// Later, unsubscribe when needed
();
Document Events
Subscribe to all document changes. The event may be triggered by local operations, importing updates, or switching to another version.
const = new ();
.((: LoroEventBatch) => {
.("Event triggered by:", .); // "local" | "import" | "checkout"
.("Event origin:", .);
for (const of .) {
.("Target container:", .);
.("Path:", .);
.("Changes:", .);
}
});
Container-specific Events
Subscribe to changes in specific containers:
const = new ();
const = .("text");
.((: LoroEventBatch) => {
// Handle text-specific changes
.("Text changed:", );
});
const = .("list");
.((: LoroEventBatch) => {
// Handle list-specific changes
.("List changed:", );
});
Advanced Features
Cursor Support
Loro provides stable cursor position tracking that remains valid across concurrent edits:
const = new ();
const = .("text");
.(0, "123");
// Get cursor at position with side (-1, 0, or 1)
const = .(0, 0);
if () {
// Get current cursor position
const = .();
.(.); // Current position
.(.); // Cursor side
// Cursor position updates automatically with concurrent edits
.(0, "abc");
const = .();
.(.); // Position updated
}
Change Tracking
Track and analyze document changes:
const = new ();
.("1");
.("text").(0, "Hello");
.();
// Get number of changes and operations
.(.()); // Number of changes
.(.()); // Number of operations
const = .();
for (const [, ] of .()) {
for (const of ) {
.("Change:", {
: .,
: .,
: .,
: .,
: .,
});
}
}
// Get specific change
const = { : "1", : 0 } as ;
const = .();
// Get operations in a change
const = .();
.([], () => {
.("Ancestor change:", );
return true; // continue traversal
});
// Get modified containers in a change
const = .(, 1);
Advanced Import/Export
Loro supports various import and export modes:
// Export modes
const = new ();
const = .();
.("text").(0, "Hello");
const = .({ : "snapshot" });
const = .({ : "update", : });
const = .({
: "shallow-snapshot",
: .(),
});
const = .({
: "updates-in-range",
: [{ : { : "1", : 0 }, : 10 }],
});
// Import with status tracking
const = .();
.("Successfully imported:", .);
.("Pending imports:", .);
// Batch import
const = .([, ]);
// Import JSON updates
const = .({
: 1,
: new ([["1", 0]]),
: ["1"],
: [],
});
Path and Value Access
Access document content through paths:
const = new ();
// Get value or container by path
const = .("map/key");
const = .("list");
// Get path to a container
const = .("cid:root-list:List");
// JSONPath support
const = .("$.list[*]");
// Get shallow values (container IDs instead of resolved values)
const = .();
.(); // { list: 'cid:root-list:List', ... }
// Custom JSON serialization
const = .((, ) => {
if ( instanceof ) {
return .();
}
return ;
});
Debug and Metadata
Access debug information and metadata:
const = new ();
// Enable debug info
();
const = .({ : "update" });
// Get import blob metadata
const = (, true);
.({
: .,
: .,
: .,
: .,
});