Rivet Actors 实时聊天室实战:Actor 状态管理、事件广播与多房间隔离完整实现
2026/9/18 9:59:47 网站建设 项目流程

Rivet Actors 实时聊天室实战:Actor 状态管理、事件广播与多房间隔离完整实现

【免费下载链接】actorsRivet Actors are the primitive for stateful workloads. Built for AI agents, collaborative apps, and durable execution.项目地址: https://gitcode.com/GitHub_Trending/riv/actors

实时聊天室是验证有状态工作负载能力的典型场景:需要瞬时把消息推送给所有在线客户端,需要让历史记录在进程重启后依然完整,还需要让不同房间的数据互不干扰。仓库中的 examples/chat-room 示例项目,正是用 Rivet Actors 把这些诉求落地的完整参考实现。本文以此项目为主体,结合仓库内 RivetKit SDK 的源码与测试,逐层拆解 Actor 定义、SQLite 持久化、事件广播、客户端接入与测试验证的完整链路,帮助你把同样的模式迁移到自己的协作类应用中。

项目全景:一个聊天室由哪些部分组成

先看 examples/chat-room 的目录结构,理解一个 Actor 示例项目的完整形态:

examples/chat-room/ ├── src/ │ └── index.ts # 服务端:Actor 定义 + registry 注册 + 服务启动 ├── frontend/ │ ├── App.tsx # React 前端:useActor + useEvent 实时订阅 │ └── main.tsx # React 入口 ├── tests/ │ └── chat-room.test.ts # vitest 集成测试 ├── package.json # dev / test / build 脚本与依赖 ├── vite.config.ts # 开发代理(/actors、/metadata、/health) ├── vitest.config.ts ├── tsconfig.json ├── turbo.json # 依赖 @rivetkit/react、rivetkit 的构建顺序 ├── Dockerfile # 多阶段镜像构建(server + static 资源) ├── index.html └── README.md

整个项目只有一份服务端入口 src/index.ts,其余是前端与工程化配置。它的数据流模型非常简洁:

  1. 客户端通过 React 的createRivetKit建立与 Actor 服务器的连接;
  2. Action是客户端可远程调用的函数,负责写入状态(如sendMessage);
  3. Event是服务端向所有已连接客户端主动推送的广播(如newMessage);
  4. SQLite DB是 Actor 的持久化存储,保证历史消息在重启后不丢失。

这一「Action 写入 + Event 广播 + DB 持久化」的三段式模型,正是 Rivet Actors 处理有状态实时交互的核心范式,也贯穿本文后续所有小节。

快速启动:五分钟跑起聊天室

根据 examples/chat-room/README.md 的说明,本项目使用concurrently同时启动服务端与前端:

git clone https://github.com/rivet-dev/rivet.git cd rivet/examples/chat-room npm install npm run dev

对应到仓库中 examples/chat-room/package.json 的脚本定义:

{ "name": "chat-room", "version": "2.0.21", "type": "module", "scripts": { "dev": "concurrently -n server,vite \"tsx --watch src/index.ts\" \"vite\"", "dev:server": "tsx --watch src/index.ts", "check-types": "tsc --noEmit", "test": "vitest run", "build": "vite build", "start": "tsx src/index.ts" }, "dependencies": { "@rivetkit/react": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0", "rivetkit": "workspace:*" } }

脚本含义如下:

  • npm run dev:用tsx --watch热重载服务端,同时启动 Vite 开发服务器,日志以server/vite两个前缀区分;
  • npm run dev:server:只启动 Actor 服务端,适合调试后端逻辑;
  • npm run test:运行 vitest 集成测试(见后文「测试验证」小节);
  • npm run build:构建前端静态资源;
  • npm run start:以生产方式运行服务端。

启动后,Actor 服务器监听在http://localhost:6420(端口由 src/index.ts 中registry.start()与 frontend/App.tsx 中createRivetKit("http://localhost:6420")共同约定)。开发期的跨域与 WebSocket 代理由 vite.config.ts 处理:

server: { clearScreen: false, proxy: { "/actors": { target: "http://localhost:6420", ws: true }, "/metadata": { target: "http://localhost:6420" }, "/health": { target: "http://localhost:6420" }, }, },

其中/actors走 WebSocket(ws: true)——这正是实时事件推送的通道;/metadata/health是 HTTP 端点,分别用于获取 Actor 元信息与健康检查。若在仓库中直接使用,注意rivetkit@rivetkit/react采用workspace:*工作区依赖,需在仓库根目录执行pnpm install建立 workspace 链接后再运行示例。

服务端实现:用 actor() 定义有状态消息实体

聊天室的服务端全部逻辑集中在 src/index.ts,它展示了 Rivet Actors 三个最核心的抽象:db(持久化)、events(广播)、actions(远程调用)。

消息类型与 SQLite 持久化(db)

首先定义一个消息的数据结构,并给 Actor 挂上一个 SQLite 数据库,用它来持久化聊天记录:

import { actor, event, setup } from "rivetkit"; import { db } from "rivetkit/db"; export type Message = { sender: string; text: string; timestamp: number }; export const chatRoom = actor({ // Persist chat history in the actor's SQLite database db: db({ onMigrate: async (db) => { await db.execute(` CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, sender TEXT NOT NULL, text TEXT NOT NULL, timestamp INTEGER NOT NULL ) `); }, }), // ... });

要点拆解:

  • db({ onMigrate })为 Actor 声明一个内置 SQLite 数据库,onMigrate在 Actor 首次初始化时执行,负责建表等迁移逻辑。这里创建了messages表:id自增主键,sender/text为消息内容,timestamp为毫秒级时间戳;
  • 该表保存在 Actor 自己的持久化存储中,因此即使 Actor 实例休眠或重启,聊天历史也能恢复——这是「Persistent chat history」特性的底层来源;
  • 在 rivetkit-typescript/packages/rivetkit/src/actor/definition.ts 中可以看到actor()定义的完整形态:BaseActorDefinition携带config,内部通过flattenActionHandlersflattenActionInputSchemas等工具把actions/events/db等配置归一化,最终由setup()注册为可寻址的 Actor 类型。

事件广播(events):服务端主动推送给客户端

事件是「客户端被实时通知」的关键机制,声明方式与使用方式如下:

events: { newMessage: event<Message>(), },

sendMessage中通过c.broadcast("newMessage", message)把新消息推送给所有已连接的客户端。每个事件声明为强类型:event<Message>()让广播内容与前端订阅回调的类型完全对齐,编译期即可校验负载结构,避免手写字符串协议带来的类型漂移。

动作(actions):客户端可远程调用的函数

actions是客户端可以直接跨网络调用的函数,聊天室定义了两个核心动作:

actions: { sendMessage: async (c, sender: string, text: string) => { const message: Message = { sender, text, timestamp: Date.now() }; await c.db.execute( "INSERT INTO messages (sender, text, timestamp) VALUES (?, ?, ?)", sender, text, message.timestamp, ); // Send events to all connected clients c.broadcast("newMessage", message); return message; }, getHistory: async (c) => { const rows = await c.db.execute( "SELECT sender, text, timestamp FROM messages ORDER BY id ASC", ); return rows as Message[]; }, },
  • sendMessage(sender, text):先写入 SQLite,再通过c.broadcast("newMessage", message)广播,最后把消息对象返回给调用方。注意写入与广播的顺序——先落库、后广播,保证任何重放或重连后客户端拉取到的历史与实时流是一致的;
  • getHistory():按id ASC读出全部消息,作为新客户端加入时的「初始历史」。前端在建立连接后首先调用它渲染历史,再用useEvent增量追加新消息,二者配合实现无缝的聊天记录加载。

注册与启动(setup + start)

最后把 Actor 注册进 registry 并启动服务:

export const registry = setup({ use: { chatRoom }, }); // Start the server on port 6420 registry.start();
  • setup({ use: { chatRoom } })声明该服务器对外暴露的 Actor 类型集合,export它以便前端createRivetKit<typeof registry>与测试setupTest(ctx, registry)复用同一份类型与运行时;
  • registry.start()启动 HTTP + WebSocket 服务,默认端口 6420。

前端实现:useActor 订阅实时消息流

前端是纯 React,入口在 frontend/main.tsx,业务逻辑在 frontend/App.tsx。

建立类型安全的连接

import { createRivetKit } from "@rivetkit/react"; import type { Message, registry } from "../src/index.ts"; const { useActor } = createRivetKit<typeof registry>("http://localhost:6420");

createRivetKit<typeof registry>直接把服务端registry的类型带到前端:此后useActor({ name: "chatRoom", ... })返回的connection上会自动推导出sendMessagegetHistory的签名与newMessage事件的负载类型。前后端共享同一份类型定义,是这种架构在大型协作项目中最有价值的收益。

接入 Actor 与实时事件

export function App() { const [roomId, setRoomId] = useState("general"); const [username, setUsername] = useState("User"); const [input, setInput] = useState(""); const [messages, setMessages] = useState<Message[]>([]); const chatRoom = useActor({ name: "chatRoom", key: [roomId], }); useEffect(() => { if (chatRoom.connection) { chatRoom.connection.getHistory().then(setMessages); } }, [chatRoom.connection]); chatRoom.useEvent("newMessage", (message: Message) => { setMessages((prev) => [...prev, message]); }); // ... }

两个关键 Hook 的行为:

  • useActor({ name: "chatRoom", key: [roomId] }):按key定位/创建 Actor 实例并建立连接,key变化(如切换房间名)会重建连接。返回的connection是已连通的 RPC 句柄,未连通时为null
  • useEvent("newMessage", handler):订阅服务端广播,收到新消息后以函数式setMessages增量追加,天然避免闭包捕获过期状态的问题。

发送消息与界面渲染

const sendMessage = async () => { if (chatRoom.connection && input.trim()) { await chatRoom.connection.sendMessage(username, input); setInput(""); } };

界面层把消息按sender/text/timestamp渲染,时间戳通过new Date(msg.timestamp).toLocaleTimeString()本地化展示;输入框与发送按钮在connection尚未建立时处于disabled状态,防止连接未就绪时的无效调用。这套「连接状态驱动 UI 可用性」的处理方式,同样适用于生产级协作应用。

多房间隔离:key 驱动的 Actor 实例模型

README 明确指出:「Multiple chat rooms: Each room is a separate actor instance with isolated state」。这个特性的实现机制在前端调用中可见一斑:

const chatRoom = useActor({ name: "chatRoom", key: [roomId], });
  • name是 Actor 类型key是同一类型下的实例标识;name + key共同决定一个唯一的 Actor 实例;
  • 不同roomId(如"general""design")会解析到不同的 Actor 实例,每个实例拥有自己独立的 SQLite 数据库与事件连接集合——这就是「隔离状态」的来源:房间 A 的消息不会出现在房间 B 的历史里,房间 A 的广播也只到达房间 A 的客户端;
  • 同一roomId的多个客户端连接到同一个实例,因此能共享同一份messages表并同时收到newMessage广播。

测试中也印证了这一点:client.chatRoom.getOrCreate(["test-room"])getOrCreate(["test-timestamps"])使用不同的 key,得到的是相互独立的实例(详见 tests/chat-room.test.ts)。这种「一个逻辑实体 = 一个 Actor 实例」的映射,是 Rivet Actors 相对传统无状态服务的关键差异:状态归属清晰、天然支持按 key 的水平切分。

测试验证:用 vitest 覆盖核心行为

examples/chat-room/tests/chat-room.test.ts 通过rivetkit/testsetupTest启动 Actor 运行时进行集成测试,覆盖了聊天室的核心契约:

import { setupTest } from "rivetkit/test"; import { expect, test } from "vitest"; import { registry } from "../src/index.ts"; test("Chat room can handle message sending and history", async (ctx) => { const { client } = await setupTest(ctx, registry); const room = client.chatRoom.getOrCreate(["test-room"]); // Test initial state const initialHistory = await room.getHistory(); expect(initialHistory).toEqual([]); // Send a message const message1 = await room.sendMessage("Alice", "Hello everyone!"); expect(message1).toMatchObject({ sender: "Alice", text: "Hello everyone!", timestamp: expect.any(Number), }); // Send another message, verify order const message2 = await room.sendMessage("Bob", "Hi Alice!"); const history = await room.getHistory(); expect(history).toHaveLength(2); expect(history[0]).toEqual(message1); expect(history[1]).toEqual(message2); });

这套测试还覆盖了另外三个行为维度:

  1. 时间戳单调递增:连续发送三条消息,断言message2.timestamp >= message1.timestamp,并遍历历史保证整体有序;
  2. 多用户支持:Alice / Bob / Charlie 交替发言后,历史长度为 4,且sendertext顺序与发送顺序完全一致;
  3. 空消息边界:发送空字符串""也能正常落库并返回(text为空、timestamp > 0),说明动作层对参数没有隐式的非空校验,边界行为明确。

这些断言直接验证了 README 中「Real-time messaging」「Persistent chat history」「Multiple chat rooms」三个核心特性的可测试性,也为你在自己的 Actor 上添加测试提供了模板:setupTest(ctx, registry)返回一个类型安全的clientclient.<actorName>.getOrCreate([key])即得到一个可调用的 Actor 句柄。

容器化部署:多阶段构建与静态托管

examples/chat-room/Dockerfile 提供了把「Actor 服务器 + 前端静态资源」打包进一个镜像的参考做法:

# Build stage FROM node:22-alpine AS builder WORKDIR /app RUN corepack enable && corepack prepare pnpm@latest --activate COPY package.json ./ RUN pnpm install --frozen-lockfile=false COPY . . RUN pnpm run build # Runtime stage FROM node:22-alpine AS runtime WORKDIR /app RUN corepack enable && corepack prepare pnpm@latest --activate COPY package.json ./ RUN pnpm install --prod --frozen-lockfile=false COPY --from=builder /app/dist ./dist COPY --from=builder /app/public ./dist/public EXPOSE 8080 ENV PORT=8080 ENV NODE_ENV=production CMD ["node_modules/.bin/srvx", "dist/server.js"]

关键点:

  • 多阶段构建:builder 阶段安装完整依赖(含 devDependencies)并执行pnpm run build;runtime 阶段只安装生产依赖,并复制构建产物dist与前端静态资源public,镜像体积更小;
  • 启动方式:用srvx同时托管dist/server.js(Actor 服务器)与dist/public(前端静态文件),对外暴露 8080 端口,生产环境只需一个容器;
  • 工作目录约定:注释指出srvx--static路径相对 CWD(/app)解析,因此public/映射到/app/public/,构建时须保证静态资源输出到该位置。

需要说明的是:本示例以npm脚本作为开发入口,Docker 采用pnpm安装依赖;两者在 package.json 与 Dockerfile 中并存,是示例仓库的既有配置,直接照搬时请按你的包管理器选择其一。

总结:把聊天室模式迁移到你的有状态应用

从 examples/chat-room 可以提炼出一套可复用的 Actor 应用骨架:

需求Rivet Actors 对应能力本示例中的体现
实时推送events+c.broadcastnewMessage事件广播
历史持久化db+ SQLite 迁移messages表与onMigrate
多租户/多房间隔离actor({ name, key })实例模型房间名作为 key
客户端远程调用actionssendMessage/getHistory
类型安全全链路typeof registry共享类型createRivetKit<typeof registry>
自动化验证setupTest+ vitest顺序、时间戳、多用户、空消息用例

把「房间」换成「会话」「租户」「文档」「游戏对局」中的任意一个,把messages表换成对应的业务表,就能得到一套具备实时协作、持久化与隔离能力的有状态服务。仓库中的 docs/actors 文档体系(actions / state / events / sqlite 等主题)可作为继续深入每一层能力的阅读入口,而 rivetkit-typescript/packages/rivetkit 下的 SDK 源码则是验证行为细节的第一手资料。

【免费下载链接】actorsRivet Actors are the primitive for stateful workloads. Built for AI agents, collaborative apps, and durable execution.项目地址: https://gitcode.com/GitHub_Trending/riv/actors

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询