网页事件流云工适配
流。豫 用豫言写在云工宿主库之上,不增加任何宿主 JavaScript。它实现[网页事件流接口规范](../../../豫言操作系统接口/网页事件流/规范.汉语.md) 0.1.0:立即提交响应头、逐块写入(受背压)、断开检测、正常结束与异常终止。
实现
- 响应与背压:创建
TransformStream,可读端交给Response作正文,用云工设置响应句柄提交;宿主对fetch、service-fetch、durable-fetch事件用Promise.race([响应已备, 运行毕]),所以响应一设置fetch就返回,Wasm 继续运行。可写端的写器负责写入:writer.write(字节)在读者拉取之前不会完成,这就是背压。 - 断开检测:创建“断开等待”承诺——
scheduler.wait(24 小时, {signal: AbortSignal.any([请求信号, 我方信号])})再经Promise.allSettled包装(从不拒绝)。每次写入或关闭都让操作承诺与它Promise.race,断开等待排在前面:请求信号一旦中止(客户端断开),无论写入本身能否完成都返回 1;操作被拒绝(响应体读端已取消)也返回 1。入站事件流已断开读请求信号的aborted与写器的desiredSize(出错为null)。流结束、中止或断开后通过“我方信号”撤销计时器并释放句柄,不留悬挂的计时。 - 状态与流柄:每个 Wasm 实例内有一张登记簿(流柄、写器号、状态引用、请求信号号、断开等待号、我方控制器号)。流柄是“实例标记:写器句柄号”,实例标记取自
crypto.randomUUID()的前 13 个字符,所以另一个事件(哪怕句柄号相同)拿到它也被识别为无效。状态 0 开放、1 客户端已断开、2 已结束或中止、3 断开后又结束或中止。 - 校验:状态、标头(token、值字节、禁用名、重复、数量)全部在提交响应头之前由豫言校验,违规抛豫言异常;写入大小先于状态判断;事件种类不是 HTTP 事件时抛异常。中止用
writer.abort(new Error(原因)),结束用writer.close(),都不在慢读者上阻塞。 - 所有内部辅助名都以
流内起首,避免与应用或别的适配同名。
部署要求
- 兼容标志 `enable_request_signal`:工作者配置的
compatibility_flags必须包含它。没有它请求的 AbortSignal 不会中止(本地 workerd 已验),断开无从察觉,客户端走后写入可能永久停在背压上。云仓的wrangler.jsonc需要为使用本包的应用加上这一项。 - 全局 `scheduler.wait` 与 `AbortSignal.any`:Workers 自带;Node 宿主没有全局
scheduler,用node:timers/promises的scheduler垫片经创建云工宿主({全局})传入,见一致性验证/工具.mjs的造全局。 - 事件时限:每个事件的豫言实例有墙钟时限,默认 30 秒;需要更久的流(长连接)在应用目录放
执行配置.json,例如{"事件时限毫秒":{"durable-fetch":900000}}(至多 15 分钟)。
本地 workerd 实测(Wrangler 4.129.0,兼容日期 2026-09-10,enable_request_signal,持久对象内 durable-fetch,真实 HTTP)
- 响应头先于程序结束到达;帧序、UTF-8、64 KiB 边界、3000 个小块、80 个 64 KiB 块、12 个并发流均正确;线路上出现附加标头,
Content-Type不可覆盖。 - 背压是宽的:读者停读 800 毫秒后写入方已写出 267 至 348 个 10 KB 帧(套接字与管道缓冲约 3 MB 量级),随后才阻塞。
- 客户端中止
fetch后应用继续运行:写入约 85 至 130 毫秒内返回 1,并完成 D1 结算、结束、结束后写入返回 2。空闲连接不会立刻被发现:断开之后第一、二次写入仍返回 0,请求信号在写入失败之后才中止,第三次写入返回 1(本地 HTTP/1.1,见组合范例的 3 个分片与 354 毫秒的心跳观察);所以长期保持的流要按周期写心跳。 中止:HTTP/1.1 客户端读到的是正常结束(分块结尾照发),宿主日志记未捕获异常;需要客户端可靠得知失败时,应在流里先写一帧错误事件。- 未调用结束或中止而程序返回:客户端一直等待(20 秒无终止)。
- 事件超过墙钟时限:宿主在下一次调用时失败(约 30 秒时,写入停止),响应流保持打开无人终止;订阅同理不会回收。见下“宿主改动请求”。
- 云端(Cloudflare 边缘)行为——空闲流的超时、断开是否立即通知、异常终止在 HTTP/2 上的可见形态——待云端验证。
宿主改动请求(写给宿主壳维护者;本适配不改宿主 JS)
事件结束时收尾流写器与读取器。 当前宿主在事件结束(正常、失败或超时)时不处理仍登记在事件句柄表里的流:未结束的响应流会悬挂,未取消的订阅会泄漏(云工每实例仅 64 个频道名额,泄漏能把它们耗尽,此后订阅新频道都得 HTTP 500)。建议在 宿主.mjs 的 执行 里、运行毕 之前加入下面的收尾,并在 句柄.mjs 的返回对象里增加 全部: () => Array.from(表.values()):
// 文言:事毕(成、败、逾时皆然),其未终之写器则终之,未退之读器则退之。
// 汉语:事件结束时收尾该事件句柄表里仍登记的流写器与读取器:正常结束则 close 写器;失败或超时则 abort 写器,使响应流以错误终止而不悬挂;读取器一律 cancel,使订阅得以回收。
const 收尾流 = 失败 => {
for (const 值 of 句柄.全部()) {
try {
if (值 instanceof 全局.WritableStreamDefaultWriter) void (失败 === undefined ? 值.close() : 值.abort(失败)).catch(() => {});
else if (值 instanceof 全局.ReadableStreamDefaultReader) void 值.cancel().catch(() => {});
} catch { /* 已释放或已终的流不再收尾。 */ }
}
};
// 在 `运行().then(() => {` 的开头调用 收尾流();
// 在 `.catch(错 => {` 的开头调用 收尾流(错);
已在私有 dist 副本上验证(Node 与本地 workerd 两处):打补丁后,未结束的流在程序返回时正常结束、事件超时(把 durable-fetch 时限压到 300 毫秒)时以“豫言执行超过时限”终止而不悬挂、遗留订阅被回收;本地 workerd(真实持久对象)里:未结束的流在程序返回时以正常结束收场,长流在 30 秒时限处终止而不再悬挂,遗留订阅数归 0;一致性验证的三组测试在补丁宿主上全部通过(两条“已知限制(待宿主改进)”测试由待办转为通过)。WritableStreamDefaultWriter 与 ReadableStreamDefaultReader 在 workerd 与 Node 里都是全局类,instanceof 可用。
一致性验证
一致性验证/ 含试验应用 事件流一致性应用(真实 Wasm)与 Node 测试 事件流一致性.test.mjs(响应头先到、帧序、64 KiB 边界与大量小块、背压、读端取消与请求信号中止两种断开、断开后收尾、中止、结束后语义、无效与他事流柄、非 HTTP 事件、并发流、durable-fetch、附加标头的全部校验分支),以及在真实 workerd 里复验的驱动 事件流云端复验.test.mjs。复跑方法见该目录说明。