/** * [ref] A9 seam —— 通用可恢复 SSE 泵(P2.8),从 `http/server.ts` 整段搬出的原文。 * * task_run 与 image_bake 两条日志共用同一套 liveness/heartbeat/416/终态复取/15 分钟帽逻辑;域模块 * (routes/images.ts)与 server.ts 都要用它,所以它必须住在 http/ 叶子里(routes/* 值 import server.ts * 会闭合运行时装载环)。 */ import type { IncomingMessage, ServerResponse } from "node:http"; export interface SseLogProvider { /** The coarse terminality status + the freshness clock (`updatedAt`). Undefined/null ⇒ unknown id (the * caller already 404'd existence before opening the stream, so this is the in-flight terminal poll). */ statusOf(id: string): Promise<{ status: string; updatedAt: string; } | null | undefined>; /** Events with seq strictly greater than `after`, ascending. */ getEvents(id: string, after: number): Promise; /** Earliest retained seq → the 416 resume boundary. */ retainedFrom(id: string): Promise; /** Map ONE event to its SSE frame (the `id:`/`event:`/`data:` lines differ per log). */ formatEvent(ev: E): { id: number; event: string; data: unknown; }; /** The synthetic frame written when the log is stale (no events + `updatedAt` older than `staleMs`). */ staleFrame(): { event: string; data: unknown; }; /** * [ref] 车3 §5:**开流 preamble** —— 在 416 判定与 `sseHeaders` **之后**、日志 tail **之前**写的 * 一批**非账本**帧(今天唯一的用户 = 审批未决卡的全量对账基准)。 * * 🔴 为什么必须是这里的内部 hook,而不是调用方在 `streamSseLog` **之前**自己写:本 helper 的判序是 * ①读 `last-event-id`/`?from=` → ②保留期比对、超界直接 `sendError(416)` → ③`sseHeaders` → ④tail。 * 在 ① 之前写任何字节 = 提前提交 200 头 ⇒ ② 的 416 分支**结构上不可能再触发**(或触发 * headers-already-sent),而调用方自己先调 `sseHeaders` 又会被本 helper 重复写头。 * * 返回帧**不带 `id:`** —— 它不是账本行,不参与 `Last-Event-ID` 游标(壳的规则:preamble 给的是 * 权威全量卡集,账本重放里的同 id 帧只用于时间线渲染)。 * * 缺席 = 零影响(其余 provider 不实现)。失败/挂起的处置见 {@link collectSsePreamble}。 * * `signal`([ref] 车3 刀 3b,§14.2 遗留记账):有界窗到点时**被 abort** —— 实现方应把它传进自己的 * store 读口,让那次读当场以 abort 拒绝,而不是留一个「结果已被丢弃、调用链却还在飞」的悬挂读。 * 可达到的语义与残留边界见 `ApprovalAskStore.listPendingBySession` 的 `signal` 注(驱动无取消 seam)。 */ preamble?(signal: AbortSignal): Promise>; /** * `preamble` 被有界超时掐断("timeout")或抛出("error")时的**观测**钩(fail-open,已经决定不挡 * 开流了,这里只让域模块用自己的 logger 记一条 warn —— 本叶子模块没有、也不该有 logger)。 */ onPreambleFailure?(reason: "timeout" | "error", err?: unknown): void; } /** [ref] 车3 §5.3:preamble 的**有界**等待上限。挂起的 store 不许拖住开流——开流是壳的主路径, * 而 preamble 只是一个锦上添花的对账基准。超时即按「无卡集基准」继续(与今天等同)。 */ export declare const SSE_PREAMBLE_TIMEOUT_MS = 2000; /** * 跑一次 preamble,**有界 + fail-open**([ref] 车3 §5.3)。 * * 🔴 只 catch throw 是不够的:store **挂起**(不抛)会把整条开流一起拖住。所以这里是 `Promise.race` * 而不是 try/catch —— 两种失败形(抛 / 不返回)各自有一条出路,都落到「零帧 + 一次 warn + 继续开流」。 * * 🔴 [ref] 车3 刀 3b(§14.2 遗留记账兑现):超时**同时 abort** 一个交给 `preamble` 的 `AbortSignal`。 * 此前只是「丢结果」,原 promise 仍在飞 —— DB 挂死时的重连风暴会一轮轮往连接池里堆等待者。现在读口 * 拿到 abort 会当场拒绝,调用链不再累积;库那侧的那条查询仍会跑完(驱动无取消 seam,残留边界见 * `ApprovalAskStore.listPendingBySession` 的注)。丢结果这条语义不变:preamble 只读,丢弃零副作用。 * * 导出给 `routes/tasks.ts` 的 sync 腿直接用 —— 那条腿不走 `streamSseLog`(它不是账本 tail),但 * 「有界 + fail-open」这条判据必须是**同一份实现**,不能各腿各写一遍。 */ export declare function collectSsePreamble(preamble: (signal: AbortSignal) => Promise>, onFailure?: (reason: "timeout" | "error", err?: unknown) => void, timeoutMs?: number): Promise>; /** The shared resumable SSE pump (P2.8) — provider supplies the log-specific differences; the liveness/ * heartbeat/416/terminal-re-fetch/15-min-cap logic is identical for task_run + image_bake. */ export declare function streamSseLog(req: IncomingMessage, res: ServerResponse, provider: SseLogProvider, id: string, staleMs: number): Promise; export declare function sleep(ms: number): Promise; //# sourceMappingURL=sse-log.d.ts.map