| name | loro-crdt |
| description | Expert guidance for the Loro CRDT library and loro-protocol sync protocol in Rust and TypeScript. Covers LoroDoc, container selection (Map, List, Text, MovableList, Tree, Counter), WebSocket sync via loro-protocol (handshake, rooms, CrdtDocAdaptor trait), encoding/export, undo/redo, event subscriptions, time travel, cursors, and ephemeral state. MUST invoke before any response that reads or modifies files under `*/loro/*`, `*/loro*.rs`, or `*/crdt/*`, or when the user's message or referenced files contain Loro types (LoroDoc, LoroMap, LoroTree, etc.). Also invoke when working with collaborative editing, real-time sync, WebSocket sync, or CRDTs. Trigger: "loro", "crdt", "collaborative editing", "loro-crdt", "loro-protocol", "LoroDoc", "LoroMap", "LoroText", "LoroList", "LoroTree", "CrdtDocAdaptor", "WebSocket sync". |
Loro CRDT
Loro is a high-performance CRDT framework. It is a pure library: it handles conflict-free data structures and merge semantics, but not networking, storage, or transport. You export bytes and import bytes; everything in between is your responsibility.
Rust-native (loro crate), TypeScript via WASM (loro-crdt npm package). LoroDoc is the entry point for both.
For comprehensive API details beyond this skill, fetch the LLM-optimized reference:
https://loro.dev/llms-full.txt (642KB, the entire docs site in one file)
Documentation Links
Getting Started & Core Concepts
- Get Started -- install, bundler setup, first sync
- Core Concepts -- attached/detached, OpLog vs DocState, version vectors, frontiers
- LoroDoc -- the main entry point, container access, commits, forking
Container Tutorials (both Rust and TypeScript examples)
- Map -- LWW key-value, nested containers, conflict resolution
- List -- ordered sequence, Fugue algorithm
- Text / Richtext -- rich text with marks, ExpandType config
- MovableList -- list with CRDT-safe reordering
- Tree -- hierarchical movable tree, fractional index siblings
- Counter -- additive distributed counter
Collaboration & Versioning
Advanced
Sync Protocol (loro-protocol)
The loro-protocol library provides a complete WebSocket sync protocol on top of Loro's raw export/import primitives: handshake, room management, fragmentation, and an adaptor trait.
API References
Rust (docs.rs, authoritative for Rust API):
TypeScript:
Language-Specific Patterns (reference files in this skill)
- Rust idioms, cargo setup, trait system: read
references/rust-patterns.md
- TypeScript idioms, WASM bundler setup: read
references/typescript-patterns.md
- Sync protocols, export modes, versioning deep-dive: read
references/sync-and-encoding.md
- WebSocket sync protocol, CrdtDocAdaptor trait, handshake, rooms: read
references/loro-protocol.md
Container Selection
What kind of data are you modeling?
Key-value pairs, properties, settings, or labeled fields?
Use LoroMap. LWW conflict resolution (Lamport timestamp; higher PeerID wins ties).
Do NOT use a list of pairs; do NOT use a map for ordered sequences.
Tutorial | Rust API
Ordered collection of items?
- Items need drag-and-drop / reordering? Use
LoroMovableList. ~80% slower encode, ~50% more memory than LoroList. Tutorial
- Append-only or no reordering? Use
LoroList. Fugue algorithm for maximal non-interleaving. Tutorial
Editable text (plain or rich)?
Use LoroText. O(log N) insert/delete. Concurrent edits merge by interleaving characters.
Configure mark expansion with config_text_style before inserting marks.
Tutorial | Rust API
Hierarchical / tree-shaped data (folders, outlines, org charts)?
Use LoroTree. Based on Kleppmann's algorithm. Enable fractional_index for ordered siblings.
Tutorial | Rust API
Numeric accumulator (votes, counters, scores)?
Use LoroCounter. Additive CRDT. Requires features = ["counter"] in Rust.
Tutorial
A string value where concurrent edits should pick a winner, not merge characters?
Use a String value inside LoroMap (LWW), NOT LoroText.
LoroText merges concurrent insertions character-by-character. Map picks one winner.
Good for: URLs, identifiers, hashes, enum-like values.
LoroDoc Lifecycle
let doc = LoroDoc::new();
let map = doc.get_map("config");
map.insert("key", "value").unwrap();
doc.commit();
let bytes = doc.export(ExportMode::Snapshot).unwrap();
let restored = LoroDoc::from_snapshot(&bytes).unwrap();
let forked = doc.fork();
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("config");
map.set("key", "value");
doc.commit();
const bytes = doc.export({ mode: "snapshot" });
const restored = LoroDoc.fromSnapshot(bytes);
const forked = doc.fork();
Full lifecycle tutorial: LoroDoc
Critical Pitfalls
CRITICAL: Never reuse PeerID across concurrent writers.
Each writer must have a globally unique PeerID. Reusing one across concurrent sessions permanently corrupts the document by producing conflicting operation IDs in the OpLog DAG. There is no recovery. Default behavior (random assignment) is safe; only call setPeerId() / set_peer_id() when you have a durable, unique identifier per writer.
CRITICAL: clone() is a reference clone in Rust.
doc.clone() gives you a second handle to the same underlying document. Mutations through either handle affect both. Use doc.fork() for an independent deep copy. In TypeScript, always use doc.fork().
HIGH: Concurrent sub-container creation at the same map key.
If two peers both insert_container("child", LoroText::new()) on the same map key, LWW picks one container and the other's edits are silently lost. Initialize all child containers during setup, or use distinct keys / root-level containers.
HIGH: Document is read-only after checkout().
Calling checkout(frontiers) enters detached mode. Edits will fail unless you call set_detached_editing(true) first. Call attach() or checkout_to_latest() / checkoutToLatest() to resume normal editing.
HIGH: Timestamps are not recorded by default.
Call doc.set_record_timestamp(true) / doc.setRecordTimestamp(true) before making edits. This is a runtime setting, not persisted in the document.
MEDIUM: LoroText vs LoroMap for string values.
LoroText merges concurrent edits character-by-character (correct for prose). For values where partial merge breaks semantics (URLs, IDs, config strings), store as a String in LoroMap so LWW picks one winner.
MEDIUM: Text indexing differs between Rust and TypeScript.
TypeScript uses UTF-16 offsets by default. Rust uses Unicode scalar positions. When sharing cursor positions or text ranges cross-language, use the explicit _utf8 / _utf16 method variants and convert deliberately.
Rust vs TypeScript Quick Reference
| Concern | Rust | TypeScript |
|---|
| Package | loro crate | loro-crdt npm |
| Entry point | LoroDoc::new() | new LoroDoc() |
| Text index unit | Unicode scalar | UTF-16 |
| PeerID type | u64 | bigint / decimal string |
| Unsubscribe | Drop the Subscription object | Call returned function |
| Ephemeral state | awareness module | EphemeralStore |
| Deep copy | fork() (NOT clone()) | fork() |
| Counter feature | features = ["counter"] | Built-in |
| Events | Synchronous | Synchronous |
| Map set method | map.insert(key, value) | map.set(key, value) |
| Sub-containers | map.insert_container(key, T::new()) | map.setContainer(key, new T()) |
Sync Protocol
Two-peer sync at the primitive level requires two exchanges:
let updates = doc_a.export(ExportMode::updates(&doc_b.oplog_vv())).unwrap();
doc_b.import(&updates).unwrap();
let updates = doc_b.export(ExportMode::updates(&doc_a.oplog_vv())).unwrap();
doc_a.import(&updates).unwrap();
For real-time streaming, use subscribe_local_update / subscribeLocalUpdates to push bytes as they are produced.
For encoding modes, export strategies, and shallow snapshots, read references/sync-and-encoding.md.
For WebSocket-based sync, the loro-protocol library provides a complete protocol on top of these primitives: handshake with auth, room management, automatic fragmentation, error handling, and an adaptor trait (CrdtDocAdaptor) for plugging in your LoroDoc. Read references/loro-protocol.md for the full API.