fetch 如何将 JavaScript 运行时连接到持久化计算
持久化计算
JavaScript 是一种常见的执行目标;Web 平台 API 在其运行时之间提供了一个共享接口。=fetch= 连接了浏览器和服务器,而相同的 Request=/=Response 模型也适用于具名、有状态的执行体(actors)。关键的转变在于主机在请求(或整个进程)结束后能够恢复什么。
考虑一个工作流:获取输入、调用 AI 服务、等待数小时的人工审批,然后发布结果。其生命周期超过了任何单一的 HTTP 请求或服务器进程。JavaScript 运行时和 Web API 提供了熟悉的编程模型;而持久化让这些工作能够超越启动它的执行过程而存在。1
运行时与 Web 平台 API
引擎执行 JavaScript;运行时添加 I/O。共享的 Web API 核心包括:=fetch=、=Request=、=Response=、=Headers=、流、=URL= 和 Web Crypto。
| 运行时 | 典型环境 | Web API 支持 |
|---|---|---|
| 浏览器 | 客户端 | 核心 + DOM |
| Node.js | 服务器 / 云虚拟机 | 核心;HTTP 服务器适配器 |
| Deno | 服务器 / 云虚拟机 | 核心;=Deno.serve= |
| Bun | 服务器 / 云虚拟机 | 核心;=Bun.serve= |
| QuickJS | 嵌入式引擎 | 由宿主提供的 Web API |
| workerd | Cloudflare / 自托管 | Worker 子集;=fetch= 处理程序 |
| celld | 自托管集群 | 兼容 Workers 的子集 |
支持情况因运行时/版本而异;DOM、CORS、Cookie 和流处理并不统一。 客户端: =fetch(request) -> Promise=。 服务器: =handler(Request) -> Response | Promise=;启动 API 各不相同。
这个共同的边界是各部分连接的地方:=fetch= 调用可以位于持久化工作流步骤中,=Request= 可以路由到具名的有状态对象,主机可以在外部工作完成时挂起程序。HTTP 接口保持熟悉,而主机增加了持久化、调度和恢复功能——这并非因为 Fetch 本身是持久化的,而是因为可以在其周围构建持久化机制。
Deno 团队于 2026 年 10 月宣布提供一年的错误/安全修复,随后结束其运行时开发;Deno 仍保持开源。
持久化执行
- 工作流: 持久化步骤结果;中断后,重放(replay)会重用已完成的步骤并重试未完成的工作。使外部影响具有幂等性。
- dquickts: 在安全点(safepoints)对 JS 堆和挂起的栈进行快照;使用相同的引擎构建在另一个进程中恢复并继续。主机 I/O 是独立的。
dquickts:保存运行中的程序,而不仅仅是步骤结果
QuickJS 扩展展示了一种更深层次的中断和恢复形式:主机可以暂停 JavaScript *即使是在循环中间*,序列化运行时堆和挂起的调用栈,并将该快照写入磁盘。对象、闭包、局部变量、挂起的 Promise 任务和保存的指令位置都是可恢复执行状态的一部分。2
原始宿主进程随后可以退出。新进程将快照加载到兼容的引擎构建中,并从保存的指令处恢复程序,而不是重新运行工作流来重建进度。这使得休眠和长时间等待成为可能,而无需保持原始进程驻留。
与工作流 I/O 的连接同样重要:=JS_SuspendCall()= 可以挂起一个主机调用,而 JS_ResumeWith() 可以在恢复运行时后提供其结果。该项目的示例使用类似 fetch 的主机函数来说明这个边界;它不是标准 Fetch API 的内置实现。主机仍然负责外部 I/O 和重复副作用的安全处理。3
%%{init: {"themeVariables": {"fontSize": "18px"}, "flowchart": {"htmlLabels": false, "curve": "linear", "nodeSpacing": 24, "rankSpacing": 32}}}%%
flowchart TB
subgraph WF["工作流"]
direction TB
W1["运行步骤"] --> W2[("持久化步骤结果")]
W2 -->|"进程重启"| W3["重放工作流
重用已保存结果"]
end
subgraph JS["dquickts"]
direction TB
R1["在循环中挂起"] --> R2[("堆 + 栈快照
写入磁盘")]
R2 -->|"新进程"| R3["恢复 + 继续
保存的指令"]
end
classDef running fill:#f8fafc,stroke:#64748b,color:#0f172a
classDef saved fill:#eff6ff,stroke:#2563eb,color:#0f172a
classDef resumed fill:#ecfdf5,stroke:#059669,color:#0f172a
class W1,R1 running
class W2,R2 saved
class W3,R3 resumed
style WF fill:#fff,stroke:#cbd5e1,color:#0f172a
style JS fill:#fff,stroke:#cbd5e1,color:#0f172aDurable Objects
一个具名的、单线程的执行体拥有强一致性的 SQLite/KV 存储。Cloudflare 将分布式路由、存储、警报和 WebSocket 休眠叠加在 workerd 的 Web API 模型之上。=fetch= 是 HTTP 的接缝,*不是*持久化本身;RPC 是另一个入口点。持久化状态在驱逐后依然存在;但 JS 调用栈不会。
对于类似工作流的负载,这些是互补的构建块:一个对象可以拥有作业或代理的状态,持久化进度,并调度警报以在稍后继续。Cloudflare 的 WebSocket 休眠可以在对象内存不足时保持客户端连接。这通过存储的状态恢复执行体,而不是像 dquickts 那样恢复其运行栈。4
%%{init: {"themeVariables": {"fontSize": "18px"}, "flowchart": {"htmlLabels": false, "curve": "linear", "nodeSpacing": 24, "rankSpacing": 32}}}%%
flowchart TB
C["浏览器或服务器
fetch(request)"]
W["Worker
Request → Response"]
D["Durable Object / cell
具名 fetch 处理程序"]
S[("私有 SQLite / KV
驱逐后依然存在")]
E["唤醒事件
警报 / WebSocket"]
C <-->|"Request / Response"| W
W <-->|"Object ID + stub.fetch"| D
D <-->|"读 / 写"| S
E -.->|唤醒| D
classDef handler fill:#f8fafc,stroke:#64748b,color:#0f172a
classDef actor fill:#ecfdf5,stroke:#059669,color:#0f172a
classDef saved fill:#eff6ff,stroke:#2563eb,color:#0f172a
class C,W,E handler
class D actor
class S savedcelld:提供商中立的部署
celld 将 Workers/Durable Objects 模型引入您的基础设施:V8 + 每个 cell 的 SQLite + 共享对象存储桶;存在兼容性差异。
继 Deno 团队转向 Cloudflare 后,计划是将 celld 的分布式自托管代码和理念合并到 workerd 中——目前尚未完成集成。
编译为 JavaScript 的语言
语言选择不必是托管选择
ClojureScript、ReScript 和 TypeScript 编译为 JavaScript,而不需要单独的生产虚拟机。它们的互操作/绑定暴露了 =fetch=、流和其他主机 API:选择一种语言并不意味着放弃 JavaScript 的运行时生态系统。
ClojureScript 的数据结构或 ReScript 的类型不会改变 Request=/=Response 边界。兼容的输出可以针对浏览器、服务器或持久化主机。这是接口的可移植性,而不是保证每个包都能原样运行:生成的 JS、模块和依赖项必须适配主机。仅限 DOM 的代码和特定于 Node 的库仍然是平台限制。
FastTwitch:ClojureScript 映射,原生 Fetch
FastTwitch 将 HTTP 应用程序表示为 Ring 风格的请求/响应映射以及普通的处理函数和中间件。其适配器将这些映射桥接到原生的 Request=/=Response 对象;其客户端使用原生的 fetch=、主体流和请求选项。Node HTTP、=Deno.serve 和 Bun.serve 在底层有所不同,但面向应用程序的处理程序契约保持一致。
实际好处: 在更换运行时而不是重建 HTTP 边界时,重用路由、身份验证和应用程序逻辑。特定于运行时的功能仍然需要匹配的适配器和测试。ReScript 可以绑定到相同的 Web API;FastTwitch 是 ClojureScript 的示例,而不是其他语言的要求。
FastTwitch Celld:从 HTTP 处理程序到具名的、有状态的 cell
考虑一个聊天应用程序:为每个房间提供自己的 cell,而不是在一个服务器进程内协调所有房间。FastTwitch Celld 添加了 ClojureScript cell 声明、HTTP/RPC 入口点、存储、警报和 WebSocket 绑定。HTTP 仍然使用 Fetch 模型;平台将每个房间路由到其具名的 cell,应用程序代码将消息历史记录持久化在该 cell 的 SQLite/KV 存储中。
在驱逐或重启后,新的激活可以从存储的历史记录中重建房间。普通的 ClojureScript atom 不会自动持久化;调用栈也不会。这是 Durable Objects 风格的状态恢复,而不是 dquickts 风格的执行快照。该扩展尚未发布。
%%{init: {"themeVariables": {"fontSize": "18px"}, "flowchart": {"htmlLabels": false, "curve": "linear", "nodeSpacing": 24, "rankSpacing": 32}}}%%
flowchart TB
H["ClojureScript 应用程序
处理程序 + 中间件"]
F["FastTwitch
原生 Fetch 适配器"]
N["Node · Deno · Bun
HTTP 应用程序"]
X["FastTwitch Celld
Cell 入口点"]
C["Cell: room-42
HTTP · RPC · 事件"]
S[("私有 SQLite / KV
消息历史")]
H --> F
F --> N
F -->|扩展| X
X --> C
C <-->|"持久化 / 恢复"| S
classDef application fill:#f8fafc,stroke:#64748b,color:#0f172a
classDef bridge fill:#eff6ff,stroke:#2563eb,color:#0f172a
classDef durable fill:#ecfdf5,stroke:#059669,color:#0f172a
class H,N application
class F,X bridge
class C,S durable主线: 共享的运行时和 Web API 使其成为一种通用的应用程序模型,而不是为每种语言或主机提供单独的网络栈。工作流日志保存已完成的工作,运行时快照保存执行状态,持久化对象保存可寻址单元的数据和生命周期。这些是让现代工作负载在单个请求或进程之外生存的不同方式。
参考资料
- 活动描述
- Fetch API
- Web API: Node.js, Deno, Bun
- 引擎: QuickJS, dquickts
- workerd; Workers Web API
- Durable Objects; 工作流休眠/重试
- celld; 兼容性
- Deno 的转型; celld/workerd 集成计划
- 语言: ClojureScript; ReScript
- FastTwitch: 文档; 源码; Celld 扩展

