RivetKit TypeScript 运行时工程规范:从构建流程到网关、SQLite 与 WebAssembly 的开发者指南
【免费下载链接】actorsRivet Actors are the primitive for stateful workloads. Built for AI agents, collaborative apps, and durable execution.项目地址: https://gitcode.com/GitHub_Trending/riv/actors
导读
本文以仓库中 rivetkit-typescript/CLAUDE.md 这份工程约定文档为骨架,系统梳理 RivetKit TypeScript SDK(@rivetkit/*系列包)在维护与扩展时必须遵守的边界与最佳实践。内容涵盖 Inspector UI 嵌入构建、原生/WebAssembly 运行时划分、网关rvt-*查询参数协议、原始 KV 硬性限额、原生 SQLite v2 VFS、NAPI 接收循环、休眠关闭语义、Drizzle 兼容性测试与测试基架等主题。读完本文,你将掌握在rivetkit-typescript代码库中安全修改构建流程、运行时适配层、网关客户端与测试代码的完整规则,并了解每一条约定的源码依据。
适用对象:正在维护或二次开发
rivetkit-typescript的工程师;本文所有结论均可在仓库对应源码路径中验证。
Inspector UI 嵌入的两步构建(build:embed)
Inspector 用户界面(Inspector UI)被嵌入到@rivetkit/rivetkit-napi与@rivetkit/rivetkit-wasm原生二进制的产物中,但嵌入动作发生在它们下游:由rivetkit-core的build.rs在编译原生模块时把已构建好的 UI 打包进二进制。由于 Turbo 的dependsOn无法表达这种"先构建 UI、再编译依赖它的原生模块"的依赖关系(直接声明会形成循环依赖),仓库用了一个带作用域的build:embed任务来解决:
pnpm -w build(等价于turbo build build:embed)会先执行build:inspector-ui,再重跑一次原生构建,把 UI 产物嵌入.node/ wasm 包;- 任何部分构建(partial build)——例如只运行
@rivetkit/rivetkit-napi#build而没有跟着跑build:embed——都会让.node中留下空的 bundle,导致/inspector/ui/*路由返回ui_asset_not_found404 错误。
在 rivetkit-typescript/packages/rivetkit-napi/package.json 中可以看到完整的脚本族:build、build:release、build:force、build:force:release与build:embed,其中build:embed与build执行同一份scripts/build.mjs,只是在 Turbo 管线中被安排在build:inspector-ui之后运行。
修复排查的完整上下文记录在仓库的Build troubleshooting参考文档中(ui_asset_not_found 404一节)。如果你改动了 Inspector UI 却没有触发两步构建,不要直接在生成物上找问题,先确认是否走的是完整管线。
从纯 TypeScript 到 Rust Core 的迁移参考
提交1761e6f0f(chore: rivetkit to rust,2026-04-16)是 RivetKit 运行时从全 TypeScript 实现切换到Rust core + NAPI 绑定的里程碑提交。
- 对比当前行为与最初 TypeScript 实现时,应使用
1761e6f0f^(或更早的提交)作为基线; - 典型问题包括:生命周期顺序(lifecycle ordering)、持久化布局(persistence layout)、以及"这一版 TypeScript 实现当时是怎么做的"这类历史疑问;
- 当前分支上,原生 TS actor/conn 持久化胶水代码仍位于
packages/rivetkit/src/registry/native.ts(详见下文"NAPI 接收循环"一节)。
理解这一点,有助于在阅读较旧文档或外部资料时判断其描述的是"TS 时代"还是"Rust 时代"的行为。
树摇(Tree-Shaking)边界
为了让最终打包产物尽可能小,rivetkit-typescript对模块依赖划定了明确边界:
@rivetkit/workflow-engine只允许在rivetkit/workflow入口点被导入,保证 workflow-engine 保持可树摇状态;- SQLite 运行时代码必须走原生
@rivetkit/rivetkit-napi路径,不得重新引入 WebAssembly SQLite 或 KV-backed VFS 回退方案; - 导入
rivetkit/db是对 SQLite 的显式 opt-in,不应再从该入口懒加载额外的 SQLite 运行时; - 核心驱动必须保持SQLite 无关(SQLite-agnostic),任何 SQLite 专属接线都应放在原生数据库 provider 边界之后;
- 在删除某个
rivetkit/*包导出前,必须先在examples/、website/、frontend/中 grep 是否存在自引用——这些消费方属于本分支支持的公开包面(package surface)。
运行时边界(Runtime Boundary)
RivetKit 同时提供 NAPI(原生)与 WASM 两套运行时,共享同一套 actor 胶水逻辑。为保持两者行为一致,约定如下:
- 运行时行为的选择依据
CoreRuntime.kind字段,而不是用instanceof去判断适配器类——napi映射原生运行时 kind,wasm映射 wasm kind(定义见 registry/runtime.ts); CoreRuntime的 SQL 方法必须放在可移植的RuntimeSql*结构体上(registry/runtime.ts),NAPI 专属的Buffer转换只允许存在于NapiCoreRuntime内部;RuntimeSqlBindParam的变体必须保持精确(null/int/float/text/blob,见 registry/runtime.ts),RuntimeSqlExecuteResult.route只能取read、write、writeFallback之一,生成的适配器输出在返回前要归一化;CoreRuntime的字节载荷统一使用RuntimeBytes/Uint8Array,NAPI 专属Buffer转换归NapiCoreRuntime;- 共享 actor 胶水(
packages/rivetkit/src/registry/native.ts)只构造RuntimeBytes/Uint8Array,Buffer的创建留给NapiCoreRuntime; - wasm 绑定对 NAPI 支持的运行时 API 应转发到
rivetkit-core,避免用占位返回值破坏运行时一致性(runtime parity); - wasm 的
registerTask必须转发到 core 的ActorContext::register_task(...),而不是waitUntil——这样关闭时的 drain 语义与 NAPI 一致,且不会把公开waitUntil的工作混为一谈; - 网桥错误 HTTP 状态码提升(status promotion)属于
rivetkit-core的职责;TS 侧原生/wasm 适配器应解码编码后的状态字段,而不是各自维护一张提升表; - 使用公开
sqlite配置选择运行时 SQLite 后端;wasm 在未设置 SQLite 时默认远程(remote),并且必须在运行时构造前拒绝 local 配置。
这些约定保证了同一份 actor 用户代码在 NAPI 与 WASM 两种部署形态下行为一致——这正是 SDK 可移植性的根基。
原生 SQLite v2 的实现约束
SQLite 原生层(packages/sqlite-native与rivetkit-napi配合)是 actor 持久化的关键路径,相关约定如下:
- v2 SQLite VFS 必须为部分(partial)
xRead/xWrite回调重建完整的 4 KiB 页——因为即使提交是页粒度的,SQLite 也可能发出子页头的 I/O; head_txid与db_size_pages是 VFS 自有的状态:读侧的get_pages(...)响应可以刷新max_delta_bytes,但只有提交响应加上本地xWrite/xTruncate路径才能推进或收缩这两个字段;- 修改
packages/rivetkit-napi或packages/sqlite-native下的 Rust 代码后,必须从packages/rivetkit-napi目录执行pnpm build:force,才能刷新原生.node构件; - 真正驱动 v2 VFS 的
sqlite-native测试(通过直接SqliteEngine)需要multithread Tokio 运行时;current_thread只适合 mock transport 测试,在真实引擎回调上会卡死; - 任何 sqlite v2 的 transport 或 commit 错误对该 VFS 实例都是致命的:标记其 dead、通过
take_last_kv_error()上报,然后依赖 reopen + takeover 恢复,而不是带着脏页硬撑; - v2 的致命 commit 清理逻辑必须留在
flush_dirty_pages与commit_atomic_write中;回调包装器只负责把 fence 不匹配翻译成 SQLite 的 I/O 返回码; - 如果原生 SQLite 层新增了 introspection 或 metrics getter,必须通过
wrapJsNativeDatabase(...)转发出来,否则 actor inspector 的指标会悄悄丢失。
上下文类型同步(Context Types Sync)
*ContextOf类型是公开 API 的组成部分,它们从 actor/contexts/index.ts 导出,并由 actor/mod.ts 再导出。新增、删除或重命名上下文类型时,必须同步更新两个文档位置:
website/src/content/docs/actors/types.mdx(公开文档页);website/src/content/docs/actors/index.mdx(速成课程中的 Context Types 一节)。
这保证了类型系统与文档始终一致,避免出现"文档里的类型不存在"的断链。
网关目标与rvt-*查询参数协议
网关(Gateway)是客户端访问 actor 的统一入口。面向客户端的网关操作必须使用 engine-client/driver.ts 中共享的GatewayTarget类型({ directId: string } | ActorQuery),而不是临时拼string | ActorQuery联合类型。引擎控制客户端应保留直接 actor ID 行为,并在客户端实现内部解析ActorQuery目标,这样上层客户端流可以在不复制查询解析逻辑的情况下拓宽目标类型。
ActorKey与ActorContext.key的 TS 面保持纯字符串,除非client/query.ts、key 序列化与网关查询解析在同一次变更中端到端拓宽。
actor-connect 协议的actionId值是可空的,且0是合法 action ID——只有null才被视为连接级错误。
查询式网关 URL 结构
查询式(query-backed)远程网关 URL 使用rvt-*查询参数:
/gateway/{name}/{path}?rvt-namespace=...&rvt-method=...&rvt-key=...- actor 名是干净的路径段,所有路由参数都是带
rvt-前缀的标准查询参数; - 已知
rvt-*参数:rvt-namespace、rvt-method、rvt-runner、rvt-key、rvt-input、rvt-region、rvt-crash-policy、rvt-token(另见rvt-skip-ready-wait); rvt-runner对getOrCreate必填、对get禁用;- 多组件 key 使用单个逗号分隔的
rvt-key参数,例如rvt-key=tenant,room; - 必须用
URLSearchParams构建与解析查询串。
这一协议在 engine-client/actor-websocket-client.ts 的buildActorQueryGatewayUrl中有完整实现:rvt-method只有get与getOrCreate两种取值,get不接受crashPolicy与runnerName,getOrCreate的crashPolicy默认为sleep,输入经过 CBOR 兼容编码后以 base64url 放入rvt-input。
查询路径的解析规则
在 actor-gateway/gateway.ts(或等价实现)解析查询网关路径时:
- 通过检查是否有查询参数以
rvt-开头来识别查询路径; - 用
URLSearchParams解析参数,并把参数划分为rvt-*参数与 actor 参数两部分; - 拒绝裸
@token语法、未知的rvt-*参数以及重复的标量rvt-*参数; - 转发给 actor 前,从查询串中剥掉所有
rvt-*参数,只用 actor 参数重建查询串。
一旦查询路径被解析,就在共享的基于路径的 HTTP 与 WebSocket 网关 helper 中把它解析为 actor ID,再调用proxyRequest/proxyWebSocket;解析后复用现有的直接 ID 代理流程,并保留剥离rvt-*参数后的原始剩余路径。
网关客户端的解析时机
buildGatewayUrl()对get()/getOrCreate()句柄保持查询式,而不是预先解析成 actor ID;本地getGatewayUrl()流程应在被服务的运行时路由路径上复用共享的actorGateway查询参数解析器,而直接 actor ID 目标仍走/gateway/{actorId};- 在 engine-client/actor-websocket-client.ts 中校验查询输入载荷时,必须用原始 CBOR 字节长度(base64url 编码前)来执行
ClientConfig.maxInputSize——这样限额与实际序列化载荷对齐,而不是被 URL 编码膨胀后的长度; ClientRaw.get()/getOrCreate()流程不要在ActorResolutionState上缓存解析后的 actor ID:基于 key 的句柄与连接应每次操作都重新解析,避免在 destroy/recreate 之后仍钉在旧 actor 选择上;- client 中的网关客户端 helper 应通过
getGatewayTarget()派生EngineControlClient目标,而不是预先调用resolveActorId()。get()/getOrCreate()句柄必须把ActorQuery一路透传到sendRequest、openWebSocket、buildGatewayUrl,让每个请求与重连都在网关处重新解析;只有getForId()与 create-backed 句柄才收拢为纯 actor ID 目标。
原始 KV 硬性限额(Raw KV Limits)
使用原始 actor KV(raw actor KV)时,始终强制执行引擎限额:
| 限额项 | 数值 |
|---|---|
| 单 key 最大尺寸 | 2048 字节 |
单批kv put总载荷(keys + values) | 976 KiB |
单批kv put最大条目数 | 128 对 |
| actor KV 总存储上限 | 10 GiB |
设计原始 KV 操作时必须把约束考虑在内:单请求可能超限时要把操作拆分为多个请求;10 GiB 总存储上限是硬限制,写入失败要 fail-closed——显式报错,而不是吞掉、截断或忽略 KV 写入失败。
涉及 KV、队列、工作流持久化、SQLite-over-KV 或任何限额相关行为变更时,必须同步更新website/src/content/docs/actors/limits.mdx文档。仓库中 workflow/driver.ts 也注释了"former actor-KV transport allowed 976 KiB batches"这一历史来源,说明限额在迁移后仍被沿用。
启动日志规范(Startup Logging)
actor 启动的每个离散阶段都必须有对应的 debug 日志,并使用统一前缀区分框架与用户代码:
- 框架/基础设施阶段:
perf internal:; - 用户代码回调:
perf user:。
示例:
DEBUG perf internal: loadStateMs durationMs=... DEBUG perf internal: initQueueMs durationMs=... DEBUG perf user: onCreateMs durationMs=... DEBUG perf user: dbMigrateMs durationMs=...日志名与ActorMetrics.startup中的键一一对应。这一约定让启动日志可 grep,并方便区分框架开销与用户代码耗时。新增启动阶段时,必须加上带正确前缀的对应日志;如果该阶段运行用户代码,还要更新ActorInstance中的#userStartupKeys集合。
NAPI 接收循环与休眠关闭语义
NAPI 适配器拥有自己的接收循环(receive loop),负责从 core 拉取 actor 事件并派发给 JS。相关约定:
- 适配器自有的长生命周期任务句柄(例如 NAPI
runhandler)保留在packages/rivetkit-napi/src/napi_actor_events.rs,只通过共享ActorContext状态暴露同步的 restart 钩子;JS 侧的 restart 方法不得依赖异步锁; - 不要在
packages/rivetkit-napi/src/actor_context.rs中镜像 actor 的ready/started标志;这些生命周期门(lifecycle gates)的读写都通过 coreActorContext,让 sleep 门控保持单一事实来源; - 优雅适配器 drain 应使用
while let Some(...) = tasks.join_next().await;JoinSet::shutdown()会中止进行中的工作,破坏 Sleep/Destroy 的顺序; Sleep与Destroy必须在成功与错误回复上都设置共享适配器的end_reason,否则外层接收循环会在关闭已失败后继续消费排队事件;- 公共 TS actor
onWake映射到原生回调袋的onWake;onBeforeActorStart是内部 driver/NAPI 启动钩子,不是公共 actor 配置; - registry/native.ts 中静态 actor
state值必须按 actor 实例做structuredClone(...),复用字面量会让变更跨不同 keyed actor 泄漏; - JS-only 原生 actor 缓存应放在
ActorContext.runtimeState()上,而不是 actorId 为键的模块全局变量——同 key 重建必须拿到全新的 bag; - registry/native.ts 中每个
NativeConnAdapter构造路径都必须保留CONN_STATE_MANAGER_SYMBOL钩子,可休眠连接(hibernatable conn)的变更依赖 coreConnHandle::set_state的脏追踪来请求持久化; - 可持久化的原生 actor 保存必须使用
ctx.requestSaveAndWait({ immediate: true }),状态字节只通过serializeState回调收集; - 需要跨 Rust JSON/CBOR 桥保留 JS
undefined的不透明用户载荷,应走encodeCborCompat/decodeCborCompat;结构化的 JSON 信封不要用这两个 helper(必须保持省略字段确实被省略); - 带回复的 TSF 派发必须通过共享的 timed-spawn helper 用
with_timeout(...)包裹回调 future;裸用spawn_reply(...)处理 HTTP 或 workflow 回调可能让卡住的 JS promise 一直泄漏到关闭; - 派发取消(dispatch cancellation)必须在
napi_actor_events.rs与registry/native.ts之间端到端传递CancellationToken对象,不要重新引入 BigInt token 注册表或轮询循环; - 原生持久化与
saveState覆盖必须放在使用真实 NAPI +hardCrashActor或 sleep/wake 的 driver 测试中,不要在 Vitest 里 mockNativeActorContext。
Sleep 关闭的等待语义
- Sleep 关闭应等待进行中的 HTTP action 工作与挂起的 disconnect 回调之后再触发
onSleep,但不应仅因存在打开的休眠连接就阻塞——已有连接上的 action 在优雅关闭窗口内仍可能完成; - 等待目标 actor 休眠的 driver 测试,应把生命周期事件记录到独立的 observer actor,不要在等待期间轮询目标 action 或保持普通目标连接打开。
Drizzle 兼容性测试
RivetKit 的 drizzle 集成需要跨多个 drizzle-orm 版本保持兼容,测试脚本位于rivetkit-typescript/packages/rivetkit:
cd rivetkit-typescript/packages/rivetkit ./scripts/test-drizzle-compat.sh # 测试全部默认版本 ./scripts/test-drizzle-compat.sh 0.44.2 0.45.1 # 测试指定版本脚本会为每个 drizzle-orm 版本执行安装、针对rivetkit/db/drizzle公开面 typecheckscripts/drizzle-compat-smoke.ts,并逐版本报告 pass/fail;退出时会恢复原始 package.json 与 lockfile。新增 drizzle 版本支持时,要更新脚本中的DEFAULT_VERSIONS数组。
测试基架(Test Harness)
- 共享的本地
rivet-engine生命周期位于packages/rivetkit/tests/shared-engine.ts,driver 与平台测试应复用它,而不是各自启动引擎; - 运行时一致性测试可以用 fake binding class 实例化
NapiCoreRuntime与WasmCoreRuntime,再通过buildNativeFactory(...)驱动共享 actor 胶水,无需生成 NAPI 或 wasm 构件; - 平台 wasm smoke 测试复用
packages/rivetkit/tests/platforms/shared-registry.ts(原始 SQL SQLite counter actor 与公开 wasmsetup(...)形态); - 平台 smoke 测试使用
packages/rivetkit/tests/platforms/shared-platform-harness.ts处理 serverless runner 启动、应用进程日志、临时应用目录、健康检查与固定的pnpm dlxCLI 启动; - 新增、删除或重命名使用
describeDriverMatrix的packages/rivetkit/tests/driver/*.test.ts文件时,必须在同一变更中同步更新 Fast/Slow/Excluded 测试列表与.claude/skills/driver-test-runner/SKILL.md中的套件描述表。
Cloudflare Workers 兼容性
Cloudflare Workers 禁止在全局作用域(请求 handler 之外)调用setTimeout、fetch、connect及其他异步 I/O。而Registry构造函数运行在全局作用域,因此绝不能无条件调用这些 API。任何延迟工作(例如预启动运行时)都必须先用同步配置检查把关,再调度定时器。
参考 registry/index.ts 的实现模式:外层if守护setTimeout,内层if在 tick 之后重新检查,以便拾取迟到的配置变更。这是把 actor 运行时平移到边缘无服务器环境的先决条件。
Wasm 绑定包规范
@rivetkit/rivetkit-wasm由 wasm-pack 产出,维护规则包括:
packages/rivetkit-wasm/pkg/视为 wasm-pack 输出目录:提交源码与构建脚本,在包构建期间重新生成产物;- wasm 原始 WebSocket 句柄导出为
WebSocketHandle而不是WebSocket,因为 wasm-bindgen 会拒绝与宿主全局重名的类; - wasm 运行时适配器字节归一化保持在
Uint8Array上,不要给 registry/wasm-runtime.ts 引入 NodeBuffer依赖; - 平台 wasm 绑定通过
setup({ wasm: { bindings, initInput } })传入,不要加隐藏的globalThis绑定钩子; - wasm
CoreRegistry的 serverless 启动必须使用BuildingServerlesswaiter 状态;构建期间发生关闭必须唤醒 waiters 并 drain 新构建的运行时; - 在转换为 core
ServeConfig或启动注册表副作用之前,先校验 wasm-only serve 配置约束; - wasm 包导出或文件变更后运行
pnpm --filter @rivetkit/rivetkit-wasm run check:package,验证发布的 tarball 包含根入口点与 wasm 产物; - 构建
@rivetkit/rivetkit-wasm使用包内锁定的wasm-pack依赖,不要用npx -y wasm-pack; - wasm 桥接的
RivetErrorSchema值只按(group, code)作为内部键,实时错误消息必须留在RivetError.message上,不要扩大 schema 缓存键; - wasm websocket 回调区域用「以活跃区域 ID 为键的 map」追踪,结束时移除条目,避免重复回调残留空槽;
- wasm websocket 回调区域 ID
0是"未追踪"哨兵;u32分配通过跳过活跃 ID 来避免 panic。
Workflow 上下文的 Actor 访问守卫
Workflow 上下文被拆分为两个(workflow/context.ts):
WorkflowContext(workflow 函数体 +try/loop/race/join回调)只暴露可重放原语,不含任何 actor 数据;WorkflowStepContext(step/tryStep/rollback回调)是唯一能触达 actor 数据与副作用的地方。
约束:
- 有副作用与 actor 数据的成员必须放在
WorkflowStepContext上,并调用#ensureActive,使 step 结算后被捕获使用的 step 上下文立即抛错;只有只读属性(例如actorId、name、key、log、abortSignal)例外; - 不要向
WorkflowContext添加 actor 数据(state、vars、db、client、broadcast),必须通过传给step/tryStep的 step 上下文访问。
这一设计保证 workflow 的可重放性(replayability)不被副作用污染,是 durable execution 语义正确性的关键。
动态 Actor 架构文档
涉及动态 actor 行为、桥接契约(bridge contracts)、isolate 生命周期或运行时沙箱接线时,以docs-internal/rivetkit-typescript/DYNAMIC_ACTORS_ARCHITECTURE.md为权威参考;每次变更动态 actor 架构、生命周期、桥接载荷、安全行为或临时兼容路径时,都必须在同一变更中保持该文档最新。
小结
rivetkit-typescript/CLAUDE.md本质上是一份"行为契约":它把跨 NAPI/WASM 双运行时、原生 SQLite VFS、网关协议、KV 限额与测试矩阵之间的隐性耦合显式化。从两步构建的build:embed,到CoreRuntime.kind驱动的运行时选择,再到rvt-*查询参数与 10 GiB KV 硬上限,每条规则的背后都能在 packages/rivetkit/src 中找到对应的实现文件。遵循这些约定,既能避免ui_asset_not_found这类构建陷阱,也能保证 actor 生命周期、持久化与网关路由在 NAPI 与 WASM 两条路径上长期保持一致。
【免费下载链接】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),仅供参考