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

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

源码浏览已生成;不能解析的文件以纯文本展示,不表示通过类型检查。

说明.汉语.md

# 网页事件流云工适配

`流。豫` 用豫言写在云工宿主库之上,**不增加任何宿主 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())`:

```js
// 文言:事毕(成、败、逾时皆然),其未终之写器则终之,未退之读器则退之。
// 汉语:事件结束时收尾该事件句柄表里仍登记的流写器与读取器:正常结束则 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`。复跑方法见该目录说明。

说明.文言.md

# 网页事件流云工适配

`流。豫` 以豫言书于云工宿主库之上,**不增宿主 JavaScript 一字**。其行[网页事件流公约](../../../豫言操作系统接口/网页事件流/规范.文言.md) 0.1.0:即提响应头、逐块书文(受背压)、察断、正终与异终。

## 所以行之

- **响应与背压**:造 `TransformStream`,其可读端授 `Response` 为体,以 `云工设置响应句柄` 提之;宿主于 `fetch`、`service-fetch`、`durable-fetch` 之事用 `Promise.race([响应已备, 运行毕])`,故响应一设,`fetch` 即返,Wasm 续行。可写端之写器司书:`writer.write(字节)` 待读者来取而后成,此即背压。
- **察断**:造“断开候”之承诺——`scheduler.wait(二十四时, {signal: AbortSignal.any([请求信号, 我方信号])})` 再以 `Promise.allSettled` 裹之(永不拒)。每书与每闭,皆使操作之承诺与之 `Promise.race`,**断开候居前**:请求信号一止(客断),无论书本身能成与否皆返一;操作被拒(响应体读端已取消)亦返一。`入站事件流已断开` 读请求信号之 `aborted` 与写器之 `desiredSize`(误则 `null`)。流终、止或断之后,经“我方信号”撤计时而释诸柄,不留悬挂之时。
- **状与柄**:每 Wasm 实例内有一登记簿(流柄、写器号、状态引用、请求信号号、断开候号、我方控制器号)。流柄为“实例标记:写器句柄号”,实例标记取 `crypto.randomUUID()` 之首十三字,故他事(句柄号纵同)得之亦辨为无效。状态零开、一客断、二已终或止、三断而后终或止。
- **校验**:状态、标头(令牌、值字节、禁名、重复、数)皆于提响应头之前由豫言校之,违则抛豫言之异;书之大小先于状态之辨;事类非 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` 之 `造全局`。
三、**事之限**:每事之豫言实例有墙钟之限,默认三十秒;需更久之流(长联)者,于应用目录置 `执行配置.json`,如 `{"事件时限毫秒":{"durable-fetch":900000}}`(至多十五分钟)。

## 本地 workerd 之实测(Wrangler 4.129.0,兼容日 2026-09-10,`enable_request_signal`,持久对象内 `durable-fetch`,真实 HTTP)

- 响应头先于程序毕而至;帧序、UTF-8、六十四 KiB 之界、三千小块、八十个六十四 KiB 之块、十二并发流,皆正;线上见附加标头,`Content-Type` 不可覆。
- 背压宽:读者止读八百毫秒,书者已出二百六十七至三百四十八个万字节之帧(套接字与管道之缓冲约三 MB 之量),而后乃阻。
- 客中止 `fetch` 后应用续行:书约八十五至一百三十毫秒内返一,毕 D1 结算、终、终后书返二。**空闲之联不即被见**:断后首、二次之书仍返零,请求信号待书败而后止,第三次书返一(本地 HTTP/1.1;见组合范例三分片与心跳三百五十四毫秒之察);故久持之流当按期书心跳。
- `止`:HTTP/1.1 之客所读者为**正终**(分块之尾照发),宿主志记未捕之异;欲使客可靠知败者,当先于流中书一帧误事件。
- 未终未止而程序返:客久候(二十秒不见终)。
- 事逾墙钟之限:宿主于下一调即败(约三十秒,书止),响应流仍开而无人终之;订阅同不收。见下“宿主改动之请”。
- 云端(Cloudflare 边缘)之行——空闲流之限时、断是否即告、异终于 HTTP/2 之所见——**待云端验证**。

## 宿主改动之请(致宿主壳之守者;此适配不改宿主 JS)

**事毕,收尾流之写器与读取器。** 今宿主于事毕(正、败、逾时)不理仍登于事句柄表之流:未终之响应流悬挂,未取之订阅泄漏(云工每实例仅六十四频道之额,泄漏可尽之,此后订新频道皆得 HTTP 500)。宜于 `宿主.mjs` 之 `执行` 中、`运行毕` 之前加下列收尾,并于 `句柄.mjs` 所返之物增 `全部: () => Array.from(表.values())`:

```js
// 文言:事毕(成、败、逾时皆然),其未终之写器则终之,未退之读器则退之。
// 汉语:事件结束时收尾该事件句柄表里仍登记的流写器与读取器:正常结束则 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` 之限至三百毫秒)则以“豫言执行超过时限”终而不悬挂,遗订被收;本地 workerd(真持久对象)中:未终之流于程序返时以正终收场,长流于三十秒之限而止,不复悬挂,遗订之数归零;一致性验证三组测试于补丁宿主上尽通过(二则“已知限制(待宿主改进)”之测由待办转通过)。`WritableStreamDefaultWriter` 与 `ReadableStreamDefaultReader` 于 workerd 与 Node 皆为全局之类,`instanceof` 可用。

## 一致性验证

`一致性验证/` 含试验应用 `事件流一致性应用`(实 Wasm)与 Node 测试 `事件流一致性.test.mjs`(响应头先至、帧序、六十四 KiB 之界与大量小块、背压、读端取消与请求信号中止二种断、断后收尾、止、终后之义、无效与他事之流柄、非 HTTP 事、并发流、`durable-fetch`、附加标头诸校验之支),并有于真实 workerd 复验之驱动 `事件流云端复验.test.mjs`。复行之法见该目录之说明。