mcpField
L
live-cursors

live-cursors

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

关于 live-cursors

RivetKit 中实时光标与在线状态的示例,面向构建协作界面的开发者。它提供基于类型化操作与事件的实现,以及可自定义 JSON WebSocket 协议的实现。

主要功能

  1. 01房间级光标共享
  2. 02连接级临时状态
  3. 03光标移动与离开通知
  4. 04支持原始 WebSocket 协议
  5. 05持久化文本标签

适用场景

  1. 01协作画布显示其他用户的光标
  2. 02在多人房间同步在线状态
  3. 03无需 RivetKit 客户端库的 WebSocket 通信

安装

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

$npx skills add https://github.com/rivet-dev/skills --skill live-cursors

安装后在 Claude Code 中输入/live-cursors确认已加载。

包含的文件

除 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 构建实时光标、多人在线状态和实时光标共享的模式。一个房间 Actor 会将光标位置广播给所有已连接的客户端,并通过 Actor 键按房间进行键控。

起始代码

从 GitHub 上两个可运行的变体中任选一个开始。两者都实现了同一个协作式光标画布,并支持持久化文本标签;它们的区别仅在于传输方式。

变体起始代码传输方式在线状态存储
cursorsGitHub通过 RivetKit 连接传输类型化操作和事件每个连接的 connState
cursors-raw-websocketGitHub使用自定义 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 状态中持久化共享文本标签,并向所有连接广播光标和文本更新。

  • 操作

    • updateCursor
    • updateText
    • removeText
    • getRoomState
  • 事件

    • cursorMoved
    • cursorRemoved
    • textUpdated
    • textRemoved
  • 队列

    • 无
  • 状态

    • 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
CLI
客户端
实用指南
部署
通用
自托管

评论

支持 Markdown