mcpField
C
collaborative-text-editor

collaborative-text-editor

github.com/rivet-dev开发441.1k 次安装1 天前更新
代码仓库

关于 collaborative-text-editor

基于 RivetKit 与 Yjs 的协作文本编辑示例,适合开发共享文档的开发者。每份文档由独立 Actor 中继 CRDT 更新,并保存快照,同时转发光标与在线状态。

主要功能

  1. 01Yjs CRDT 文档同步
  2. 02按文档划分 Actor
  3. 03二进制快照持久化
  4. 04转发光标与在线状态
  5. 05工作区文档索引

适用场景

  1. 01构建多人共享笔记或文档
  2. 02在断线重连后合并本地编辑
  3. 03为文档添加远程光标与在线状态

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rivet-dev/skills --skill collaborative-text-editor

安装后在 Claude Code 中输入/collaborative-text-editor确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

BASE_SKILL.md
index.json
openapi.json
reference/actors/
access-control.md
actions.md
actor-runtime-socket.md
appearance.md
authentication.md
communicating-between-actors.md
connections.md
debugging.md
design-patterns.md
destroy.md
errors.md
events.md
fetch-and-websocket-handler.md
helper-types.md
http-api.md
input.md
inspector-tabs.md
keys.md
kv.md
lifecycle.md
limits.md
metadata.md
queues.md
reference/actors/quickstart/
backend.md
cloudflare.md
effect.md
next-js.md

详细介绍

协作文本文档编辑器

重要:开始任何操作之前,你必须先阅读此技能目录中的 BASE_SKILL.md。其中包含有关调试、错误处理、状态管理、部署和项目设置的重要指导。这些规则和模式适用于所有 RivetKit 工作。以下内容均假设你已阅读并理解这些规则。

工作示例

如需参考实现,请阅读以下模板中的原始可运行示例代码:

在 RivetKit 上构建 Yjs 服务的模式:CRDT 文档同步、在线状态与光标,以及快照持久化;每个文档由一个 Rivet Actor 充当中继。

入门代码

从 GitHub 上的可运行示例开始,并根据你的编辑器进行改造。该示例附带一个 React 前端,包含纯文本区域、远程光标叠加层和工作区文档索引。

使用场景入门代码常见示例
共享文档编辑GitHub类似 Notion 的文档、共享笔记、协作写作工具、表单协同编辑

CRDT 与 OT

有两类算法可以解决并发文本编辑问题。选择哪一种决定了你的服务端需要做什么。

维度CRDT(Yjs)操作转换
冲突解决模型可交换合并。更新可以在任意对等端按任意顺序应用,并最终收敛到相同结果。服务端会针对每个并发操作转换每项操作。正确性取决于中央排序器。
离线支持强。客户端可以在本地持续编辑,并在重新连接时合并缓冲的更新。弱。长期分歧会使转换链变得复杂且脆弱。
服务端职责中继加持久化。服务端应用不透明更新并将其广播出去,无需理解文档语义。权威转换器。服务端必须为每种操作类型实现转换逻辑。
库的成熟度Yjs 已成熟且广泛部署,并支持 ProseMirror、CodeMirror、Monaco 等绑定。生产级实现大多是专有的(Google Docs)或已显陈旧(ShareDB)。

该示例使用 Yjs,因为 CRDT 可以让服务端保持为中继式 Rivet Actor。Actor 会将每个传入更新应用到服务端的 Y.Doc,以便持久化合并后的状态并为后来加入的用户提供数据,但它不会转换操作,也不会仲裁冲突。由于 Yjs 合并具有可交换性,因此顺序并不重要。

文档 Actor 模型

主题摘要
拓扑结构每个文档对应一个 document[workspaceId, documentId] Actor,每个工作区对应一个 documentList[workspaceId] 协调器。
同步模型每个客户端都持有一个本地 Y.Doc。文档 Actor 通过广播 事件 转发增量 Yjs 更新,并在 vars 中保留一份服务端合并副本。
持久化每次同步更新时,将完整合并后的 Yjs 快照覆盖写入一个二进制 Actor KV 键(yjs:doc)。文档元数据存储在 JSON 状态 中。
队列无。此示例仅使用 操作 和广播事件。
在线状态Yjs Awareness 通过同一个 applyUpdate 操作转发。每个连接的 connState 会跟踪该连接声明的 awareness clientIds,以便断开连接时清理。

这两个 Actor 的拆分遵循 设计模式 中的协调器模式:协调器负责发现和创建,每个文档 Actor 负责一个文档的实时状态。多段式 键 将两个 Actor 都限定在一个工作区内。

Actor

  • 键:document[workspaceId, documentId]

  • 职责:将传入的同步和 awareness 更新应用到服务端的 Y.Doc 和 Awareness,将合并后的 Yjs 快照持久化到 Actor KV,并向所有已连接的协作者广播更新。

  • 操作

    • getContent
    • applyUpdate
    • getAwareness
  • 队列

    • 无
  • 状态

    • 仅包含 JSON 元数据:title、createdAt、updatedAt
    • 二进制 KV 键 yjs:doc,用于保存完整合并后的 Yjs 快照
    • 临时 vars:实时的 Y.Doc 和 Awareness,在 createVars 中创建,并在 Actor 启动时从 KV 重新加载
    • 每个连接的 connState:该连接声明的 awareness 客户端的 clientIds
  • 键:documentList[workspaceId]

  • 职责:一个工作区的协调器。通过 Actor 间客户端创建文档 Actor,并维护文档摘要索引。

  • 操作

    • createDocument
    • listDocuments
    • deleteDocument
  • 队列

    • 无
  • 状态

    • JSON
    • documents 数组,包含 DocumentSummary 条目(id、title、createdAt、updatedAt)

协调器的 createDocument 会生成一个 UUID,然后使用 c.client<typeof registry>() 显式创建文档 Actor,并传入 { title, createdAt } 作为创建输入,由文档 Actor 的 createState 使用。关于 Actor 间客户端,请参阅在 Actor 之间通信。

更新中继

一个 applyUpdate(update, kind, clientId?) 操作可处理这两种更新。更新以 number[] 字节数组的形式跨越操作边界,并在每一端转换回 Uint8Array。

类型服务端应用到持久化广播
"sync"通过 Y.applyUpdate 应用到 c.vars.doc,来源标记为 "client"将完整合并快照写入 KV 键 yjs:doc,然后更新 updatedAt携带增量更新的 sync 事件
"awareness"通过 applyAwarenessUpdate 应用到 c.vars.awareness,来源标记为 "client"不持久化。在线状态是临时的。携带更新的 awareness 事件

请注意,sync 分支存在不对称之处:广播只携带较小的增量更新,而 KV 写入会存储使用 Y.encodeStateAsUpdate 重新编码后的完整合并文档。

Yjs 来源标签可防止中继循环:

来源标签设置位置效果
"local"客户端在 doc.transact(..., "local") 中进行编辑客户端的更新监听器会触发,并向 actor 发送 applyUpdate。
"client"服务端将传入的更新应用到其 Y.Doc 或 Awareness 时在服务端副本上将更改标记为源自客户端。
"remote"客户端应用广播事件或初始同步数据时更新监听器会在遇到 "remote" 时提前返回,因此客户端不会重新发送自己的回显。

连接或重新连接时,客户端会调用 getContent 和 getAwareness,然后将两者的结果以来源标记 "remote" 应用到本地 Y.Doc 和 Awareness。此后,所有更改都会通过 applyUpdate 和广播事件流转。

Awareness 与在线状态

在线状态(用户名、颜色、光标位置)通过 Yjs Awareness 协议传递,而非存储在 actor 状态中:

  • 客户端使用 awareness.setLocalStateField 设置 user 和 cursor 字段的在线状态。Awareness 更新监听器会对更改进行编码,并发送 applyUpdate(update, "awareness", awareness.clientID)。
  • actor 会将每个声明的 clientId 记录到该连接的 connState.clientIds,将更新应用到服务端的 Awareness,并向所有对等端广播 awareness 事件。有关每个连接的状态,请参阅连接。
  • onDisconnect 会读取连接的 clientIds,在服务端的 Awareness 上调用 removeAwarenessStates,并广播编码后的移除信息,以便所有剩余客户端移除离开用户的光标。有关此钩子的说明,请参阅生命周期。

由于 actor 会跟踪哪些 awareness clientId 属于哪个连接,因此断开连接时会自动清理在线状态,无需客户端配合。

持久化与压缩

此示例使用完整快照覆盖进行持久化:每次收到 "sync" 更新时,actor 都会通过 Y.encodeStateAsUpdate 重新编码整个合并后的文档,并覆盖单个二进制 KV 键 yjs:doc。这里没有只追加的更新日志,也没有单独的压缩任务。压缩是隐式完成的,因为 Y.encodeStateAsUpdate 会生成文档的单个紧凑合并表示,因此 Yjs 的合并语义会自动让存储的 blob 保持紧凑。

属性完整快照覆盖(此示例)
写入成本每次同步更新都要写入一次完整文档 KV,因此每次按键都会重写整个 blob。
读取成本actor 启动时,在 createVars 中读取一次二进制 KV,以恢复文档。
崩溃安全性最近一次完成的 applyUpdate 已持久化。无需重放日志。
适用场景中小型文档,简单性比写入放大更重要。

建议的扩展(此示例未包含):对于大型文档或编辑频率极高的情况,可改为将增量更新追加到 KV 更新日志,并仅定期写入合并快照(例如每 N 次更新写入一次)。启动时先加载快照,再重放日志;快照写入则是显式的压缩步骤,会截断日志。只有在完整快照写入经测量成为瓶颈,或 blob 接近 KV 值大小限制时,才采用此方案。

生命周期

sequenceDiagram
	participant A as Client A
	participant B as Client B
	participant DL as documentList
	participant D as document

	A->>DL: listDocuments()
	A->>DL: createDocument(title)
	DL->>D: create([workspaceId, documentId], input)
	DL-->>A: DocumentSummary
	A->>D: connect
	B->>D: connect
	Note over D: createVars rehydrates Y.Doc from KV "yjs:doc"
	A->>D: getContent() + getAwareness()
	D-->>A: encoded doc + awareness state
	Note over A: local edit with origin "local"
	A->>D: applyUpdate(update, "sync")
	Note over D: apply with origin "client", overwrite KV snapshot
	D-->>B: sync event (incremental update)
	Note over B: apply with origin "remote", no echo
	A->>D: applyUpdate(update, "awareness", clientId)
	D-->>B: awareness event
	B-->>D: disconnect
	Note over D: onDisconnect removes B's awareness clientIds
	D-->>A: awareness event (removal)

安全检查清单

此示例未提供身份验证或授权功能。投入生产前,请按照以下基线要求加固。示例中均未实现这些措施。

  • 连接前进行身份验证:任何知道或猜到工作区 ID 的人都能连接;由于 useActor 会隐式执行 getOrCreates,使用不存在的工作区 ID 连接会悄无声息地创建一个空白的 documentList 协调器。添加连接身份验证,确保未经身份验证的客户端无法访问 actor。请参阅身份验证。
  • 按文档进行访问控制:验证经过身份验证的用户有权访问特定的 [workspaceId, documentId] 键,而不只是任意文档。
  • 限制 applyUpdate 的大小和频率:更新负载是未经验证的 number[] 数组,没有大小限制;示例客户端会在每次按键和光标移动时发送一个操作,且完全没有节流。请在服务端限制负载大小并按连接实施速率限制,同时在客户端进行防抖。
  • 不要信任客户端声明的 awareness clientIds:applyUpdate 的 clientId 参数由客户端提供,并会被直接信任。应从连接范围内的服务端状态中推导或验证在线状态身份。
  • 删除时销毁 actor 和 KV:deleteDocument 只会从协调器的索引中移除该条目。文档 actor 及其 KV 快照会成为孤立数据。删除时还应销毁文档 actor 及其存储,并检查执行删除的用户是否具有相应权限。

参考索引

Actors
命令行工具
客户端
教程集
部署
通用
自托管

评论

支持 Markdown