API 参考
最后更新:2025-08-09 [email protected]
概览
Loro 是一款功能强大的无冲突复制数据类型(CRDT)库,可用于构建实时协作体验。如果你对 CRDT 还不熟悉,建议先阅读什么是 CRDT 了解基础概念。本 API 参考涵盖了 JavaScript/TypeScript 绑定中提供的全部类、方法和类型。
注意:在底层,Loro 将基于 Fugue 的 CRDT 内核与受 Eg-walker 启发的技术相结合,通过简单的索引操作在合并时仅重放分叉历史。这样既能保证本地编辑迅捷,也能在无需永久墓碑的情况下完成高效合并和低开销存储。可以参阅教程 Event Graph Walker (Eg-walker) 以及 v1.0 博文中的性能说明(导入/导出提速、浅快照):https://loro.dev/blog/v1.0
常见陷阱与最佳实践
Peer ID 管理
- 切勿在并发会话(标签页 / 设备)之间共享 PeerID——会导致文档分叉
- 除非你实现了严格的单所有者锁定,否则请使用随机 PeerID(默认)
- 不要为用户或设备分配固定 PeerID
UTF-16 文本编码
- JS API 默认使用 UTF-16 索引处理文本操作
- 在多单元码点中间切片会破坏字符
- 对接 UTF-8 系统时,使用
insertUtf8()/deleteUtf8()
容器创建
- 在同一个 LoroMap 上以相同键并发创建子容器会互相覆盖
- 如有可能,请提前初始化 LoroMap 所需的全部子容器
- 对根容器的操作不会互相覆盖
事件与事务
- 在 JS API 中,事件会在微任务之后异步触发
- import/export/checkout 会自动提交
- Loro 事务不具备 ACID 特性——没有回滚 / 隔离
版本控制
- 调用
checkout()后,文档会进入只读的“分离”模式,除非调用setDetachedEditing(true) - 未保留历史时,Frontiers 无法确定完整的操作集
数据结构选择
- Map 中用于 URL / ID 时请使用字符串(最后写入胜出),协同编辑文本使用 LoroText
常见任务与示例
快速上手
- 创建文档:
new LoroDoc()—— 初始化一个新的协作文档 - 添加容器:
getText、getList、getMap、getTree - 监听变更:
subscribe—— 监听文档更新 - 导入 / 导出状态:
export与import—— 保存与加载文档
实时协作
- 在节点之间同步:使用
export(mode: "update")配合import/importBatch交换增量更新 - 流式发送更新:
subscribeLocalUpdates—— 通过 WebSocket/WebRTC 推送变更 - 设置唯一 Peer ID:
setPeerId—— 确保每个客户端都有唯一标识 - 处理冲突:自动完成——所有 Loro 数据类型都是可合并并发编辑的 CRDT
富文本编辑
- 创建富文本容器:
getText—— 初始化协同文本容器 - 编辑文本:
insert、delete、applyDelta - 应用格式:
mark—— 添加加粗、斜体、链接、自定义样式 - 追踪光标位置:
getCursor与getCursorPos—— 在编辑中保持稳定位置 - 配置样式行为:
configTextStyle—— 定义标记的扩展策略
数据结构
- 有序列表:
getList—— 结合push、insert、delete - 键值映射:
getMap—— 搭配set、get、delete - 分层树结构:
getTree—— 通过createNode、move构建文件系统或嵌套评论 - 可重排列表:
getMovableList—— 使用move、set完成拖拽排序 - 计数器:
getCounter—— 搭配increment构建分布式计数
临时状态与在线状态
- 用户在线信息:
EphemeralStore—— 共享光标位置、选区、用户状态(不会持久化) - 同步光标:使用
EphemeralStore.set配合getCursor获取的光标数据 - 实时指示:展示在线用户、输入指示、鼠标位置
- 重要:EphemeralStore 是一个无历史的独立 CRDT——适合不应持久化的临时状态
版本控制与历史
- 撤销 / 重做:
UndoManager—— 回滚用户本地操作 - 时光旅行:使用
checkout跳转到任意frontiers—— 调试或回顾历史 - 版本追踪:
version、frontiers、versionVector - 分叉文档:
fork或forkAt—— 创建实验分支 - 合并分支:
import—— 将分叉文档的更改合并回来
性能与存储
- 增量更新:从指定
version运行export—— 仅发送变更 - 压缩历史:使用
export并设置mode: "snapshot"—— 导出完整状态及压缩历史 - 浅快照:
export搭配mode: "shallow-snapshot"—— 获取无部分历史的状态(详见浅快照)
configDefaultTextStyle(style?: { expand: "after" | "before" | "both" | "none" }): void配置 LoroText 的默认样式。如果传入 undefined,则会重置默认样式。
参数:
style- 可选的默认样式配置
示例:
import { } from "loro-crdt";
const = new ();
.({ : "after" });容器访问方法
📝 提示: 创建根容器(例如 doc.getText("..."))不会记录操作;创建嵌套容器(如 map.setContainer(...))会记录操作。
⚠️ 注意: 避免在同一个 LoroMap 中以相同键并发创建子容器。不要这样:
// 危险:可能发生覆盖
doc.getMap("user").getOrCreateContainer(userId, new LoroMap());请改用:
// 安全:每个用户使用独立的根容器
doc.getMap("user." + userId);getText(name: string): LoroText获取或创建指定名称的文本容器。如果你对 LoroText 与标记还不熟悉,请参阅文本。
参数:
name- 容器名称
返回值: LoroText 实例
示例:
import { } from "loro-crdt";
const = new ();
const = .("editor");
.(0, "Hello");getList(name: string): LoroList获取或创建指定名称的列表容器。不确定该用 List 还是 MovableList?参阅列表与可移动列表以及选择 CRDT 类型。
参数:
name- 容器名称
返回值: LoroList 实例
示例:
import { } from "loro-crdt";
const = new ();
const = .("items");
.("Item 1");getMap(name: string): LoroMap获取或创建指定名称的 Map 容器。基础用法与最佳实践参阅Map。
参数:
name- 容器名称
返回值: LoroMap 实例
示例:
import { } from "loro-crdt";
const = new ();
const = .("settings");
.("theme", "dark");getTree(name: string): LoroTree获取或创建指定名称的树形容器。关于分层编辑与节点移动,参阅Tree。
参数:
name- 容器名称
返回值: LoroTree 实例
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("fileSystem");
const root = tree.createNode();getCounter(name: string): LoroCounter获取或创建指定名称的计数器容器。计数器是专门用于累加并发增量的 CRDT;详见Counter。
参数:
name- 容器名称
返回值: LoroCounter 实例
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const counter = doc.getCounter("likes");
counter.increment(1);getMovableList(name: string): LoroMovableList获取或创建指定名称的可移动列表容器。MovableList 专为并发重排设计。参阅列表与可移动列表以及选择 CRDT 类型。
参数:
name- 容器名称
返回值: LoroMovableList 实例
示例:
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); // 将第一个条目移动到第三个位置getContainerById(id: ContainerID): Container | undefined根据唯一 ID 获取容器。容器 ID(CID)可在不同更新之间唯一定位容器;参阅容器 ID与容器。
参数:
id- 容器 ID
返回值: 找到则返回容器实例,否则为 undefined
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
const textId = text.id;
const sameText = doc.getContainerById(textId);导入 / 导出方法
export(mode?: ExportMode): Uint8Array以不同模式导出文档,用于同步或持久化。关于快照、增量、浅快照与区间更新等模式的说明,参阅导出模式。浅快照在保留当前状态的同时移除历史;详见浅快照。VersionVector 与 Frontiers 是两种表示版本的方式,可参考版本向量与Frontiers。
参数:
mode- 可选的导出配置
ExportMode 选项:
type ExportMode =
| { mode: "snapshot" }
| { mode: "update"; from?: VersionVector }
| { mode: "shallow-snapshot"; frontiers: Frontiers }
| { mode: "updates-in-range"; spans: { id: OpId; len: number }[] };返回值: 编码后的二进制数据
⚠️ 注意事项:
- 浅快照:无法导入浅快照起点之前的更新,节点需拥有之后的版本才能继续同步。
- 自动提交:导出前会自动提交待处理操作。
- 性能:建议定期导出新快照,以缩短新节点初次导入的耗时。
示例:
import { LoroDoc, VersionVector } from "loro-crdt";
const doc = new LoroDoc();
// ... 对文档进行一些修改 ...
// 导出完整快照
const snapshot = doc.export({ mode: "snapshot" });
// 基于指定版本导出增量
const lastSyncVersion = doc.version(); // 当前版本
// ... 再进行一些修改 ...
const updates = doc.export({
mode: "update",
from: lastSyncVersion,
});
// 在当前版本导出浅快照
const shallowSnapshot = doc.export({
mode: "shallow-snapshot",
frontiers: doc.frontiers(),
});import(data: Uint8Array): ImportStatus将更新或快照导入文档。返回的 ImportStatus 会说明哪些 peer 区间已应用或仍待处理。了解 Loro 如何处理乱序与部分更新,可参阅同步与导入状态。
参数:
data- 待导入的二进制数据,或来自其他 LoroDoc 的导出
⚠️ 重要提示: 导入前 LoroDoc 会自动提交尚未提交的操作。如果文档处于分离模式,导入的操作会写入 OpLog,但在调用 attach() 之前不会应用到 DocState。详见附着与分离状态与OpLog 与 DocState。
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
// 从其他节点接收更新(例如通过网络)
const otherDoc = new LoroDoc();
otherDoc.getText("text").insert(0, "Hello");
const updates: Uint8Array = otherDoc.export({ mode: "update" });
// 导入二进制更新
const status = doc.import(updates);
console.log(status.success);importBatch(data: Uint8Array[]): ImportStatus高效地批量导入多个更新。性能考量与使用方式参阅批量导入。
参数:
data- 二进制更新数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
declare const update1: Uint8Array;
declare const update2: Uint8Array;
declare const update3: Uint8Array;
// 用法示例:
const updates = [update1, update2, update3];
const status = doc.importBatch(updates);exportJsonUpdates(start?: VersionVector, end?: VersionVector, withPeerCompression?: boolean): JsonSchema以 JSON 格式导出更新,适合调试或采用不同的存储方案。格式细节与权衡请参考导出模式。
参数:
start- 起始版本(可选)end- 结束版本(可选)
返回值: 更新的 JSON 表示
示例:
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从 JSON 中导入更新,常用于调试、迁移或自定义存储层;参阅导出模式。
参数:
json- 包含更新的 JSON 字符串或对象
示例:
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);版本管理
通过 frontiers(多个头)与版本向量操作历史 DAG,可安全地切换、分支与合并版本,无需手动解决冲突。建议阅读版本深入解析及附着与分离状态。
版本控制方法
checkout(frontiers: Frontiers): void将文档检出到指定版本,使其在该历史点上变为只读。这是“时光旅行”能力的核心,可结合时光旅行与版本阅读更多背景。
参数:
frontiers- 目标版本对应的 OpId 数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const frontiers = doc.frontiers();
// 做一些修改……
doc.checkout(frontiers); // 回到之前的版本⚠️ 注意: 在 Loro 1.0 中,version() / frontiers() 会包含尚未提交的本地操作。
📝 提示: 调用 checkout() 后,文档进入只读的“分离”模式。可通过 attach() 或 checkoutToLatest() 返回可编辑状态。详见版本深入解析与附着与分离状态。
checkoutToLatest(): void在检出后回到最新版本。相关概念包括Frontiers与版本向量。
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
doc.checkoutToLatest();attach(): void使文档重新附着,以继续追踪最新变化。关于状态如何与历史分离,请参阅附着与分离状态。
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
doc.attach();detach(): void将文档切换到分离模式,停止追踪最新变化并冻结在当前版本。详见附着与分离状态。
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
doc.detach();fork(): LoroDoc创建一个从当前文档分叉而来的新文档,并生成新的 Peer ID。适用于需要创建分支的工作流,详见版本。
返回值: 新的 LoroDoc 实例
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const forkedDoc = doc.fork();forkAt(frontiers: Frontiers): LoroDoc在历史中的指定版本位置创建分叉。关于版本、DAG 历史与多个头的细节,可参考版本深入解析。
参数:
frontiers- 分叉所依据的版本
返回值: 新的 LoroDoc 实例
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const frontiers = doc.frontiers();
const forkedDoc = doc.forkAt(frontiers);事件与事务
响应变更,并将本地操作归组为事务。事件会在一个微任务之后异步派发。参见事件处理与事务模型。
订阅方法
subscribe(listener: (event: LoroEventBatch) => void): () => void订阅文档中的所有变更。事件模型及最佳实践详见事件处理。
参数:
listener- 接收变更事件的回调函数
返回值: 取消订阅函数
事件结构:
interface LoroEventBatch {
by: "local" | "import" | "checkout";
origin?: string;
currentTarget?: ContainerID;
events: LoroEvent[];
from: Frontiers;
to: Frontiers;
}示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const unsubscribe = doc.subscribe((event) => {
console.log("变更类型:", event.by);
event.events.forEach((e) => {
console.log("容器发生变化:", e.target);
console.log("差异:", e.diff);
});
});
// 稍后再取消订阅⚠️ 注意: 事件会在一个微任务完成后异步派发。
doc.commit();
await Promise.resolve();
// 现在事件已经派发完毕📝 提示: commit 之前的多个操作会合并为一个事件。详见事件处理。
subscribeLocalUpdates(f: (bytes: Uint8Array) => void): () => void仅订阅本地变更,适用于与远端 peer 同步。这通常会连接到你的传输层;参见同步。
参数:
f- 接收二进制更新的回调
返回值: 取消订阅函数
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
declare const websocket: { send: (data: Uint8Array) => void };
// 用法示例:
const unsubscribe = doc.subscribeLocalUpdates((updates) => {
// 将更新发送给远端 peer
websocket.send(updates);
});subscribeFirstCommitFromPeer(f: (e: { peer: PeerID }) => void): () => void订阅每个 peer 的首次 commit,便于追踪 peer 元数据。
参数:
f- 接收 peer 信息的回调
返回值: 取消订阅函数
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
doc.subscribeFirstCommitFromPeer(({ peer }) => {
// 存储 peer 元数据
doc.getMap("peers").set(peer, {
joinedAt: Date.now(),
name: `User ${peer}`,
});
});事务方法
commit(options?: { origin?: string, message?: string, timestamp?: number }): void将待处理的修改作为一次事务提交。事务会把多次操作打包成一个 Change;参见操作与 Change。
⚠️ 关键区别: Loro 的事务不是 ACID 数据库事务:
- 不支持回滚
- 不保证隔离性
- 目的:将本地操作打包,便于事件批量派发与历史分组
- 许多操作(import/export/checkout)会隐式触发 commit
详见事务模型。
参数:
options- 可选的提交配置message- 提交信息(会类似 git commit message 一样持久化在文档中,并在同步后对所有 peer 可见)origin- 来源标识(仅在本地使用,用于标记本地事件,远端 peer 不会看到)timestamp- 以秒为单位的 Unix 时间戳(参见存储时间戳)
重要区别:
message会持久化在文档历史中,并会同步到所有 peer,类似于 git 的提交信息origin仅用于本地过滤事件(例如从撤销中排除某些来源),不会同步到远端 peer
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
doc.commit({
message: "更新文档标题", // 会持久化并同步到所有 peer
origin: "user-action", // 仅本地,用于事件过滤
timestamp: Math.floor(Date.now() / 1000),
});查询方法
toJSON(): Value将整个文档转换为兼容 JSON 的值。如果你更倾向于让子容器通过 ID 引用(例如出于隐私或流式传输的考虑),请使用 getShallowValue();参见浅快照。
返回值: 文档的 JSON 表示
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const json = doc.toJSON();
console.log(JSON.stringify(json, null, 2));getShallowValue(): Record<string, ContainerID>获取一个浅层表示,子容器会用各自的 ID 表示。在你希望分享结构但不包含历史时非常有用;参见浅快照。
返回值: 浅层 JSON 值
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const shallow = doc.getShallowValue();
// 子容器会显示为: "cid:..."getDeepValueWithID(): any获取文档的深层值,并保留容器 ID。当你需要遍历文档结构且要维持容器 ID 引用时非常实用。
返回值: 带容器 ID 的文档值
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const deepValue = doc.getDeepValueWithID();version(): VersionVector获取文档当前的版本向量。版本向量用于追踪你从每个 peer 已接收的数据量;参见版本向量。
返回值: 从 PeerID 到计数器的映射
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const vv = doc.version();
console.log(vv.toJSON());frontiers(): Frontiers获取文档当前的 frontiers(头)。Frontiers 是一种紧凑的版本表示;关于在何时用它替代版本向量,参见Frontiers。
📝 提示: Frontiers 是一种紧凑的版本表示。
⚠️ 限制: 如果某个 Frontier 指向你尚未掌握的操作,就无法确定该版本包含的完整操作 ID 集合。版本向量没有这个限制,但会更冗长。权衡取舍详见Frontiers。
返回值: OpId 数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const frontiers = doc.frontiers();
// 可用于 checkout 或浅快照diff(from: Frontiers, to: Frontiers, for_json?: boolean): [ContainerID, Diff | JsonDiff][]计算两个版本之间的差异。理解 Loro 如何计算 diff 有赖于历史 DAG 模型;参见版本深入解析。
参数:
from- 起始 frontiersto- 结束 frontiersfor_json- 若为 true,则返回 JsonDiff 格式(默认值:true)
返回值: 由容器 ID 及其差异组成的数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const fromFrontiers = doc.frontiers();
// 进行一些更改...
const toFrontiers = doc.frontiers();
const diffs = doc.diff(fromFrontiers, toFrontiers);
diffs.forEach(([containerId, diff]) => {
console.log(`容器 ${containerId} 发生变化:`, diff);
});提交前钩子
subscribePreCommit(f: (e: { changeMeta: Change, origin: string, modifier: ChangeModifier }) => void): () => void订阅提交前事件。你可以修改下一次 Change 的 message 与 timestamp。该钩子会在记录 Change 之前触发;参见事务模型与操作与 Change。
易踩坑:commit() 可能被 import、export 与 checkout 隐式触发。使用此钩子即可为这些隐式提交附加元数据。
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const unsubscribe = doc.subscribePreCommit(({ modifier }) => {
modifier
.setMessage("由提交前钩子标记")
.setTimestamp(Math.floor(Date.now() / 1000));
});
doc.getText("text").insert(0, "Hello");
doc.commit();
unsubscribe();光标工具
getCursorPos(cursor: Cursor): { update?: Cursor, offset: number, side: Side }将稳定的 Cursor 解析为绝对位置。光标在并发编辑下仍然有效;参见光标与稳定位置与光标教程。side 参数用于控制光标位于插入边界时的贴靠方向。
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "abc");
// 获取位置 1 的光标
const c0 = text.getCursor(1);
const pos = doc.getCursorPos(c0!);
console.log(pos.offset); // 1待提交操作
getUncommittedOpsAsJson(): JsonSchema | undefined以 JSON 格式获取当前事务中的待提交操作。适合用于调试下一次 Change 会包含哪些内容;参见事务模型。
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(); // commit 之后为 undefinedChange 图与历史
这些 API 会遍历变更的历史 DAG(祖先/后代、区间)。如果对这些概念不熟悉,请先阅读 Loro 的版本深入解析与事件图遍历器。
travelChangeAncestors(ids: OpId[], f: (change: Change) => boolean): void按因果顺序访问给定变更的祖先。
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; // 继续遍历
});VersionVectorDiff ```
</Method>
<Indent>查找位于两个版本之间的操作 ID 区间。</Indent>
<Method id="LoroDoc.exportJsonInIdSpan">
```typescript no_run
exportJsonInIdSpan(idSpan: { peer: PeerID, counter: number, length: number }): JsonChange[]import { LoroDoc } from "loro-crdt";
const a = new LoroDoc();
const b = new LoroDoc();
// 用法示例:
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[]获取指定 ID 区间内被修改的容器 ID。
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);回退与应用差异
revertTo(frontiers: Frontiers): void通过生成逆向操作,将文档回退到指定版本。
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将一组 diff 应用到文档中。
import { LoroDoc } from "loro-crdt";
const doc1 = new LoroDoc();
const doc2 = new LoroDoc();
// 用法示例:
doc1.getText("text").insert(0, "Hello");
const diff = doc1.diff([], doc1.frontiers());
doc2.applyDiff(diff);分离编辑
setDetachedEditing(enable: boolean): void启用或禁用分离编辑模式。分离编辑允许你在与最新头分离的状态下暂存修改;参见附着与分离状态。
参数:
enable- 是否启用分离编辑
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
doc.setDetachedEditing(true);isDetachedEditingEnabled(): boolean检查是否已启用分离编辑模式。
返回值: 若已启用分离编辑则为 true
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const enabled = doc.isDetachedEditingEnabled();isDetached(): boolean检查文档当前是否处于分离状态。
返回值: 若文档为分离状态则为 true
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
console.log(doc.isDetached());Commit 选项辅助
setNextCommitMessage(msg: string): void设置下一次 commit 的 message。
参数:
msg- 提交信息
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
doc.setNextCommitMessage("用户操作");setNextCommitOrigin(origin: string): void设置下一次 commit 的 origin。
参数:
origin- 来源标识
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
doc.setNextCommitOrigin("ui");setNextCommitTimestamp(timestamp: number): void设置下一次 commit 的时间戳。
参数:
timestamp- 以秒为单位的 Unix 时间戳
示例:
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批量设置下一次 commit 的选项。
参数:
options- 提交选项对象
示例:
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清除所有待用的 commit 选项。
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
doc.clearNextCommitOptions();版本与 Frontier 工具
frontiersToVV(frontiers: Frontiers): VersionVector将 frontiers 转换为版本向量。
参数:
frontiers- 待转换的 frontiers
返回值: 版本向量表示
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const frontiers = doc.frontiers();
const vv = doc.frontiersToVV(frontiers);vvToFrontiers(vv: VersionVector): Frontiers将版本向量转换为 frontiers。
参数:
vv- 待转换的版本向量
返回值: frontiers 表示
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const vv = doc.version();
const frontiers = doc.vvToFrontiers(vv);oplogVersion(): VersionVector获取 OpLog 的版本向量。
返回值: OpLog 版本向量
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const vv = doc.oplogVersion();oplogFrontiers(): Frontiers获取 OpLog 的 frontiers。
返回值: OpLog frontiers
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const frontiers = doc.oplogFrontiers();cmpWithFrontiers(frontiers: Frontiers): -1 | 0 | 1比较当前文档状态与给定 frontiers 的关系。
参数:
frontiers- 用于比较的 frontiers
返回值: 若落后为 -1,持平为 0,领先为 1
示例:
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比较两个 frontiers。
参数:
a- 第一个 frontiersb- 第二个 frontiers
返回值: 若 a < b 为 -1,等于为 0,a > b 为 1,无法比较则为 undefined
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const f1 = doc.frontiers();
const f2 = doc.frontiers();
const cmp = doc.cmpFrontiers(f1, f2);JSONPath 与路径查询
使用简单路径字符串与 JSONPath 获取嵌套值和容器。路径由根容器名称与键组合而成(例如 map/key 或 list/0)。关于容器 ID,参见容器 ID。
getByPath(path: string): Value | Container | undefined根据路径获取值或容器。
参数:
path- 路径字符串(如 “map/key”)
返回值: 该路径下的值或容器,若不存在则为 undefined
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
map.set("key", 1);
// 用法示例:
const value = doc.getByPath("map/key");getPathToContainer(id: ContainerID): (string | number)[] | undefined根据容器 ID 获取其路径。
参数:
id- 容器 ID
返回值: 表示路径的数组,若无路径则为 undefined
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
const path = doc.getPathToContainer(map.id);JSONPath(jsonpath: string): any[]使用 JSONPath 语法查询文档。
参数:
jsonpath- JSONPath 查询字符串
返回值: 匹配结果数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
map.set("key", 1);
// 用法示例:
const results = doc.JSONPath("$.map");浅文档工具
这些辅助函数与浅快照及数据裁剪相关。如需回顾 “浅” 的含义,请参阅浅快照。
shallowSinceVV(): VersionVector获取文档保持浅状态所对应的版本向量。
返回值: 版本向量
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const vv = doc.shallowSinceVV();shallowSinceFrontiers(): Frontiers获取文档保持浅状态所对应的 frontiers。
返回值: frontiers
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const frontiers = doc.shallowSinceFrontiers();isShallow(): boolean检查文档是否为浅状态。
返回值: 若文档为浅状态则为 true
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const shallow = doc.isShallow();setHideEmptyRootContainers(hide: boolean): void控制 JSON 输出中是否隐藏空的根容器。
参数:
hide- 是否隐藏空的根容器
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
doc.setHideEmptyRootContainers(true);
// 现在 toJSON() 会隐藏空的根容器deleteRootContainer(cid: ContainerID): void删除一个根容器。
参数:
cid- 要删除的容器 ID
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
// 用法示例:
doc.deleteRootContainer(map.id);hasContainer(id: ContainerID): boolean检查文档中是否存在指定容器。
参数:
id- 待检查的容器 ID
返回值: 若容器存在则为 true
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
const exists = doc.hasContainer(map.id);使用 Replacer 的 JSON 序列化
toJsonWithReplacer(replacer: (key: string | number, value: Value | Container) => Value | Container | undefined): Value自定义容器与值的 JSON 序列化。
参数:
replacer- 序列化过程中用于转换值的函数
返回值: 定制化的 JSON 值
示例:
import { LoroDoc, LoroText } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello");
// 用法示例:
const json = doc.toJsonWithReplacer((key, value) => {
if (value instanceof LoroText) {
return value.toDelta();
}
return value;
});统计与自省
debugHistory(): void打印文档历史的调试信息。
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
doc.debugHistory();changeCount(): number获取文档中的变更总数。
返回值: 变更数量
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const changes = doc.changeCount();opCount(): number获取文档中的操作总数。
返回值: 操作数量
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const ops = doc.opCount();getAllChanges(): Map<PeerID, Change[]>按 peer ID 分组获取全部变更。
返回值: 从 peer ID 映射到变更数组的 Map
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const changes = doc.getAllChanges();getChangeAt(id: OpId): Change根据操作 ID 获取特定的 Change。
参数:
id- 操作 ID
返回值: Change 对象
示例:
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根据 peer ID 与 Lamport 时间戳获取 Change。
参数:
peer_id- Peer IDlamport- Lamport 时间戳
返回值: Change 对象,若不存在则为 undefined
示例:
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[]获取某个 Change 中的所有操作。
参数:
id- 操作 ID
返回值: 操作数组
示例:
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获取当前事务中待提交操作的数量。
返回值: 待提交操作数量
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
doc.getText("text").insert(0, "x");
console.log(doc.getPendingTxnLength());
doc.commit();导入/导出工具
decodeImportBlobMeta(blob: Uint8Array, check_checksum: boolean): ImportBlobMetadata从导入的二进制块中解析元数据。
参数:
blob- 待解析的二进制数据check_checksum- 是否校验校验和
返回值: 导入块的元数据
示例:
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在指定版本范围内对 JSON 更新进行涂抹。
可用来安全地从历史中移除意外泄露的敏感内容,同时保留结构。参阅技巧:数据涂抹。
参数:
json- 待涂抹的 JSON 更新version_range- 需要涂抹的版本范围
返回值: 已涂抹的 JSON Schema
示例:
import { LoroDoc, redactJsonUpdates } from "loro-crdt";
const doc = new LoroDoc();
const json = doc.exportJsonUpdates();
const redacted = redactJsonUpdates(json, { [doc.peerIdStr]: [0, 999999] });容器类型
用于建模类 JSON 结构的常见 CRDT 容器。关于如何选择适合的类型以及如何组合嵌套,参见选择 CRDT 类型与组合 CRDT。
LoroText
支持带格式协作编辑的富文本容器,可处理重叠标记(加粗、斜体、链接)与稳定光标。其合并语义能够避免并发时出现交错伪影(借鉴 Fugue 与 Eg-walker 的思路);你只需调用简单的索引 API,由 Loro 负责索引转换。更多内容请参阅文本、Eg-walker以及富文本博文:https://loro.dev/blog/loro-richtext。
⚠️ 重点:UTF-16 字符串编码
LoroText 使用 UTF-16 编码,与 JavaScript 原生字符串一致:
- 所有标准方法(
insert()、delete()、mark()、slice()、charAt())都使用 UTF-16 码元索引 length返回 UTF-16 码元数量(与 JavaScriptstring.length相同)- 在与 UTF-8 系统集成时,使用
insertUtf8()与deleteUtf8()进行基于 UTF-8 字节的操作
⚠️ 常见陷阱:
- 索引不匹配:UTF-16 索引与视觉字符数量并不一致
- 性能注意:查询已删除位置的光标需要遍历历史,此时会返回一个已刷新、且不再指向被删文本的 Cursor 对象
含 emoji 的示例:
const text = doc.getText("text");
text.insert(0, "Hello 😀 World");
console.log(text.length); // 13(emoji 记为 2)
console.log(text.toString()[6]); // ⚠️ 错误:会拆分 emoji
text.delete(6, 2); // ✅ 正确:删除整个 emoji
text.delete(6, 1); // ❌ 错误:破坏 emoji
// 安全迭代
text.iter((char) => {
console.log(char); // 每个字符都能被正确处理
return true;
});📝 在 Map 中使用 Text 还是字符串:
- 当需要保留所有并发编辑时,请使用
LoroText - 当值为原子型(URL、ID、哈希值)并倾向于最后写入生效(Last-Write-Wins)时,请在
LoroMap中使用普通字符串 - 示例:URL 应该以字符串形式存储在 Map 中,而不是使用 LoroText,否则自动合并后可能得到无效 URL
typescript no_run insert(index: number, text: string): void
在指定位置插入文本,索引基于 UTF-16 码元(与 JavaScript 字符串索引一致)。
参数:
index- 要插入的位置(0 起,UTF-16 码元索引)text- 待插入的文本
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
// 用法示例:
text.insert(0, "Hello ");
text.insert(6, "World");typescript no_run delete(index: number, len: number): void
按 UTF-16 码元删除指定位置的文本。
参数:
index- 起始 UTF-16 码元索引(与 JavaScript 字符串索引一致)len- 要删除的 UTF-16 码元数量
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello 😀 World");
text.delete(6, 2); // 删除 emoji(占 2 个 UTF-16 单位)
text.delete(5, 1); // 删除 World 前的空格mark(range: { start: number, end: number }, key: string, value: Value): void为指定文本区间应用样式。通过 configTextStyle() 可以配置标记在区间边界的扩展/停止策略;标记行为详见文本。
参数:
range- 需要设置样式的范围key- 样式属性名value- 样式属性值
示例:
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移除指定文本区间的样式。关于标记冲突的合并规则,参见文本。
参数:
range- 要移除样式的范围key- 需要移除的样式键
示例:
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>[]将文本转换为 Delta 格式(兼容 Quill)。
返回值: Delta 操作数组
示例:
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对文本应用 Delta 操作。
参数:
delta- Delta 操作数组
示例:
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使用 Myers 差分算法将当前文本更新为目标文本。
参数:
text- 新的文本内容options- 更新选项timeoutMs- 差分计算的可选超时时长useRefinedDiff- 对长文本使用精细差分以获得更好质量
示例:
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按行进行的更新,对大文本更快(精度低于 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获取可跨编辑保持有效的稳定光标位置。
参数:
pos- 文本中的位置side- 光标贴靠方向(-1、0 或 1)
返回值: Cursor 对象或 undefined
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello World");
const cursor = text.getCursor(5);
// 即使之后发生编辑,光标仍保持有效toString(): string返回纯文本字符串。
返回值: 纯文本内容
示例:
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获取指定 UTF-16 码元位置的字符。
参数:
pos- UTF-16 码元位置
返回值: 该位置上的字符
示例:
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按 UTF-16 码元索引提取文本片段。
参数:
start- 起始 UTF-16 码元索引end- 结束 UTF-16 码元索引(不包含)
返回值: 切片后的文本
示例:
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 占索引 6-8)
const slice3 = text.slice(9, 14); // "World"splice(pos: number, len: number, s: string): string用新内容替换指定位置的文本。
参数:
pos- 起始位置len- 要删除的长度s- 要插入的字符串
返回值: 被删除的文本
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello World");
// 用法示例:
const deleted = text.splice(6, 5, "Loro"); // 返回 "World"push(s: string): void在文档末尾追加文本。
参数:
s- 要追加的字符串
示例:
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遍历文本中的每一个字符。
参数:
callback- 针对每个字符调用的函数,返回 false 可终止遍历
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello");
// 用法示例:
text.iter((char) => {
console.log(char);
return true; // 继续遍历
});insertUtf8(index: number, content: string): void在指定 UTF-8 字节索引处插入文本。
参数:
index- UTF-8 字节索引content- 要插入的文本
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insertUtf8(0, "Hello");deleteUtf8(index: number, len: number): void在指定 UTF-8 字节索引处删除文本。
参数:
index- UTF-8 字节索引len- 要删除的 UTF-8 字节数
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello World");
// 用法示例:
text.deleteUtf8(6, 5); // 删除 "World"getEditorOf(pos: number): PeerID | undefined获取指定字符位置最后一次编辑者的 Peer ID。
参数:
pos- 字符位置
返回值: 最后编辑者的 PeerID,若未知则为 undefined
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello");
// 用法示例:
const editor = text.getEditorOf(0);kind(): "Text"返回容器类型。
返回值: "Text"
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
const type = text.kind(); // "Text"parent(): Container | undefined若该文本容器被嵌套,返回其父容器。
返回值: 父容器,若不存在则为 undefined
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
const text = list.insertContainer(0, doc.getText("nested"));
// 用法示例:
const parent = text.parent(); // 返回 listisAttached(): boolean检查该容器是否已附着到文档。
返回值: 若已附着则为 true
示例:
import { LoroDoc, LoroText } from "loro-crdt";
const doc = new LoroDoc();
const text = new LoroText();
const attached = text.isAttached(); // 未附着到文档前为 falsegetAttached(): LoroText | undefined获取该容器附着到文档后的实例。
返回值: 已附着的容器,若尚未附着则为 undefined
示例:
import { LoroDoc, LoroText } from "loro-crdt";
const doc = new LoroDoc();
const text = new LoroText();
const attached = text.getAttached();isDeleted(): boolean检查该容器是否已被删除。
返回值: 若已删除则为 true
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
const deleted = text.isDeleted();getShallowValue(): string获取不带标记的纯文本内容。
返回值: 纯文本字符串
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello");
// 用法示例:
const value = text.getShallowValue(); // "Hello"toJSON(): any将文本转换为 JSON 表示。
返回值: JSON 值
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello");
// 用法示例:
const json = text.toJSON(); // "Hello"readonly id: ContainerID获取该容器的唯一 ID。
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
const containerId = text.id;readonly length: number获取文本的长度,单位为 UTF-16 码元(等同于 JavaScript 的 string.length)。
⚠️ 注意: emoji 及超出基本多文种平面的字符会占用 2 个 UTF-16 单位,这会影响所有基于索引的操作:
text.insert(0, "👨👩👧👦"); // 家庭 emoji
console.log(text.length); // 11(不是 1!)- 包含 ZWJ 序列的组合 emoji示例:
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(空格 + emoji(计为 2))LoroList
面向协作数组的有序列表容器,提供基于索引的 API。并发场景下,Loro 仅重放必要的历史片段对索引进行转换(借鉴 Eg-walker)。详见列表与可移动列表、选择 CRDT 类型以及Eg-walker。
⚠️ 注意:坐标场景请用 Map 而非 List
// ❌ 错误示例:不要用 List 表示坐标
const coord = doc.getList("coord");
coord.push(10); // x
coord.push(20); // y
// 并发更新可能得到 [10, 20a, 20b],而不是 [10, 20]
// ✅ 正确示例:使用 Map 表示坐标
const coord = doc.getMap("coord");
coord.set("x", 10);
coord.set("y", 20);
// 并发更新可正确合并为 {x: 10, y: 20}insert(pos: number, value: Value | Container): void在指定位置插入一个值。
参数:
pos- 插入位置value- 要插入的值
示例:
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在指定位置插入一个新的容器。
参数:
pos- 插入位置container- 容器实例
返回值: 被插入的容器
示例:
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从列表中删除元素。
参数:
pos- 起始位置len- 删除元素的数量
示例:
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 1push(value: Value | Container): void在列表末尾追加一个值。
参数:
value- 要追加的值
示例:
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 } | undefinedimport { 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): TAppends a container to the end of the list.
参数:
container- 要追加的容器
返回值: 追加后的容器
示例:
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移除并返回最后一个元素。
返回值: 被移除的元素,若为空则为 undefined
示例:
import { LoroList } from "loro-crdt";
declare const list: LoroList;
// ---cut---
const lastItem = list.pop();get(index: number): Value | Container | undefined获取指定索引处的值。
参数:
index- 元素索引
返回值: 对应的值,若不存在则为 undefined
示例:
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获取该位置的稳定光标。
参数:
pos- 列表中的位置side- 光标贴靠方向
返回值: Cursor 对象,若无法获取则为 undefined
示例:
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)[]将列表转换为 JavaScript 数组。
返回值: 值数组
示例:
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移除列表中的所有元素。
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
list.push("a", "b", "c");
list.clear();length: number获取列表中的元素数量。
返回值: 列表长度
示例:
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"返回容器类型。
返回值: "List"
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
const type = list.kind(); // "List"toJSON(): any将列表转换为 JSON 表示。
返回值: JSON 数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
list.push(1, 2, 3);
// 用法示例:
const json = list.toJSON(); // [1, 2, 3]parent(): Container | undefined若该列表被嵌套,返回其父容器。
返回值: 父容器,若不存在则为 undefined
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
const list = map.setContainer("nested", doc.getList("list"));
// 用法示例:
const parent = list.parent(); // 返回 mapisAttached(): boolean检查该容器是否已附着到文档。
返回值: 若已附着则为 true
示例:
import { LoroDoc, LoroList } from "loro-crdt";
const doc = new LoroDoc();
const list = new LoroList();
const attached = list.isAttached(); // 未附着到文档前为 falsegetAttached(): LoroList | undefined获取该容器附着到文档后的实例。
返回值: 已附着的容器,若尚未附着则为 undefined
示例:
import { LoroDoc, LoroList } from "loro-crdt";
const doc = new LoroDoc();
const list = new LoroList();
const attached = list.getAttached();isDeleted(): boolean检查容器是否已被删除。
返回值: 若已删除则为 true
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
const deleted = list.isDeleted();getShallowValue(): Value[]获取列表值,其中子容器会以各自的 ID 表示。
返回值: 值数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
list.push(1, 2);
// 用法示例:
const values = list.getShallowValue(); // [1, 2]readonly id: ContainerID获取该容器的唯一 ID。
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
const containerId = list.id;LoroMap
用于协作对象的键值映射容器。参见Map 教程。
set(key: string, value: Value | Container): void设置一个键值对。
注意:将键设置为相同的值不会产生操作(no-op)。详见Map 基础。
参数:
key- 键value- 值
示例:
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将容器作为指定键的值。
⚠️ 易错点: 多个 peer 在同一个键上并发创建子容器会互相覆盖(不同容器 ID 无法自动合并)。参见容器初始化。
参数:
key- 键container- 容器实例
返回值: 设置后的容器
示例:
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获取指定键对应的值。
参数:
key- 键
返回值: 对应的值,若不存在则为 undefined
示例:
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获取已存在的容器,或在不存在时创建新的容器。
⚠️ 易错点: 不同 peer 在同一键上并发创建容器会互相覆盖。参见容器初始化。
参数:
key- 键container- 当不存在时要创建的容器
返回值: 对应的容器
示例:
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删除一个键值对。
参数:
key- 需要移除的键
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
map.delete("obsoleteKey");clear(): void清空所有键值对。
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
map.clear();keys(): string[]获取 Map 中的所有键。
返回值: 键组成的数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
const allKeys = map.keys();values(): (Value | Container)[]获取 Map 中的所有值。
返回值: 值组成的数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
const allValues = map.values();entries(): [string, Value | Container][]获取所有键值对。
返回值: 形如 [key, value] 的数组
示例:
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获取指定键最后编辑该键的 Peer ID。
参数:
key- 键
返回值: 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.peerIdStrsize: number获取 Map 中键值对的数量。
返回值: Map 的大小
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
console.log(`Map has ${map.size} entries`);kind(): "Map"返回容器类型。
返回值: "Map"
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
const type = map.kind(); // "Map"toJSON(): any将 Map 转换为 JSON 表示。
返回值: JSON 对象
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
map.set("name", "Alice");
// 用法示例:
const json = map.toJSON(); // { name: "Alice" }parent(): Container | undefined若该 Map 被嵌套,返回其父容器。
返回值: 父容器,若不存在则为 undefined
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
const map = list.insertContainer(0, doc.getMap("nested"));
// 用法示例:
const parent = map.parent(); // 返回 listisAttached(): boolean检查该容器是否已附着到文档。
返回值: 若已附着则为 true
示例:
import { LoroDoc, LoroMap } from "loro-crdt";
const doc = new LoroDoc();
const map = new LoroMap();
const attached = map.isAttached(); // 未附着到文档前为 falsegetAttached(): LoroMap | undefined获取该容器附着到文档后的实例。
返回值: 已附着的容器,若尚未附着则为 undefined
示例:
import { LoroDoc, LoroMap } from "loro-crdt";
const doc = new LoroDoc();
const map = new LoroMap();
const attached = map.getAttached();isDeleted(): boolean检查容器是否已被删除。
返回值: 若已删除则为 true
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
const deleted = map.isDeleted();getShallowValue(): Record<string, Value>获取 Map 的值,其中子容器会以各自的 ID 表示。
返回值: 含值的对象
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
map.set("key", "value");
// 用法示例:
const values = map.getShallowValue(); // { key: "value" }readonly id: ContainerID获取该容器的唯一 ID。
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
const containerId = map.id;LoroTree
用于表示嵌套结构的层级树容器,支持在并发编辑下安全移动子树。详见树。
⚠️ 树操作的重要提示:
- 并发移动可能造成环:Loro 会自动检测并阻止
- 分数索引:可能出现插入交错,但能保持相对顺序
- 需要保持兄弟顺序时,请勿禁用分数索引,详见树。
createNode(parent?: TreeID, index?: number): LoroTreeNode创建一个新的树节点。
参数:
parent- 父节点 ID(可选,省略则创建根节点)index- 在同级中的位置(可选)
返回值: 新节点的句柄
示例:
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将节点移动到新的位置。
参数:
target- 需要移动的节点 IDparent- 新的父节点(对根节点可为 undefined)index- 在同级中的插入位置
示例:
import { LoroDoc, TreeID } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
declare const nodeId: TreeID;
declare const newParentId: TreeID;
// 用法示例:
tree.move(nodeId, newParentId, 0);delete(target: TreeID): void删除一个节点及其所有子节点。
参数:
target- 要删除的节点 ID
示例:
import { LoroDoc, TreeID } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
const nodeId = node.id;
// 用法示例:
tree.delete(nodeId);getNodeByID(id: TreeID): LoroTreeNode | undefined根据节点 ID 获取节点句柄。
参数:
id- 节点 ID
返回值: 节点句柄,若不存在则为 undefined
示例:
import { LoroDoc, TreeID } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const _node = tree.createNode();
const nodeId = _node.id;
// 用法示例:
const node = tree.getNodeByID(nodeId);
if (node) {
node.data.set("label", "New Label");
}nodes(): LoroTreeNode[]获取树中的所有节点。
返回值: 包含全部节点的数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const allNodes = tree.nodes();roots(): LoroTreeNode[]获取所有根节点。
返回值: 根节点数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const rootNodes = tree.roots();has(target: TreeID): boolean检查某个节点是否存在。
参数:
target- 要检查的节点 ID
返回值: 布尔值,表示是否存在
示例:
import { LoroDoc, TreeID } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
declare const nodeId: TreeID;
// 用法示例:
if (tree.has(nodeId)) {
console.log("Node exists");
}isNodeDeleted(target: TreeID): boolean检查节点是否已被删除。
参数:
target- 要检查的节点 ID
返回值: 布尔值,表示删除状态
示例:
import { LoroDoc, TreeID } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
declare const nodeId: TreeID;
// 用法示例:
if (tree.isNodeDeleted(nodeId)) {
console.log("Node was deleted");
}enableFractionalIndex(jitter: number): void启用分数索引,以提升并发移动性能。
参数:
jitter- 分数索引使用的抖动量
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
tree.enableFractionalIndex(0.001);disableFractionalIndex(): void禁用分数索引。
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
tree.disableFractionalIndex();isFractionalIndexEnabled(): boolean检查是否已启用分数索引。
返回值: 若已启用则为 true
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const enabled = tree.isFractionalIndexEnabled();kind(): "Tree"返回容器类型。
返回值: "Tree"
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const type = tree.kind(); // "Tree"toJSON(): any将树转换为 JSON 表示。
返回值: JSON 树结构
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const json = tree.toJSON();parent(): Container | undefined若该树被嵌套,返回其父容器。
返回值: 父容器,若不存在则为 undefined
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
const tree = map.setContainer("tree", doc.getTree("nested"));
// 用法示例:
const parent = tree.parent();isAttached(): boolean检查该容器是否已附着到文档。
返回值: 若已附着则为 true
示例:
import { LoroDoc, LoroTree } from "loro-crdt";
const doc = new LoroDoc();
const tree = new LoroTree();
const attached = tree.isAttached();getAttached(): LoroTree | undefined获取该容器附着到文档后的实例。
返回值: 已附着的容器,若尚未附着则为 undefined
示例:
import { LoroDoc, LoroTree } from "loro-crdt";
const doc = new LoroDoc();
const tree = new LoroTree();
const attached = tree.getAttached();isDeleted(): boolean检查该容器是否已被删除。
返回值: 若已删除则为 true
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const deleted = tree.isDeleted();getShallowValue(): TreeNodeShallowValue[]获取树的值,其中子容器会以 ID 表示。
返回值: 树节点值数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const values = tree.getShallowValue();readonly id: ContainerID获取该容器的唯一 ID。
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const containerId = tree.id;LoroTreeNode
表示树中的单个节点。
data: LoroMap用于存储节点元数据的 Map 容器。
示例:
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创建一个子节点。
参数:
index- 在同级中的位置
返回值: 新节点的句柄
示例:
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将当前节点移动到新的父节点下。
参数:
parent- 新的父节点index- 在同级中的位置
示例:
import { LoroDoc, LoroTreeNode } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
const parent = tree.createNode();
// 用法示例:
node.move(parent, 0);moveAfter(target: LoroTreeNode): void将当前节点移动到某个兄弟节点之后。
参数:
target- 兄弟节点
示例:
import { LoroDoc, LoroTreeNode } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
const sibling = tree.createNode();
// 用法示例:
node.moveAfter(sibling);moveBefore(target: LoroTreeNode): void将当前节点移动到某个兄弟节点之前。
参数:
target- 兄弟节点
示例:
import { LoroDoc, LoroTreeNode } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
const sibling = tree.createNode();
// 用法示例:
node.moveBefore(sibling);parent(): LoroTreeNode | undefined获取父节点。
返回值: 父节点,若无则为 undefined
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
// 用法示例:
const parentNode = node.parent();children(): LoroTreeNode[]获取所有子节点。
返回值: 子节点数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
// 用法示例:
const childNodes = node.children();index(): number | undefined获取在同级中的位置。
返回值: 索引值,若为根节点则为 undefined
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
// 用法示例:
const position = node.index();fractionalIndex(): string | undefined返回用于确定性排序兄弟节点的分数索引。该值是分数索引的十六进制字符串表示,能够稳定地维持顺序。根节点会返回 undefined。注意:树必须已附着到文档。
返回值: 十六进制字符串,根节点则为 undefined
示例:
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 acreationId(): { peer: PeerID, counter: number }返回创建该节点的 OpID。
返回值: { peer: PeerID, counter: number } 类型的创建标识
示例:
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返回创建该节点的 Peer ID(等同于 creationId().peer)。
返回值: PeerID
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
const author = node.creator();
// author == doc.peerIdStrgetLastMoveId(): { peer: PeerID, counter: number } | undefined返回此节点最近一次移动操作的 OpID;若从未移动则为 undefined。
返回值: 移动操作的 OpID,或 undefined
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
const lastMove = node.getLastMoveId();isDeleted(): boolean检查当前节点是否已被删除。
返回值: 布尔值,表示删除状态
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
const node = tree.createNode();
// 用法示例:
if (node.isDeleted()) {
console.log("Node is deleted");
}LoroCounter
用于协作数值的计数器 CRDT。
increment(value: number): void递增计数器。
参数:
value- 增量(默认 1)
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const counter = doc.getCounter("counter");
counter.increment(5); // +5decrement(value: number): void递减计数器。
参数:
value- 减量(默认 1)
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const counter = doc.getCounter("counter");
counter.decrement(3); // -3value: number获取当前计数值。
返回值: 当前数值
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const counter = doc.getCounter("counter");
console.log(`Counter value: ${counter.value}`);LoroMovableList
针对移动操作优化的列表,适合频繁重排(拖放)且能在并发移动下保持良好行为(最终收敛到单一位置)。详见列表与可移动列表。
📝 MovableList 与 List 的对比:
- 推荐使用 MovableList 场景:拖拽 UI、可排序列表、看板
- 继续使用 List 场景:列表项无需移动
- 关键差异:MovableList 更好地处理并发移动(无重复项)并支持 set 操作,List 在通用场景下更高效。
move(from: number, to: number): void将元素从一个位置移动到另一个位置。
参数:
from- 源索引to- 目标索引
示例:
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); // 将第一个元素移动到第 4 个位置set(pos: number, value: Value | Container): void替换指定位置上的值。
参数:
pos- 需要更新的位置value- 新值
示例:
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将指定位置的值替换为容器。
参数:
pos- 需要更新的位置container- 新容器
返回值: 设置后的容器
示例:
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());同步
通过任意传输方式导入/导出更新,并选择合适的编码以兼顾速度和体积。详见 Sync Tutorial 与 Encoding & Export Modes。
- 导入顺序:Loro 会自动处理乱序更新。
- 自动提交:导入和导出操作会触发自动提交。
导入/导出模式
基础同步
import { LoroDoc } from "loro-crdt";
// 节点 A:导出更新
const doc1 = new LoroDoc();
doc1.getText("text").insert(0, "Hello");
const updates = doc1.export({ mode: "update" });
// 节点 B:导入更新
const doc2 = new LoroDoc();
doc2.import(updates);
// 此时 doc2.getText("text").toString() === "Hello"持续同步
import { LoroDoc } from "loro-crdt";
const doc1 = new LoroDoc();
const doc2 = new LoroDoc();
// 使用示例:
// 建立双向同步
doc1.subscribeLocalUpdates((updates) => {
doc2.import(updates);
});
doc2.subscribeLocalUpdates((updates) => {
doc1.import(updates);
});性能提示:
- 优先搭配
mode: "update"与VersionVector,实现增量同步。 - 当只需要当前状态时使用
mode: "shallow-snapshot",它会剥离历史记录以加快导入/加载。 - Loro 基于 LSM 的编码与受 Eg-walker 启发的合并算法,即使历史很长也能保持快速导入/导出。详见 Encoding 与 v1.0 性能说明:https://loro.dev/blog/v1.0
使用 WebSocket 的网络同步
import { LoroDoc } from "loro-crdt";
// 假设我们已有:
declare const ws: {
send: (data: Uint8Array) => void;
on: (event: string, handler: (data: any) => void) => void;
};
// 客户端
const doc = new LoroDoc();
// 将本地更新发送到服务器
doc.subscribeLocalUpdates((updates) => {
ws.send(updates);
});
// 接收来自服务器的更新
ws.on("message", (data) => {
doc.import(new Uint8Array(data));
});浅快照
浅快照通过清理已删除的操作来提升存储效率。
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
// 创建浅快照
const frontiers = doc.frontiers();
const shallowSnapshot = doc.export({
mode: "shallow-snapshot",
frontiers: frontiers,
});
// 导入浅快照
const newDoc = new LoroDoc();
newDoc.import(shallowSnapshot);版本控制
时光回溯
在文档历史中穿梭:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
// 保存当前版本
const v1 = doc.frontiers();
// 进行修改
doc.getText("text").insert(0, "New text");
// 保存新版本
const v2 = doc.frontiers();
// 回到旧版本
doc.checkout(v1);
console.log(doc.getText("text").toString()); // Original text
// 前往新版本
doc.checkout(v2);
console.log(doc.getText("text").toString()); // New text
// 回到最新状态
doc.checkoutToLatest();分叉
创建文档分支:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
// 在当前状态下分叉
const fork1 = doc.fork();
fork1.getText("text").insert(0, "Fork 1 changes");
// 在指定版本分叉
const historicalVersion = doc.frontiers();
const fork2 = doc.forkAt(historicalVersion);
fork2.getText("text").insert(0, "Fork 2 changes");
// 原始文档保持不变版本向量
追踪文档版本:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const vv = doc.version();
console.log(`Document has ${vv.length()} peers`);事件与订阅
事件结构
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 类型
不同容器会产出不同的 diff 类型:
TextDiff
type TextDiff = {
type: "text"
diff: Delta<string>[]
}使用 Delta 格式表示文本内容的变更。
属性:
type- 文本 diff 时恒为 “text”diff- Delta 操作数组(插入、删除、保留)
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
// 示例
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)[]>[]
}使用 Delta 格式表示列表内容的变更。
属性:
type- 列表 diff 时恒为 “list”diff- 针对列表项的 Delta 操作数组
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
// 示例
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>
}表示映射内容的变更。
属性:
type- 映射 diff 时恒为 “map”updated- 保存键值变更(值为 undefined 表示已被删除)
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const map = doc.getMap("map");
// 示例
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
}
表示树结构的变更。
属性:
type- 树 diff 时恒为 “tree”diff- TreeDiffItem 操作数组(创建、删除、移动)
TreeDiffItem 操作:
create- 包含父节点与位置的节点创建delete- 带有旧父节点和位置的节点删除move- 携带旧/新位置的节点移动
示例:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const tree = doc.getTree("tree");
// 示例
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;
}
});
}
}
});深度订阅
订阅嵌套变更:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
// 订阅特定容器
const text = doc.getText("text");
text.subscribe((event) => {
console.log("Text changed:", event);
});
// 订阅并启用深度观察
doc.subscribe((event) => {
// path 表示变更发生的位置
event.events.forEach((e) => {
console.log("Change path:", e.path);
console.log("Container:", e.target);
console.log("Diff:", e.diff);
});
});撤销/重做
本地撤销仅作用于你自己的更改,不会破坏协同编辑。详见 Undo/Redo 了解设计细节与注意事项。
UndoManager
提供本地撤销/重做能力。
⚠️ 注意事项:
- 仅限本地:UndoManager 只会撤销本地用户的操作,不会影响远端操作。
- 来源过滤:使用
excludeOriginPrefixes可将特定操作(如同步产生的操作)排除在撤销之外。 - 光标恢复:借助
onPush/onPop回调保存并恢复光标位置。
constructor(doc: LoroDoc, config: UndoConfig)创建新的 UndoManager 实例。
参数:
doc- 需要管理撤销/重做的 LoroDocconfig- 配置项mergeInterval?- 合并连续操作的时间窗口(毫秒,默认 1000)maxUndoSteps?- 撤销栈的最大步数(默认 100)excludeOriginPrefixes?- 要从撤销中排除的 origin 前缀数组onPush?- 入栈时触发的回调onPop?- 撤销/重做时触发的回调
示例:
import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {
mergeInterval: 1000,
maxUndoSteps: 100,
excludeOriginPrefixes: ["sync-"],
});undo(): boolean撤销最近一次操作。
返回值: 撤销成功时返回 true
示例:
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();
// 使用示例:
const success = undo.undo();
console.log(success); // trueredo(): boolean重做最近一次被撤销的操作。
返回值: 重做成功时返回 true
示例:
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();
// 使用示例:
const success = undo.redo();
console.log(success); // truecanUndo(): boolean检查是否可以执行撤销。
返回值: 可撤销时返回 true
示例:
import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
// 使用示例:
if (undo.canUndo()) {
undo.undo();
}canRedo(): boolean检查是否可以执行重做。
返回值: 可重做时返回 true
示例:
import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
// 使用示例:
if (undo.canRedo()) {
undo.redo();
}peer(): PeerID获取 UndoManager 所属的节点 ID。
返回值: 节点 ID
示例:
import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
// 使用示例:
const peerId = undo.peer();
console.log(peerId); // e.g., "123456"setMaxUndoSteps(steps: number): void设置可保留的最大撤销步数。
参数:
steps- 最大撤销步数
示例:
import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
// 使用示例:
undo.setMaxUndoSteps(50);setMergeInterval(interval: number): void设置分组合并操作的时间窗口。
参数:
interval- 以毫秒为单位的合并间隔
示例:
import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
// 使用示例:
undo.setMergeInterval(2000); // 2 secondsgroupStart(): void开始手动分组,将后续提交聚合成单个撤销步骤。
行为:
- 将连续的
doc.commit()包裹为一次撤销。 - 在
groupEnd之前再次调用groupStart会抛错,并保持当前分组不变。 - 若遇到冲突的远端导入,可能会自动结束分组并拆分撤销项。
示例:
import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
const text = doc.getText("text");
undo.groupStart();
text.update("hello", undefined);
doc.commit();
text.update("hello world", undefined);
doc.commit();
undo.groupEnd();
undo.undo();
console.log(text.toString()); // ""groupEnd(): void结束当前手动分组,并将组合操作入队为一个撤销项。
行为:
- 必须与之前的
groupStart配对使用。 - 若分组因冲突的远端导入被自动关闭,再次调用会成为空操作且不会报错。
- 其他节点的不冲突远端更新不会纳入撤销项,也不会破坏该分组。
示例:
import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
const text = doc.getText("text");
undo.groupStart();
text.update("hello", undefined);
doc.commit();
text.update("hello world", undefined);
doc.commit();
const snapshot = doc.export({ mode: "snapshot" });
const doc2 = new LoroDoc();
doc2.import(snapshot);
doc2.getText("text2").update("hello world world", undefined); // 作用于不同的容器
doc2.commit();
const update = doc2.export({ mode: "update" });
doc.import(update); // 引入远端且无冲突的变更
text.update("hello world world world", undefined);
doc.commit();
undo.groupEnd();
undo.undo();
console.log(text.toString()); // ""addExcludeOriginPrefix(prefix: string): void将指定前缀加入撤销忽略列表。
参数:
prefix- 需要排除的 origin 前缀
示例:
import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
// 使用示例:
undo.addExcludeOriginPrefix("sync-");
undo.addExcludeOriginPrefix("import-");clear(): void清空撤销栈与重做栈。
示例:
import { LoroDoc, UndoManager } from "loro-crdt";
const doc = new LoroDoc();
const undo = new UndoManager(doc, {});
// 使用示例:
undo.clear();自定义撤销处理
处理光标恢复及其他副作用:
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) => {
// 入栈时保存光标位置
const cursors = saveCursorPositions();
return {
value: doc.toJSON(),
cursors: cursors,
};
},
onPop: (isUndo, { value, cursors }, counterRange) => {
// 撤销时恢复光标位置
restoreCursorPositions(cursors);
},
});类型与接口
列出 API 中使用的核心类型。相关概念请参阅 Containers、Version Vector、Frontiers 以及 Versioning Deep Dive。
核心类型
// 节点标识符
type PeerID = `${number}`;
// 容器标识符
type ContainerID =
| `cid:root-${string}:${ContainerType}`
| `cid:${number}@${PeerID}:${ContainerType}`;
// 树节点标识符
type TreeID = `${number}@${PeerID}`;
// 操作标识符
type OpId = {
peer: PeerID;
counter: number;
};
// 容器类型
type ContainerType =
| "Text"
| "Map"
| "List"
| "Tree"
| "MovableList"
| "Counter";
// 值类型
type Value =
| ContainerID
| string
| number
| boolean
| null
| { [key: string]: Value }
| Uint8Array
| Value[]
| undefined;版本类型
Loro 提供两种互补的版本表示:版本向量(按节点计数)以及 frontiers(紧凑的头集合)。参见 Version Vector 与 Frontiers,了解完整 DAG 模型请访问 Version Deep Dive。
// 版本向量类
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 表示具体版本
type Frontiers = OpId[];
// 范围查询使用的 ID 区间
type IdSpan = {
peer: PeerID;
counter: number;
length: number;
};变更类型
// 变更元数据
interface Change {
peer: PeerID;
counter: number;
lamport: number;
length: number;
timestamp: number; // Unix timestamp in seconds
deps: OpId[];
message: string | undefined;
}
// 预提交钩子使用的变更修饰器
interface ChangeModifier {
setMessage(message: string): this;
setTimestamp(timestamp: number): this;
}光标类型
稳定光标能够在并发编辑下保留,并可按需解析到绝对位置。参见 Cursor and Stable Positions 以及 Cursor tutorial。
// 容器中的稳定位置
class Cursor {
containerId(): ContainerID;
pos(): OpId | undefined;
side(): Side; // -1 | 0 | 1
encode(): Uint8Array;
static decode(data: Uint8Array): Cursor;
}
// 光标侧向偏好
type Side = -1 | 0 | 1;Delta 类型
Delta 是常见的富文本操作格式(如 Quill)。LoroText 支持 Delta 的导入/导出,详情见 Text。
// 富文本 Delta 操作
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;
};工具函数
用于类型检查与 ID 处理的辅助方法。关于 frontiers/版本编码,可参考 Container IDs 与 Versioning Deep Dive。
Frontier 编码
encodeFrontiers(frontiers: OpId[]): Uint8Array将 frontiers 编码以便高效传输。
参数:
frontiers- 表示前沿的操作 ID 数组
返回值: 编码后的字节
示例:
import { LoroDoc, encodeFrontiers } from "loro-crdt";
const doc = new LoroDoc();
const frontiers = doc.frontiers();
const encoded = encodeFrontiers(frontiers);
// 将编码结果发送给远端节点decodeFrontiers(bytes: Uint8Array): OpId[]从字节数据解码 frontiers。
参数:
bytes- 编码后的 frontier 字节
返回值: 操作 ID 数组
示例:
import { decodeFrontiers } from "loro-crdt";
declare const encodedData: Uint8Array;
const frontiers = decodeFrontiers(encodedData);
console.log(frontiers); // [{ peer: "1", counter: 10 }, ...]调试
setDebug(): void开启调试模式,输出更详细的日志。
示例:
import { setDebug } from "loro-crdt";
// 启用调试日志
setDebug();LORO_VERSION(): string获取当前的 Loro 版本。
返回值: 版本字符串
示例:
import { LORO_VERSION } from "loro-crdt";
const version = LORO_VERSION();
console.log("Loro version:", version);导入 Blob 元数据
decodeImportBlobMeta(blob: Uint8Array, check_checksum: boolean): ImportBlobMetadata解析导入 blob 中的元数据。
参数:
blob- 导入 blob 的字节数据check_checksum- 是否校验校验和
返回值: 导入 blob 的元数据
示例:
import { decodeImportBlobMeta } from "loro-crdt";
declare const blob: Uint8Array;
const metadata = decodeImportBlobMeta(blob, true);
console.log("Blob metadata:", metadata);EphemeralStore
用于管理光标位置、在线状态等临时数据。概念与用法示例详见 Ephemeral Store。每条记录都依赖基于时间戳的 LWW(Last-Write-Wins)来解决冲突。
⚠️ 注意:
- EphemeralStore 是独立的 CRDT,不保留历史;历史和操作不会被持久化。
- 非常适合存储光标位置、选区、输入指示等临时状态。
- 每个节点的状态会在超时时间后自动过期。
- 采用 Last-Write-Wins 规则。
EphemeralStore
constructor(timeout?: number)创建新的 EphemeralStore 实例。
参数:
timeout- 超时时长(毫秒)。当节点的最后更新时间早于该超时值时,其状态被视为过期。默认 30000 毫秒(30 秒)。
示例:
import { EphemeralStore } from "loro-crdt";
// 创建一个 30 秒超时的临时存储
const store = new EphemeralStore(30000);set<K extends keyof T>(key: K, value: T[K]): void设置临时值。
参数:
key- 要设置的键value- 要存储的值
示例:
import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
// 使用示例:
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读取临时值。
参数:
key- 要读取的键
返回值: 存储的值或 undefined
示例:
import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
store.set("cursor", { line: 10 });
// 使用示例:
const cursor = store.get("cursor");
console.log(cursor); // { line: 10 }delete<K extends keyof T>(key: K): void删除临时值。
参数:
key- 要删除的键
示例:
import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
store.set("cursor", { line: 10 });
// 使用示例:
store.delete("cursor");getAllStates(): Partial<T>获取所有临时状态。
返回值: 已存储的全部状态
示例:
import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
store.set("cursor", { line: 10 });
store.set("user", { name: "Alice" });
// 使用示例:
const allStates = store.getAllStates();
console.log(allStates);encode<K extends keyof T>(key: K): Uint8Array编码指定键的状态,以便传输。
参数:
key- 要编码的键
返回值: 编码后的字节
示例:
import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
store.set("cursor", { line: 10 });
// 使用示例:
const encoded = store.encode("cursor");
// 将编码结果发送给远端节点encodeAll(): Uint8Array编码全部状态,以便传输。
返回值: 编码后的字节
示例:
import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
store.set("cursor", { line: 10 });
store.set("user", { name: "Alice" });
// 使用示例:
const encoded = store.encodeAll();
// 将编码结果发送给远端节点apply(bytes: Uint8Array): void应用远端更新。
参数:
bytes- 来自远端节点的编码更新
示例:
import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
// 使用示例:
declare const remoteData: Uint8Array;
store.apply(remoteData);keys(): string[]获取存储中的所有键。
返回值: 键名数组
示例:
import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
store.set("cursor", { line: 10 });
store.set("user", { name: "Alice" });
// 使用示例:
const allKeys = store.keys();
console.log(allKeys); // ["cursor", "user"]subscribe(listener: EphemeralListener): () => void订阅全部临时状态的变更。
参数:
listener- 处理状态变更的回调
返回值: 取消订阅函数
示例:
import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
// 使用示例:
const unsubscribe = store.subscribe((event) => {
console.log("Ephemeral state changed:", event);
});
// 稍后可取消订阅
unsubscribe();subscribeLocalUpdates(listener: EphemeralLocalListener): () => void订阅本地临时更新,以便同步到远端节点。
参数:
listener- 接收编码更新的回调函数
返回值: 取消订阅函数
示例:
import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
// 使用示例:
declare const websocket: { send: (data: Uint8Array) => void };
const unsubscribe = store.subscribeLocalUpdates((data) => {
// 发送给远端节点
websocket.send(data);
});
// 稍后可取消订阅
unsubscribe();destroy(): void清理并销毁临时存储。
示例:
import { EphemeralStore } from "loro-crdt";
const store = new EphemeralStore(30000);
// 使用示例:
store.destroy();完整示例
import { EphemeralStore } from "loro-crdt";
// 假设我们已有:
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);
// 订阅本地更新
store.subscribeLocalUpdates((data) => {
store2.apply(data);
});
// 订阅全部更新
store2.subscribe((event) => {
console.log("event:", event);
});
// 设置一个值
store.set("key", "value");
// 编码该值
const encoded = store.encode("key");
// 应用编码结果
store2.apply(encoded);