Rivet Actors 状态管理实战:从本地开发到 Render 云端部署(state-render 示例全解析)
【免费下载链接】actorsRivet Actors are the primitive for stateful workloads. Built for AI agents, collaborative apps, and durable execution.项目地址: https://gitcode.com/GitHub_Trending/riv/actors
Rivet Actors 是面向 AI Agent、协同应用与持久化执行场景的有状态工作负载原语。本指南以仓库中的examples/state-render示例为核心,讲解 RivetKit 中 Actor 状态的自动持久化与恢复机制、类型安全的state/actions/events声明方式,以及如何通过 Hono 生产级 HTTP 服务器、Vite 构建和render.yamlBlueprint 将同一个示例一键部署到 Render。读完本文,你将掌握"本地开发 → 云端联调 → 自动化部署"的完整链路,并能直接复用这套模式到自己的项目。
背景:state-render与state示例的关系
examples/state-render是 examples/state 示例的 Render 优化版本。它的定位差异在 README 开头的注释中写得很明确:
- 增加了生产级 HTTP 服务器(基于 Hono + @hono/node-server);
- 引入Vite 构建流程,把 React 前端编译为静态资源;
- 提供
render.yamlBlueprint,用于一键部署到 Render 平台。
而examples/state侧重于本地开发与测试:它通过registry.start()直接在进程内启动 Actor 运行时,并配有 examples/state/tests/chat.test.ts 的 Vitest 测试套件。两个示例共享同一套核心业务代码——chatRoom聊天室 Actor,区别仅在于运行与交付方式。
注意:原 README 中的
cd rivet/examples/state路径在本文所属仓库中对应的是examples/state-render目录,本文以下命令均以该目录为准。
核心概念:Actor 状态如何做到自动保存与恢复
在 Rivet Actors 中,状态管理遵循"声明式 + 自动持久化"的模型,核心要点如下:
- 持久化状态(Persistent state):Actor 的
state会跨重启自动保存与恢复。服务器重启、Actor 休眠再唤醒后,消息列表依然完整存在。 - 类型安全(Typed state):状态对象是强类型的 TypeScript 结构,编译期即可发现字段拼写错误或类型不匹配。
- 状态初始化(State initialization):通过
state属性(或createState工厂)定义初始值,首次创建 Actor 时生效。 - 自动序列化(Automatic serialization):对
c.state的任何修改都会自动持久化,无需手动调用save()之类的 API。
这一"零手动保存"的设计在 examples/state-render/src/actors.ts 的注释中有直接体现:// State changes are automatically persisted(对应源文件中的sendMessage实现),即c.state.messages.push(message)之后框架自动落盘。
深入源码:chatRoomActor 的完整定义
examples/state-render/src/actors.ts是整个示例的业务核心。它用actor({...})声明了一个聊天室 Actor,包含三类声明式构件:
import { actor, event, setup } from "rivetkit"; export type Message = { id: string; sender: string; text: string; timestamp: number; }; export const chatRoom = actor({ state: { messages: [] as Message[], }, events: { newMessage: event<Message>(), messagesCleared: event<[]>(), }, actions: { sendMessage: (c, sender: string, text: string) => { const message: Message = { id: crypto.randomUUID(), sender, text, timestamp: Date.now(), }; c.state.messages.push(message); c.broadcast("newMessage", message); return message; }, getMessages: (c) => c.state.messages, clearMessages: (c) => { c.state.messages = []; c.broadcast("messagesCleared"); return { success: true }; }, }, }); export const registry = setup({ use: { chatRoom }, });逐段拆解这段代码:
state:初始状态为{ messages: [] },messages是Message[]数组。Message类型包含id(UUID)、sender(发送者)、text(文本)、timestamp(毫秒时间戳)。events:声明两个可广播的事件——newMessage(携带Message负载)与messagesCleared(空负载)。客户端通过useEvent订阅它们实现实时刷新。actions:暴露给客户端的可调用方法:sendMessage(c, sender, text):生成消息对象,push进c.state.messages(自动持久化),随后c.broadcast("newMessage", message)推送给所有已连接客户端,最后把消息返回给调用方;getMessages(c):返回全部消息,供客户端首屏加载历史;clearMessages(c):清空状态数组并广播messagesCleared事件。
registry = setup({ use: { chatRoom } }):把 Actor 注册进运行时,供服务器路由与客户端 SDK 使用。从源码结构看,setup返回的registry同时承载了handler(HTTP 处理函数)与start()(进程内启动)两条路径,分别对应云端部署与本地开发两种模式。
事件广播与连接管理:c.broadcast的客户端视角
c.broadcast("newMessage", message)会把事件推送给所有订阅了该 Actor 实例的连接。在 examples/state-render/frontend/app/App.tsx 中,前端通过chatRoom.useEvent(...)订阅:
chatRoom.useEvent("newMessage", (msg: Message) => { setMessages((prev) => [...prev, msg]); }); chatRoom.useEvent("messagesCleared", () => setMessages([]));结合useActor({ name: "chatRoom", key: ["lobby"] })可以看出调用链:前端以固定的实例 Key["lobby"]获取/创建chatRoomActor 实例,所有连接到该 Key 的客户端共享同一份持久化状态与事件流。前端还通过chatRoom.connection.getMessages()在连接建立后拉取历史消息,作为事件流的初始化补齐。
双模式启动逻辑:本地直跑与云端 HTTP 服务的切换
examples/state-render/src/index.ts通过环境变量在两种模式间自动切换:
import "./env.ts"; import { registry } from "./actors.ts"; import { port, useRivetCloud } from "./env.ts"; if (useRivetCloud) { const { serve } = await import("@hono/node-server"); const { default: app } = await import("./server.ts"); serve({ fetch: app.fetch, port }, () => { console.log(`state-render listening on http://0.0.0.0:${port}`); }); } else { registry.start(); }而 examples/state-render/src/env.ts 定义了切换条件:
export const port = Number(process.env.PORT) || 6420; export const useRivetCloud = process.env.NODE_ENV === "production" && Boolean(process.env.RIVET_ENDPOINT);- 本地开发:
NODE_ENV非 production,走registry.start(),由 RivetKit 运行时自带的管理服务器处理/actors、/metadata、/health等路由,监听6420端口(可用RIVET_MANAGER_PORT覆盖,见 examples/state-render/vite.config.ts 中的RIVET_MANAGER_PORT读取逻辑)。 - 云端部署:
NODE_ENV === "production"且设置了RIVET_ENDPOINT,则启动 Hono 服务器,将/api/rivet/*代理给registry.handler,并托管 Vite 构建出的静态前端。
生产 HTTP 服务器:Hono 路由与静态资源托管
examples/state-render/src/server.ts 是云端模式下的入口服务器:
import { serveStatic } from "@hono/node-server/serve-static"; import { Hono } from "hono"; import { registry } from "./actors.ts"; const app = new Hono(); app.all("/api/rivet/*", (c) => registry.handler(c.req.raw)); app.get("/health", (c) => c.json({ status: "ok" })); app.use("/*", serveStatic({ root: "./public" })); app.get("*", serveStatic({ root: "./public", path: "/index.html" })); export default app;各路由职责:
/api/rivet/*:把 RivetKit 的 HTTP/WebSocket 请求原样转交给registry.handler,这是前端 SDK 与 Actor 运行时通信的桥梁(/actors等 RPC 端点都经由此处);/health:健康检查端点,返回{ "status": "ok" },对应 Render Blueprint 中的healthCheckPath: /health;- 静态资源:优先按路径匹配
./public下的文件,未命中的路径回退到index.html(SPA 路由兜底)。
./public目录由 Vite 构建产物输出。在 examples/state-render/vite.config.ts 中可以看到关键配置:
build.outDir: "public"且emptyOutDir: true,构建时清空并重写该目录;- 通过
define将import.meta.env.VITE_RIVET_PUBLIC_ENDPOINT编译期注入为VITE_RIVET_PUBLIC_ENDPOINT || RIVET_PUBLIC_ENDPOINT的值; manualChunks将 react、@rivetkit、render-dds 拆分为独立 vendor chunk,优化首屏加载。
前端客户端:连接地址的智能推导
examples/state-render/frontend/app/rivet-client.ts 负责推导 SDK 的连接基地址:
export function rivetClientBase(): string { if (import.meta.env.DEV) return "http://localhost:6420"; const fromBuild = import.meta.env.VITE_RIVET_PUBLIC_ENDPOINT as | string | undefined; if (fromBuild && fromBuild.length > 0) { return fromBuild.replace(/\/$/, ""); } return window.location.origin; }- 开发模式(
import.meta.env.DEV):直连本地http://localhost:6420,配合 Vite dev server 的代理(见 examples/state-render/vite.config.ts 中/actors、/metadata、/health的proxy配置与ws: true的 WebSocket 透传); - 生产模式:优先使用构建期注入的
VITE_RIVET_PUBLIC_ENDPOINT(源自环境变量RIVET_PUBLIC_ENDPOINT),去掉末尾/后作为基地址;未设置时回退到window.location.origin,即前端与 Rivet 服务同域部署的场景。
测试先行:用setupTest验证持久化行为
虽然state-render面向部署,但其业务逻辑在 examples/state/tests/chat.test.ts 中有完整的 Vitest 测试佐证,可作为"状态自动持久化"这一特性的可验证依据。核心测试用例包括:
- 发送与接收:两个客户端
getOrCreate(["room1"])连接到同一房间,client1.sendMessage("Alice", "Hello!")后,client2.getMessages()能取到该消息,且消息结构符合Message类型(id、sender、text、timestamp); - 持久化:向
["persistent-room"]依次发送 3 条消息,再从另一个客户端实例getOrCreate(["persistent-room"])读取,仍能拿到全部 3 条——这正是"状态跨实例、跨重启保留"的验证; - 消息排序:连续发送 5 条消息后,
getMessages()严格保持发送顺序,且timestamp单调不减; - 清空:
clearMessages()返回{ success: true },之后getMessages()返回空数组; - 多房间隔离:
room1与room2的状态互不可见,每个实例 Key 对应独立的状态空间。
测试统一使用setupTest(ctx, registry)建立隔离环境,说明该状态模型是可单测、可复现的——这也是把"自动持久化"当作工程事实而非黑盒魔法的关键证据。
一键部署:render.yaml Blueprint 全解
examples/state-render/render.yaml 是 Render 的 Blueprint 声明文件(遵循 Render Blueprint 规范),完整内容如下:
services: - type: web name: state-render runtime: node plan: free region: oregon buildCommand: npm ci --include=dev && npm run build startCommand: npm start healthCheckPath: /health envVars: - key: NODE_VERSION value: "22.12.0" - key: NODE_ENV value: production - key: RIVETKIT_STORAGE_PATH value: /tmp/rivetkit - key: RIVET_ENDPOINT sync: false - key: RIVET_PUBLIC_ENDPOINT sync: false逐项解读:
| 字段 | 值 | 说明 |
|---|---|---|
type | web | 常驻 Web 服务 |
runtime | node | Node.js 运行时 |
plan | free | 免费套餐即可运行 |
region | oregon | 部署区域 |
buildCommand | npm ci --include=dev && npm run build | 安装含 devDependencies 的依赖(构建需要 Vite/tsx),再执行vite build |
startCommand | npm start | 对应package.json中的tsx src/index.ts |
healthCheckPath | /health | 对应server.ts中的健康检查路由 |
NODE_VERSION | 22.12.0 | 与 package.json 中engines.node >= 22.0.0匹配 |
NODE_ENV | production | 触发useRivetCloud的云端模式分支 |
RIVETKIT_STORAGE_PATH | /tmp/rivetkit | 本地 Actor 状态存储路径(Render 免费套餐的可写临时目录) |
RIVET_ENDPOINT/RIVET_PUBLIC_ENDPOINT | sync: false | 由用户在 Render 控制台手动填写,不自动生成 |
部署步骤如下:
- 将仓库推送到 GitHub/GitLab/Bitbucket;
- 在 Render 控制台Blueprints > New Blueprint Instance选择该仓库并 Apply;
- 若从 monorepo 部署,将Root Directory设置为
examples/state-render; - 在 Render 服务中填写两个环境变量(来自 Rivet Cloud 项目的 Rivet Cloud 后台):
| 变量 | 描述 |
|---|---|
RIVET_ENDPOINT | Rivet Cloud 项目的后端端点 URL |
RIVET_PUBLIC_ENDPOINT | Rivet Cloud 项目的公网端点 URL |
- 在 Rivet 控制台中将Connect your backend指向 Render 服务的 HTTPS 地址,完成双向联调。
RIVET_ENVOY_VERSION 的自动派生机制
README 特别强调:RIVET_ENVOY_VERSION会从 Render 的RENDER_GIT_COMMIT自动派生,每次部署无需手动 bump。其实现位于 examples/state-render/src/env.ts:
function ensureRivetEnvoyVersion(): void { if (process.env.RIVET_ENVOY_VERSION) return; if (process.env.RIVET_RUNNER_VERSION) { process.env.RIVET_ENVOY_VERSION = process.env.RIVET_RUNNER_VERSION; return; } const sha = process.env.RENDER_GIT_COMMIT; if (sha && /^[0-9a-f]{7,40}$/i.test(sha)) { const n = Number.parseInt(sha.slice(0, 8), 16); process.env.RIVET_ENVOY_VERSION = String(n > 0 ? n : 1); } }逻辑分三层:显式设置过RIVET_ENVOY_VERSION则直接沿用(优先级最高);其次复用RIVET_RUNNER_VERSION;最后把RENDER_GIT_COMMIT的 SHA 前 8 位当作十六进制整数解析,转换为数字版本号(非正数时兜底为1)。这样每次 Git 提交对应的部署都会产生唯一版本,保证 Envoy 侧能区分新旧部署。如需覆盖,显式设置该环境变量即可。
本地开发运行指南
前置条件
- Node.js ≥ 22(见 package.json 的
engines字段,render.yaml中亦指定NODE_VERSION=22.12.0); - npm ≥ 10(使用
npm ci需要锁文件)。
启动开发服务器
git clone https://github.com/rivet-dev/rivet.git cd rivet/examples/state-render npm install npm run devnpm run dev由concurrently并行启动两个进程(见 package.json scripts):
tsx --watch src/index.ts:以 watch 模式运行 Actor 运行时,监听6420端口;vite:启动前端 dev server,并将/actors、/metadata、/health代理到127.0.0.1:6420(WebSocket 也通过ws: true透传)。
生产构建与本地预览
npm run build # vite build → 输出到 public/ npm start # tsx src/index.ts,以云端模式运行(需设置 NODE_ENV=production 与 RIVET_ENDPOINT)运行测试
# 在 examples/state 目录下(测试套件所在处) npm install npm run test测试基于rivetkit/test的setupTest与 Vitest,覆盖消息收发、持久化、排序、清空与多房间隔离(见 examples/state/tests/chat.test.ts)。
可复用的工程模式总结
从state-render中可以提炼出一套可直接复用的模板:
- 业务与交付分离:把 Actor 定义(
actor/setup)放在src/actors.ts,本地直跑与云端 HTTP 两种模式由env.ts的环境变量开关决定,业务代码零改动; - 前端连接地址分层:
rivetClientBase()按"dev 直连 → 构建期注入 → 同域回退"三级推导,适配本地联调与生产部署; - Blueprint 即基础设施:
render.yaml把构建命令、启动命令、健康检查与环境变量声明化,配合RIVET_ENVOY_VERSION的自动派生,实现"提交即部署、部署即版本化"; - 状态行为可测试:用
setupTest+ Vitest 在无网络环境下验证持久化、排序与隔离语义,让"自动保存"这一特性有据可查。
更进一步,RivetKit 还提供状态管理之外的能力(如 actions、events、lifecycle hooks),相关文档位于仓库 docs-internal 目录与各示例项目中,例如 examples/state-render/frontend/app/App.tsx 中使用的useActor/useEvent模式,在 examples/chat-room 等示例中有更多变体,可作为后续深入方向的参考。
【免费下载链接】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),仅供参考