关于 live-cursors
RivetKit 中实时光标与在线状态的示例,面向构建协作界面的开发者。它提供基于类型化操作与事件的实现,以及可自定义 JSON WebSocket 协议的实现。
主要功能
- 01房间级光标共享
- 02连接级临时状态
- 03光标移动与离开通知
- 04支持原始 WebSocket 协议
- 05持久化文本标签
适用场景
- 01协作画布显示其他用户的光标
- 02在多人房间同步在线状态
- 03无需 RivetKit 客户端库的 WebSocket 通信
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rivet-dev/ skills --skill live-cursors安装后在 Claude Code 中输入/live-cursors确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
BASE_SKILL.mdindex.jsonopenapi.jsonreference/actors/access-control.mdactions.mdactor-runtime-socket.mdappearance.mdauthentication.mdcommunicating-between-actors.mdconnections.mddebugging.mddesign-patterns.mddestroy.mderrors.mdevents.mdfetch-and-websocket-handler.mdhelper-types.mdhttp-api.mdinput.mdinspector-tabs.mdkeys.mdkv.mdlifecycle.mdlimits.mdmetadata.mdqueues.mdreference/actors/quickstart/backend.mdcloudflare.mdeffect.mdnext-js.md详细介绍
实时光标与在线状态
重要:在进行任何操作之前,必须先阅读此技能目录中的 BASE_SKILL.md。其中包含有关调试、错误处理、状态管理、部署和项目设置的重要指导。所有 RivetKit 工作都必须遵循这些规则和模式。以下内容均假设你已阅读并理解该文件。
可运行示例
如需参考实现,请阅读以下模板中的原始可运行示例代码:
使用 RivetKit 构建实时光标、多人在线状态和实时光标共享的模式。一个房间 Actor 会将光标位置广播给所有已连接的客户端,并通过 Actor 键按房间进行键控。
起始代码
从 GitHub 上两个可运行的变体中任选一个开始。两者都实现了同一个协作式光标画布,并支持持久化文本标签;它们的区别仅在于传输方式。
| 变体 | 起始代码 | 传输方式 | 在线状态存储 |
|---|---|---|---|
cursors | GitHub | 通过 RivetKit 连接传输类型化操作和事件 | 每个连接的 connState |
cursors-raw-websocket | GitHub | 使用自定义 JSON 消息协议的原始 onWebSocket 处理程序 | createVars 中的套接字映射 |
默认情况下使用 cursors:类型化操作、类型化事件和自动连接跟踪可以用更少的代码满足大多数应用的需求。需要完全控制传输格式时使用 cursors-raw-websocket,例如需要自定义 JSON 或二进制协议,或者客户端不使用 RivetKit 客户端库的情况。
连接状态与持久化状态
在线状态本质上是临时的。光标位置只有在连接存活时才有意义,因此应存储在每个连接的数据中,而不是持久化的 Actor 状态中。持久化状态用于存储必须在断开连接和 Actor 重启后保留的数据。
| 数据 | 存储位置 | 原因 |
|---|---|---|
| 光标位置 | connState(cursors)或 createVars 套接字映射(cursors-raw-websocket) | 仅限于单个连接,并会随连接一同丢弃。过期的在线状态不会在存储中累积。 |
文本标签(textLabels) | 两个变体中的持久化 Actor state | 画布内容必须在断开连接和 Actor 重启后保留。 |
在 cursors 变体中,updateCursor 会写入 c.conn.state.cursor,而 getRoomState 会通过遍历 c.conns.values() 重建在线状态快照,因此光标映射始终根据活动连接生成,而不是存储起来。有关 connState,请参阅连接;有关持久化语义,请参阅状态。
在线状态生命周期
- 加入:
cursors-raw-websocket变体会在套接字连接后立即推送包含当前{ cursors, textLabels }快照的init消息。cursors变体没有显式的加入广播;客户端连接后会调用一次getRoomState操作,以初始化本地映射。其他用户会在新用户首次广播cursorMoved时看到该用户。 - 移动:每次调用
updateCursor时,都会写入该连接的在线状态条目,然后向所有连接(包括发送方)广播cursorMoved。 - 离开:
cursors变体会在onDisconnect中处理离开事件,并广播该连接最后的光标位置对应的cursorRemoved。原始变体则在套接字的close监听器中执行相同操作,然后从vars.websockets映射中删除会话。客户端会从本地光标映射中删除该用户,因此标签页关闭时,过期光标会立即消失。
有关 onDisconnect 和 createVars,请参阅生命周期。
更新频率限制
两个示例都没有进行频率限制。两个前端都会在每次原始 mousemove 事件发生时发送一次光标更新,既不防抖,也不限制发送间隔。这对于演示来说没问题,但在高刷新率显示器上快速移动鼠标时,每位用户每秒可能产生数百个事件。以下模式是在入门代码基础上建议的生产环境加固措施,并非示例中已实现的功能。
| 层级 | 模式 | 指引 |
|---|---|---|
| 客户端(平滑度) | 限制为 20-30Hz | 每隔 33-50ms 采样最新的指针位置,并且只发送该位置。丢弃中间位置的移动,但始终发送最终位置,确保光标停在实际位置。在渲染端对收到的位置进行插值。 |
| 服务端(强制执行) | 按连接进行速率限制 | 记录每个连接上次接受更新的时间戳,并丢弃或合并超过上限频率到达的更新。客户端的频率限制属于协作措施;执行边界在 actor。 |
Actors
-
键:
cursorRoom[roomId](前端默认将roomId设为"general") -
职责:在
connState中保存每个连接的光标状态,在 actor 状态中持久化共享文本标签,并向所有连接广播光标和文本更新。 -
操作
updateCursorupdateTextremoveTextgetRoomState
-
事件
cursorMovedcursorRemovedtextUpdatedtextRemoved
-
队列
- 无
-
状态
- JSON
textLabels(持久化)- 每个连接的
connState.cursor(临时)
-
键:
cursorRoom[roomId](通过client.cursorRoom.getOrCreate(roomId)解析) -
职责:公开原始 WebSocket 端点,在以
sessionId查询参数为键的createVars映射中跟踪活动套接字及其光标,持久化文本标签,并手动将 JSON 帧扇出到每个套接字。 -
操作
getOrCreate(返回{ status: "ok" }的存根;前端通过客户端句柄的getOrCreate(roomId).resolve()解析 actor ID,该操作会创建 actor,但不会分派此操作)getRoomState
-
队列
- 无
-
状态
- JSON
textLabels(持久化)vars.websockets中以sessionId为键、映射到套接字和光标的映射(驻留内存,重启后丢失)
原始变体不定义任何 RivetKit 事件。其消息名称是原始 JSON 帧中的 type 字段:
| 方向 | 消息 type | 载荷 |
|---|---|---|
| 客户端到服务端 | updateCursor | { userId, x, y } |
| 客户端到服务端 | updateText | { id, userId, text, x, y } |
| 客户端到服务端 | removeText | { id } |
| 服务端到客户端 | init | 连接时的 { cursors, textLabels } 快照 |
| 服务端到客户端 | cursorMoved, textUpdated, textRemoved, cursorRemoved | 相应的光标、标签或 ID 载荷 |
生命周期
cursors(操作 + 事件)
sequenceDiagram
participant A as Client A
participant R as cursorRoom
participant B as Other Clients
A->>R: connect via useActor (cursorRoom[roomId])
A->>R: getRoomState()
R-->>A: {cursors, textLabels}
loop every mouse move
A->>R: updateCursor(userId, x, y)
Note over R: write c.conn.state.cursor
R-->>B: cursorMoved (broadcast)
end
A->>R: updateText(id, userId, text, x, y)
Note over R: upsert persistent state.textLabels
R-->>B: textUpdated (broadcast)
Note over A: tab closes
Note over R: onDisconnect reads conn.state.cursor
R-->>B: cursorRemoved (broadcast)
cursors-raw-websocket
sequenceDiagram
participant A as Client A
participant R as cursorRoom
participant B as Other Clients
A->>R: getOrCreate(roomId).resolve()
R-->>A: actorId
A->>R: open WebSocket /gateway/{actorId}/websocket?sessionId=...
Note over R: close 1008 if sessionId is missing
Note over R: store socket in vars.websockets
R-->>A: init {cursors, textLabels}
loop every mouse move
A->>R: {type: "updateCursor"} frame
Note over R: update session cursor in vars
R-->>B: cursorMoved frame
end
Note over A: socket closes
R-->>B: cursorRemoved frame
Note over R: delete session from vars.websockets
安全检查清单
两个示例均未启用身份验证,以便清晰展示在线状态模式。以下所有内容都是生产环境中建议采取的加固措施,并非示例已实现的行为。
- 身份:将在线状态身份绑定到连接(actions 变体中使用
c.conn.id,原始变体中使用服务器生成的会话 ID)。绝不信任客户端提供的userId;示例中该值是客户端随机生成的字符串,因此任何客户端都可以冒充其他用户或移除任意光标。 - 授权:根据所有者授权标签修改操作。在示例中,
updateText接受任意id和userId参数,而removeText接受任意id,因此任何客户端都可以编辑或删除任意标签。 - 输入验证:将
x和y限制在画布范围内,限制文本标签长度,并限制textLabels的总数,避免持久化状态无限增长。 - 速率限制:按连接限制
updateCursor(例如 30Hz)和标签写入的频率,详见更新节流。 - 协议严格性(原始变体):使用消息前先验证其结构;遇到格式错误的 JSON 时关闭套接字,而非记录错误后继续处理。拒绝重复的
sessionId,不要静默覆盖其他会话的套接字条目。
参考索引
Actors
- 访问控制
- Actions
- Actor 键
- Actor 运行时套接字
- Actor 状态
- 身份验证
- Cloudflare Workers 快速入门
- Actor 之间的通信
- 连接
- 自定义检查器标签页
- 调试
- 设计模式
- 销毁 Actor
- Effect.ts 快速入门(Beta)
- 错误
- Fetch 和 WebSocket 处理器
- 辅助类型
- 图标和名称
- 内存状态
- 输入参数
- 生命周期
- 限制
- 低级 HTTP 请求处理器
- 低级 KV 存储
- 低级 WebSocket 处理器
- 元数据
- Next.js 快速入门
- Node.js 和 Bun 快速入门
- 队列和运行循环
- React 快速入门
- 实时功能
- Rust 快速入门(Beta)
- 扩展和并发
- 计划任务和 Cron
- 共享和加入状态
- SQLite
- SQLite + Drizzle
- Supabase Functions 快速入门
- 测试
- 故障排除
- 类型
- 原生 HTTP API
- 版本和升级
- 工作流
CLI
客户端
实用指南
部署
- 容器运行器
- 部署到 Amazon Web Services Lambda
- 部署到 AWS ECS
- 部署到 Cloudflare Workers
- 部署到 Freestyle
- 部署到 Google Cloud Run
- 部署到 Hetzner
- 部署到 Kubernetes
- 部署到 Railway
- 部署到 Rivet Compute
- 部署到 Supabase Functions
- 部署到 Vercel
- 部署到虚拟机和裸金属服务器