跳到正文
豫言包管理
← 全部包

豫言操作系统网页事件流适配 0.1.0

豫言 · 库

以云工的 TransformStream 与 Response 实现网页事件流接口:响应头立即提交,逐块写入受背压并可感知客户端断开。

网页事件流云工适配

流。豫 用豫言写在云工宿主库之上,不增加任何宿主 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(),都不在慢读者上阻塞。
  • 所有内部辅助名都以 流内 起首,避免与应用或别的适配同名。

部署要求

  1. 兼容标志 `enable_request_signal`:工作者配置的 compatibility_flags 必须包含它。没有它请求的 AbortSignal 不会中止(本地 workerd 已验),断开无从察觉,客户端走后写入可能永久停在背压上。云仓的 wrangler.jsonc 需要为使用本包的应用加上这一项。
  2. 全局 `scheduler.wait` 与 `AbortSignal.any`:Workers 自带;Node 宿主没有全局 scheduler,用 node:timers/promises 的 scheduler 垫片经 创建云工宿主({全局}) 传入,见 一致性验证/工具.mjs 的 造全局。
  3. 事件时限:每个事件的豫言实例有墙钟时限,默认 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。复跑方法见该目录说明。

依赖

查看包配置

上传历史

当前上传修订:1 ·

  1. 上传修订 1
    下载 ZIP

包层级

父包:豫言操作系统适配

没有子包。