Rindle

API index and search · Build metadata

Source snapshot

packages/api-server/src/streams.ts

Source revision 05d0bf2c2e56 · build details
Source revision: 05d0bf2c2e56.
TypeScript input SHA-256: aabe6cfcc4172b870d5e272142958e9ea8d8784c2aa23133156e5a7ee633318e
Generated 2026-09-04T23:58:25.590Z with TypeScript 6.0.3. Public TypeScript checks and declaration emit passed. Package runtime tests are separate.
1// LM stream checkpointing — the two-plane response path (designs-implemented/LM-STREAM-CHECKPOINT-DESIGN.md).2//3// A model response arrives as hundreds of tiny deltas per second. Every one wants to be on a screen4// immediately; none wants to be a durable write. So the response runs on TWO planes sharing ONE5// monotone coordinate (`seq`, in UTF-16 code units of the response text):6//7//   - the LIVE plane (this module): process-local, volatile, every delta, straight to subscribers;8//   - the DURABLE plane: the app's own tables, advanced at coarse boundaries — N chars, T ms, an9//     explicit `flush()`, and always at close.10//11// The contract both planes stand on (§0):12//13//   S (splice)  durableText(f) ++ concat(frames delivered from f) == producedText, for every join14//               offset f and every join time.15//   P (prefix)  durableText is always a PREFIX of producedText, and durableSeq is monotone.16//17// P is what makes S mechanical — two prefixes of one string merge by taking the longer (see18// {@link spliceStreamText}). Everything else here is bookkeeping in service of those two.19//20// TWO THINGS THIS DELIBERATELY IS NOT (both were wrong in the first cut of this module):21//22//   1. Checkpoints are SYSTEM WRITES, not mutation envelopes. `lmid` is client-authored and exists23//      to release a client's optimistic rebase point; a server-authored checkpoint has no24//      prediction to release, and CDC → IVM fanout needs no envelope at all. Checkpoints therefore25//      go through `outsideSql` with no clientID/mid — the same rule the realtime lifecycle writes26//      follow ("a system write must never advance an lmid").27//   2. A checkpoint INSERTS A CHUNK ROW; it never appends to a growing column. A normalized `edit`28//      carries BOTH the old and the new full row, so advancing a `body` column would re-stream the29//      whole message twice per checkpoint — O(n²) wire to deliver n characters. One row per chunk30//      makes each checkpoint a single `add` carrying only its own slice.31//32// Durability is never overclaimed (§0, the RINDLE-REALTIME §8.1 "ack honesty" rule): a `chunk` frame33// says only "produced", a `durable` frame says "committed", an `end` frame says "sealed". Losing34// this plane entirely costs latency and the un-checkpointed tail — never consistency: a client with35// no live stream still sees the message grow through IVM at checkpoint granularity, which is why36// `absent`/`stale` are ordinary answers rather than errors.3738// Type-only (erased at runtime) — no import cycle with ./index.ts.39import { STREAM_STATUS_STREAMING, frameResumePoint } from "@rindle/client";40import type { StreamFrame, StreamStatus } from "@rindle/client";41import type { SqlStatement } from "@rindle/daemon-client";4243import type { Authorizer, ServerSql, SqlDialect } from "./index.ts";4445// ------------------------------------------------------------------------------- the wire46//47// The frame shapes and the two pure reassembly functions live in `@rindle/client` — BOTH tiers need48// them, and a browser must never pull this package to read a stream. Re-exported here so the49// server-side import path is unchanged.5051export {52  STREAM_STATUS_STREAMING,53  assembleDurableText,54  frameResumePoint,55  spliceStreamText,56} from "@rindle/client";57export type { StreamFrame, StreamStatus } from "@rindle/client";5859// ------------------------------------------------------------------------------- the durable seam6061/** What a checkpoint hands the durable plane when the app supplies its own {@link StreamCommit}. */62export type StreamCommitInput =63  /** The pointer is marked live. The app's own mutator created the row (it owns `chatId`, `role`,64   *  the model name…); this only flips it to `streaming`. */65  | { kind: "open"; streamId: string; meta: unknown; hostId?: string; startedAt: number }66  /** A prefix advance carrying ONLY its own slice. `text.length === seq - from`, and `from` is the67   *  last append the PLANE saw confirmed — so appends are contiguous in the fault-free run, but an68   *  append that committed while its ack was lost makes the next one RE-COVER (its `from` lags what69   *  the app already applied). See {@link StreamCommit} for the two ways to stay idempotent. */70  | { kind: "append"; streamId: string; from: number; seq: number; text: string }71  /** The seal. `body` is the producer's retained text and `bodyFrom` its absolute start offset:72   *  `bodyFrom === 0` — and `body` is the WHOLE response — unless the app opted into trimming via73   *  `retainChars` (`commit` mode only). A compacting app requires `bodyFrom === 0` and writes74   *  `body` wholesale; a non-compacting app appends the outstanding tail —75   *  `body.slice(from - bodyFrom)` — as a final chunk. `seq === bodyFrom + body.length`, always. */76  | { kind: "close"; streamId: string; from: number; seq: number; body: string; bodyFrom: number; status: StreamStatus; error?: string };7778/** The escape hatch: persist the checkpoint however the app likes. Retried on throw (§3.3), so it79 *  must be idempotent under a repeated `(from, seq)` — AND under a RE-COVER: an append whose write80 *  committed but whose ack was lost leaves the plane believing less than the store holds, so the81 *  next append's `(from, seq)` can OVERLAP text already applied. Apply only the unseen suffix82 *  (`text.slice(applied - from)` when `applied > from`), or better, return `{seq: applied}` — the83 *  authoritative applied length — and the plane resynchronizes instead of re-covering at all.84 *  Return `{cancelRequested: true}` to tell the producer the reader asked it to stop (§6). */85export type StreamCommit = (86  input: StreamCommitInput,87) => Promise<void | { cancelRequested?: boolean; seq?: number }>;8889/** Which columns of the app's own tables the plane reads and writes. Every entry has a default90 *  except `cancel`, `error`, and `host`, which are opt-in BY NAMING: the plane never emits SQL91 *  against a column the app did not ask it to use. */92export interface StreamColumns {93  /** The message row's primary key, matched against `streamId`. Default `id`. */94  key?: string;95  /** The compacted response text. Default `body`. */96  body?: string;97  /** {@link STREAM_STATUS_STREAMING} then a {@link StreamStatus}. Default `status`. */98  status?: string;99  /** Total durable length — `length(body) + Σ chunk lengths`. The CAS column (§3.2). Default `seq`. */100  seq?: string;101  /** Opt-in (§6): a truthy value here stops the generation at the next checkpoint. No default —102   *  naming it is what turns cancellation on. */103  cancel?: string;104  /** Opt-in: where a failed generation's message is recorded. No default. */105  error?: string;106  /** Opt-in: where the open write records this producer's identity ({@link RindleStreamOptions.hostId}).107   *  Naming it upgrades the row-level single-flight guard from check-then-act to a true108   *  compare-and-swap — the open write turns conditional and a read-back names the winner (§5.1) —109   *  and gives multi-instance subscribe routing a column to read (§4). No default. */110  host?: string;111  /** Chunk primary key; the plane writes the deterministic `"<streamId>:<seq>"`. Default `id`. */112  chunkKey?: string;113  /** Chunk → message reference. Default `streamId`. */114  chunkStream?: string;115  /** The chunk's END offset — the ordering key. Default `seq`. */116  chunkSeq?: string;117  /** The chunk's slice of the response. Default `text`. */118  chunkText?: string;119}120121/**122 * The app's tables (§5). The app authors and migrates BOTH — the message row is unambiguously123 * app-owned (it has `chatId`, `role`, token counts) and the chunk row must be reachable from the124 * app's own query as a `related` subquery, which a Rindle system table would make awkward. The plane125 * only needs to be told where things live. Use {@link streamChunkTableDdl} for the chunk table's126 * migration.127 *128 * The message table carries WHATEVER ELSE the app wants; the plane's hard requirements are only:129 *130 * | mapped column | requirement | why |131 * | --- | --- | --- |132 * | `key`    | UNIQUE (normally the pk) | every checkpoint targets one row by it |133 * | `seq`    | integer, **NOT NULL DEFAULT 0** | the CAS column — nothing matches NULL (§3.2) |134 * | `body`   | text, empty at open | compaction overwrites it with the whole response |135 * | `status` | text accepting `streaming`/`complete`/`cancelled`/`error`/`interrupted` | a CHECK136 *              constraint that omits one of these turns a seal into an infra failure |137 * | `cancel` | truthy-readable, if mapped | read on the checkpoint round-trip (§6) |138 * | `error`  | nullable text, if mapped | compaction writes `NULL` when there is no error |139 * | `host`   | text, if mapped | written at open with the producer's token; the read-back decides the open race (§5.1) |140 *141 * `body` is PLANE-OWNED and always a bare string. Rich content (an array of content blocks, tool142 * calls, attachments) belongs in SIBLING columns the app's own mutators write — `flush()` orders the143 * text before them. For genuinely multi-block streaming, point `message` at a per-BLOCK table144 * instead: `streamId` is just an app key, so one stream per block needs nothing from this plane.145 */146export interface StreamTables {147  /** The app's message table. Must already contain the row when {@link StreamPlane.open} runs. */148  message: string;149  /** The append-only chunk table. */150  chunks: string;151  columns?: StreamColumns;152}153154export type StreamCheckpointTarget = { tables: StreamTables } | { commit: StreamCommit };155156// ------------------------------------------------------------------------------- configuration157158/** When a checkpoint fires — the FIRST of these wins (§3.1). Checkpoints are serialized, so a slow159 *  store degrades to fewer, larger checkpoints, never to a queue of them. */160export interface StreamCheckpointPolicy {161  /** Produced-but-uncommitted characters that force a checkpoint. Default 512. */162  chars?: number;163  /** Milliseconds since the last checkpoint that force one. Default 750. */164  intervalMs?: number;165  /** Retries for a failing commit before the slice is left for the next trigger. Default 3. */166  retries?: number;167}168169export interface AuthorizeStreamInput<User> {170  user: User;171  streamId: string;172  /** Where the subscriber claims to be. */173  from: number;174  /** The `meta` this stream was opened with — `undefined` when the stream is not hosted here, which175   *  is precisely when the app must decide from `streamId` and its own durable state. */176  meta: unknown;177  request?: unknown;178}179180/**181 * Optional cross-process transport for the LIVE plane182 * (designs-implemented/LM-STREAM-RELAY-DESIGN.md). Both methods are independently optional; which183 * ones you implement is which topology you built — an addressing adapter (a Durable Object named by184 * `streamId`, `fly-replay`) implements only `attach`; a broadcast adapter (Redis pub/sub, NATS)185 * mirrors with `publish` and subscribes with `attach`; a log adapter (Redis Streams, Kafka) appends186 * and replays. Never consulted for the durable plane — checkpoints are unaffected by any of this.187 *188 * The plane does not trust what `attach` yields: frames are run through the conform pass189 * ({@link StreamRelayConform}) and any contract violation downgrades the subscription to `stale`,190 * which already means "you are on the durable plane now". A broken relay costs a reader smooth191 * tokens, never corrupted text — and can never reach the producer.192 */193export interface StreamRelay {194  /** Producer side: every frame this process's producer fans out (`chunk`, `durable`, and the195   *  terminal `end`), mirrored outward. MUST NOT block; a throw or rejected promise is caught and196   *  routed to {@link RindleStreamOptions.onRelayError} — a relay outage may cost the live leg,197   *  never the generation. Returned promises are observed but never awaited. */198  publish?(streamId: string, frame: StreamFrame): void | PromiseLike<void>;199  /** Subscriber side: this process is not hosting `streamId`. Return a frame source, or `undefined`200   *  for `absent` — exactly the no-relay answer. Consulted only AFTER `authorize` has passed, and201   *  only on a live-plane miss (a local stream always wins). The plane closes the source202   *  (`return()`) when the reader disconnects. An adapter that cannot serve `from` (pub/sub has no203   *  history) yields `stale` and stops — the reader converges on the durable plane (§5). */204  attach?(streamId: string, from: number): Promise<AsyncIterable<StreamFrame> | undefined>;205}206207export interface StreamRelayErrorInfo {208  streamId: string;209  /** Where it failed: mirroring a frame out (`publish`), dialing the adapter (`attach`), or210   *  consuming/conforming its frames (`frames`). */211  phase: "publish" | "attach" | "frames";212}213214export interface RindleStreamOptions<User> {215  /** Where checkpoints land: the app's tables (the default path) or a raw `commit` callback. */216  checkpoint: StreamCheckpointTarget;217  /** REQUIRED. Subscribing to a stream is reading someone's chat, so there is no default-allow.218   *  Runs BEFORE existence is checked, so a denial cannot be used to probe for stream ids. */219  authorize: Authorizer<AuthorizeStreamInput<User>>;220  policy?: StreamCheckpointPolicy;221  /** This process's identity — it must be UNIQUE per producer process, because the open CAS trusts222   *  it to distinguish rivals (§5.1). In `tables` mode, map {@link StreamColumns.host} and the open223   *  write persists it on the message row (so the app can route later subscribers to the hosting224   *  instance, §4) and uses it as the single-flight token; setting it WITHOUT a mapped `host` column225   *  is refused at construction. In `commit` mode it rides the `open` input. When a `host` column is226   *  mapped and no hostId is given, a random per-plane token is used — the CAS still holds, routing227   *  just has no stable name to read. */228  hostId?: string;229  /** Slack retained BELOW `durableSeq` so a client whose IVM view lags a checkpoint can still join230   *  without a `stale` round trip. Text at or above `durableSeq` is never trimmed. Default 64 KiB.231   *  `commit`-mode only — REFUSED (a construction-time `TypeError`) in `tables` mode, where232   *  compaction needs the whole produced text at close (§3.4), so the buffer is retained in full. */233  retainChars?: number;234  /** How long a sealed stream stays joinable before eviction. Default 30s. */235  lingerMs?: number;236  /** Per-subscriber frame queue cap; overflow drops that subscriber with `stale` (§4). Default 1024.237   *  Relayed readers reuse the same bound: a slow reader on a relayed stream costs itself the live238   *  leg exactly as a local one does. */239  maxQueuedFrames?: number;240  /** A checkpoint that exhausted its retries. The stream keeps streaming — this is a durability241   *  stall, not a stream stall — so the error must not vanish. Absent ⇒ `console.error`. */242  onCheckpointError?: (err: unknown, info: { streamId: string; from: number; seq: number }) => void;243  /** Cross-process transport for the live plane ({@link StreamRelay}). Without one, a subscriber244   *  that lands on a process not hosting its stream gets `absent` and reads the durable plane at245   *  checkpoint granularity — correct, just chunky. */246  relay?: StreamRelay;247  /** Bound on `relay.attach`: a hung adapter yields `absent`, not a hung HTTP request. An248   *  addressing adapter MAY deliberately spend this window waiting out a subscribe that races its249   *  own kick. Default 2000. */250  relayAttachTimeoutMs?: number;251  /** A diagnostic, never a control path: relay failures (a throwing or rejecting `publish`, a failed252   *  or timed-out `attach`, a conform violation in the frames) land here, wrapped so a throwing hook253   *  cannot reach the plane. The reader-facing outcome is always the same legal `absent`/`stale`.254   *  Absent ⇒ `console.error`. */255  onRelayError?: (err: unknown, info: StreamRelayErrorInfo) => void;256}257258// ------------------------------------------------------------------------------- producer handle259260export interface OpenStreamInput<User> {261  user: User;262  /** The app's message-row id: the durable pointer AND the live plane's key. The row must already263   *  exist (the app's own mutator wrote it, alongside the user's prompt) with `seq = 0`. */264  streamId: string;265  /** Opaque app payload, passed through to a `commit` callback. Unused in `tables` mode. */266  meta?: unknown;267  request?: unknown;268}269270/** The producer's handle (§2). One writer per stream, by construction. */271export interface StreamHandle {272  readonly streamId: string;273  /** Total produced code units. */274  readonly seq: number;275  /** Total code units the store has committed. Never exceeds {@link seq} (contract P). */276  readonly durableSeq: number;277  /** True once a checkpoint round-trip has seen the reader's cancel flag (§6). `pump` stops on it;278   *  a hand-rolled generation loop should check it. */279  readonly cancelled: boolean;280  /** Append a delta: fanned to subscribers synchronously, checkpointed on policy. */281  push(text: string): void;282  /** Force a checkpoint and resolve once the store holds every character produced so far. This is283   *  the ORDERING primitive: `await flush()` before writing a discrete row (a tool call, a stop284   *  reason) so the text precedes it in the store (§1). Rejects if the checkpoint cannot commit. */285  flush(): Promise<number>;286  /** Drain a delta iterable into the stream (the shape every LLM SDK's text stream already has),287   *  stopping early — and closing the iterator, which aborts the underlying request — once the288   *  reader has cancelled. */289  pump(deltas: AsyncIterable<string>): Promise<void>;290  /** Seal the stream: `cancelled` if the reader asked it to stop, else `complete`. In `tables` mode291   *  this is the compaction (§3.4) — it writes the whole body and drops the chunk rows in one292   *  transaction, so it also REPAIRS any checkpoint that failed along the way. */293  close(): Promise<void>;294  /** Seal `error` at whatever was produced. Never throws for the reason it is sealing. */295  fail(error: unknown): Promise<void>;296}297298export interface SubscribeStreamInput<User> {299  user: User;300  streamId: string;301  /** "I already have this many characters" — from the client's IVM view, or a `Last-Event-ID`.302   *  A non-negative integer; default 0. */303  from?: number;304  request?: unknown;305}306307export interface StreamSubscription {308  readonly streamId: string;309  /** Terminates after exactly one of `end` / `stale` / `absent`. */310  readonly frames: AsyncIterable<StreamFrame>;311  /** Detach early (a disconnected client). Idempotent. */312  close(): void;313}314315// ------------------------------------------------------------------------------- defaults316317const DEFAULT_CHECKPOINT_CHARS = 512;318const DEFAULT_CHECKPOINT_INTERVAL_MS = 750;319const DEFAULT_CHECKPOINT_RETRIES = 3;320const DEFAULT_RETAIN_CHARS = 64 * 1024;321const DEFAULT_LINGER_MS = 30_000;322const DEFAULT_MAX_QUEUED_FRAMES = 1024;323const DEFAULT_RELAY_ATTACH_TIMEOUT_MS = 2000;324/** Retry backoff base — 50ms, 100ms, 200ms, … A checkpoint failure is usually a blip at the write325 *  authority; the text is safe in the buffer meanwhile, so there is nothing to rush. */326const RETRY_BACKOFF_MS = 50;327328/** A SCHEDULING timer — the checkpoint cadence, the linger window, the SSE keep-alive. It never329 *  holds the process open: none of them is work in flight, and a stream plane must not keep a CLI330 *  alive. `unref` is Node-only — Workers' `setTimeout` has no such method, hence the guard. */331function timer(fn: () => void, ms: number): ReturnType<typeof setTimeout> {332  const t = setTimeout(fn, ms);333  (t as { unref?: () => void }).unref?.();334  return t;335}336337/** The retry backoff, and deliberately NOT unref'd: a checkpoint mid-retry IS work in flight, and a338 *  process that exits during it drops text the caller is still awaiting. */339function delay(ms: number): Promise<void> {340  return new Promise((resolve) => setTimeout(resolve, ms));341}342343function errText(err: unknown): string {344  return String((err as Error)?.message ?? err);345}346347/** A per-plane stand-in for {@link RindleStreamOptions.hostId} when only the open CAS — not348 *  subscribe routing — needs a token. */349function randomOpenToken(): string {350  const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;351  return c?.randomUUID?.() ?? `open-${Math.random().toString(36).slice(2)}`;352}353354// ------------------------------------------------------------------------------- mapped-table SQL355356/** Every mapped column, defaults applied. `cancel`/`error` stay `undefined` unless named. */357export interface ResolvedStreamColumns {358  key: string;359  body: string;360  status: string;361  seq: string;362  cancel?: string;363  error?: string;364  host?: string;365  chunkKey: string;366  chunkStream: string;367  chunkSeq: string;368  chunkText: string;369}370371export function resolveStreamColumns(columns: StreamColumns | undefined): ResolvedStreamColumns {372  return {373    key: columns?.key ?? "id",374    body: columns?.body ?? "body",375    status: columns?.status ?? "status",376    seq: columns?.seq ?? "seq",377    ...(columns?.cancel !== undefined ? { cancel: columns.cancel } : {}),378    ...(columns?.error !== undefined ? { error: columns.error } : {}),379    ...(columns?.host !== undefined ? { host: columns.host } : {}),380    chunkKey: columns?.chunkKey ?? "id",381    chunkStream: columns?.chunkStream ?? "streamId",382    chunkSeq: columns?.chunkSeq ?? "seq",383    chunkText: columns?.chunkText ?? "text",384  };385}386387/** Quote an identifier for both dialects (`"x"` is standard in SQLite and Postgres alike). An388 *  embedded quote is refused rather than escaped: a mapping is app configuration, and a name that389 *  needs escaping is a mistake worth failing loudly on. */390function q(identifier: string): string {391  if (identifier.includes('"')) throw new TypeError(`invalid stream column/table name: ${JSON.stringify(identifier)}`);392  return `"${identifier}"`;393}394395/** The chunk row's deterministic id: a replayed checkpoint collides with itself and is absorbed by396 *  `ON CONFLICT DO NOTHING` — idempotency without an envelope or a dedup ledger (§3.3). */397export function streamChunkId(streamId: string, seq: number): string {398  return `${streamId}:${seq}`;399}400401/**402 * The chunk table's DDL, for the app's migration. The app owns the message table (this only states403 * the three columns the plane needs on it); the chunk table is entirely protocol-shaped, so it is404 * generated rather than hand-written.405 */406export function streamChunkTableDdl(tables: StreamTables, dialect: SqlDialect): string[] {407  const c = resolveStreamColumns(tables.columns);408  const text = dialect.name === "postgres" ? "text" : "TEXT";409  const int = dialect.name === "postgres" ? "bigint" : "INTEGER";410  return [411    `CREATE TABLE IF NOT EXISTS ${q(tables.chunks)} (` +412      `${q(c.chunkKey)} ${text} PRIMARY KEY, ` +413      `${q(c.chunkStream)} ${text} NOT NULL, ` +414      `${q(c.chunkSeq)} ${int} NOT NULL, ` +415      `${q(c.chunkText)} ${text} NOT NULL)`,416    // The read path is always "this message's chunks, in order" — and compaction deletes by stream.417    `CREATE INDEX IF NOT EXISTS ${q(`${tables.chunks}_stream_seq`)} ` +418      `ON ${q(tables.chunks)} (${q(c.chunkStream)}, ${q(c.chunkSeq)})`,419  ];420}421422/** The plane's SQL, one place, both dialects. */423class MappedTableSql {424  private readonly tables: StreamTables;425  private readonly cols: ResolvedStreamColumns;426  private readonly dialect: SqlDialect;427428  constructor(tables: StreamTables, dialect: SqlDialect) {429    this.tables = tables;430    this.cols = resolveStreamColumns(tables.columns);431    this.dialect = dialect;432  }433434  private p(i: number): string {435    return this.dialect.placeholder(i);436  }437438  /** The open probe: the row must exist and be empty. A generation is never a resume — regenerating439   *  is a NEW message id — so an already-advanced row is a bug worth refusing loudly. */440  probeOpen(): string {441    const { key, seq, status } = this.cols;442    return `SELECT ${q(seq)} AS seq, ${q(status)} AS status FROM ${q(this.tables.message)} WHERE ${q(key)} = ${this.p(1)}`;443  }444445  /** The open write. With a `host` column mapped this is the CAS half of the single-flight guard:446   *  set the status AND this producer's token only if no producer holds the row, verified by the447   *  read-back in {@link probeHost}. Without one it is the plain flip the probe already vetted. */448  markStreaming(streamId: string, hostToken: string): SqlStatement {449    const { key, status, host } = this.cols;450    if (host !== undefined) {451      return {452        sql:453          `UPDATE ${q(this.tables.message)} SET ${q(status)} = ${this.p(1)}, ${q(host)} = ${this.p(2)} ` +454          `WHERE ${q(key)} = ${this.p(3)} AND ${q(status)} <> ${this.p(4)}`,455        params: [STREAM_STATUS_STREAMING, hostToken, streamId, STREAM_STATUS_STREAMING],456      };457    }458    return {459      sql: `UPDATE ${q(this.tables.message)} SET ${q(status)} = ${this.p(1)} WHERE ${q(key)} = ${this.p(2)}`,460      params: [STREAM_STATUS_STREAMING, streamId],461    };462  }463464  /** The read-back half of the open CAS, or `undefined` when no `host` column is mapped. */465  probeHost(): string | undefined {466    const { host, key } = this.cols;467    if (host === undefined) return undefined;468    return `SELECT ${q(host)} AS host FROM ${q(this.tables.message)} WHERE ${q(key)} = ${this.p(1)}`;469  }470471  /** ONE transaction: insert the slice, then CAS the message's durable length. BOTH statements are472   *  gated on the same `seq = :from` predicate: the CAS refuses to apply twice or out of order, and473   *  the guarded insert makes a STALE RE-COVER a no-op — after a committed-but-ack-lost append, the474   *  next slice's `from` lags the row, and an unguarded insert would land an OVERLAPPING chunk row475   *  (a different deterministic id, so `ON CONFLICT` alone cannot absorb it) and corrupt the476   *  assembled text until compaction. The `ON CONFLICT` clause stays as the belt under that477   *  guard's braces. Which case actually happened is decided by the read-back ({@link probeState}),478   *  never assumed (§3.2/§3.3). */479  append(streamId: string, from: number, seq: number, text: string): SqlStatement[] {480    const c = this.cols;481    return [482      {483        sql:484          `INSERT INTO ${q(this.tables.chunks)} ` +485          `(${q(c.chunkKey)}, ${q(c.chunkStream)}, ${q(c.chunkSeq)}, ${q(c.chunkText)}) ` +486          `SELECT ${this.p(1)}, ${this.p(2)}, ${this.p(3)}, ${this.p(4)} ` +487          `WHERE EXISTS (SELECT 1 FROM ${q(this.tables.message)} ` +488          `WHERE ${q(c.key)} = ${this.p(5)} AND ${q(c.seq)} = ${this.p(6)}) ` +489          `ON CONFLICT DO NOTHING`,490        params: [streamChunkId(streamId, seq), streamId, seq, text, streamId, from],491      },492      {493        sql:494          `UPDATE ${q(this.tables.message)} SET ${q(c.seq)} = ${this.p(1)} ` +495          `WHERE ${q(c.key)} = ${this.p(2)} AND ${q(c.seq)} = ${this.p(3)}`,496        params: [seq, streamId, from],497      },498    ];499  }500501  /** Compaction (§3.4): write the whole body, seal the status, drop every chunk — atomically. Also502   *  the repair path: whatever the chunks did or did not capture, the body ends up correct. */503  compact(streamId: string, body: string, status: StreamStatus, error: string | undefined): SqlStatement[] {504    const c = this.cols;505    const sets = [`${q(c.body)} = ${this.p(1)}`, `${q(c.seq)} = ${this.p(2)}`, `${q(c.status)} = ${this.p(3)}`];506    const params: Array<string | number | null> = [body, body.length, status];507    if (c.error !== undefined) {508      sets.push(`${q(c.error)} = ${this.p(params.length + 1)}`);509      params.push(error ?? null);510    }511    params.push(streamId);512    return [513      {514        sql: `UPDATE ${q(this.tables.message)} SET ${sets.join(", ")} WHERE ${q(c.key)} = ${this.p(params.length)}`,515        params,516      },517      {518        sql: `DELETE FROM ${q(this.tables.chunks)} WHERE ${q(c.chunkStream)} = ${this.p(1)}`,519        params: [streamId],520      },521    ];522  }523524  get hasCancel(): boolean {525    return this.cols.cancel !== undefined;526  }527528  /** The post-append read-back: the row's authoritative length — what CONFIRMS an append applied529   *  (and absorbs a committed-but-ack-lost one) — plus the cancel flag when mapped (§6). */530  probeState(): string {531    const { cancel, key, seq } = this.cols;532    const picked = cancel === undefined ? `${q(seq)} AS seq` : `${q(seq)} AS seq, ${q(cancel)} AS cancel`;533    return `SELECT ${picked} FROM ${q(this.tables.message)} WHERE ${q(key)} = ${this.p(1)}`;534  }535}536537// ------------------------------------------------------------------------------- subscribers538539/** One attached reader. Frames queue; a reader that stops draining is bounded and then DROPPED with540 *  `stale` rather than allowed to pin the producer's memory — it resolves that by rejoining. */541class Subscriber {542  private queue: StreamFrame[] = [];543  private waiter?: (r: IteratorResult<StreamFrame>) => void;544  private done = false;545  private readonly cap: number;546  private readonly onDetach: (s: Subscriber) => void;547548  constructor(cap: number, onDetach: (s: Subscriber) => void) {549    this.cap = cap;550    this.onDetach = onDetach;551  }552553  /** @returns false when the queue overflowed (caller drops this subscriber). */554  offer(frame: StreamFrame): boolean {555    if (this.done) return true;556    if (this.waiter) {557      const w = this.waiter;558      this.waiter = undefined;559      w({ value: frame, done: false });560      return true;561    }562    if (this.queue.length >= this.cap) return false;563    this.queue.push(frame);564    return true;565  }566567  /** Deliver a terminal frame (bypassing the cap — it is the last one) and end the iterator. */568  finish(frame: StreamFrame): void {569    if (this.done) return;570    if (this.waiter) {571      const w = this.waiter;572      this.waiter = undefined;573      this.done = true;574      w({ value: frame, done: false });575      return;576    }577    this.queue.push(frame);578    this.done = true;579  }580581  close(): void {582    this.done = true;583    this.queue.length = 0;584    if (this.waiter) {585      const w = this.waiter;586      this.waiter = undefined;587      w({ value: undefined as never, done: true });588    }589    this.onDetach(this);590  }591592  frames(): AsyncIterable<StreamFrame> {593    const self = this;594    return {595      [Symbol.asyncIterator](): AsyncIterator<StreamFrame> {596        return {597          next(): Promise<IteratorResult<StreamFrame>> {598            const queued = self.queue.shift();599            if (queued !== undefined) return Promise.resolve({ value: queued, done: false });600            if (self.done) {601              self.onDetach(self);602              return Promise.resolve({ value: undefined as never, done: true });603            }604            return new Promise((resolve) => {605              self.waiter = resolve;606            });607          },608          return(): Promise<IteratorResult<StreamFrame>> {609            self.close();610            return Promise.resolve({ value: undefined as never, done: true });611          },612        };613      },614    };615  }616}617618// ------------------------------------------------------------------------------- the relay conform pass619620/**621 * One frame source arriving over a relay, conformed to the CP §4 contract622 * (designs-implemented/LM-STREAM-RELAY-DESIGN.md §4).623 *624 * An adapter is app code talking to Redis or a socket, and its frames feed `spliceStreamText` on a625 * browser — so the plane does not trust them. This pass enforces the frame invariants against the626 * prefix actually delivered and downgrades EVERY violation to a legal `stale` and nothing else:627 * `stale` already means "you are on the durable plane now, the store is the whole truth", so a628 * broken relay costs a reader smooth tokens, never corrupted text — and cannot wedge a producer.629 *630 * Replayed spans (a reconnecting adapter re-delivering what it already sent) are ABSORBED rather631 * than punished — deduping against the delivered prefix is what makes reconnect-replay safe without632 * every adapter hand-rolling it. Spans that overlap the prefix but extend past it pass through633 * whole: the client splices at the frame's own offset, so an exact overlap re-covers and appends.634 *635 * Pure state, no I/O, no plane: `feed` maps one incoming frame to 0-2 outgoing frames (a missing636 * `open` is synthesized at the join offset); `end`/`fail` close out a source that finished or threw637 * without a terminal. After a terminal, every method returns `[]`.638 */639export class StreamRelayConform {640  private readonly streamId: string;641  /** The requested join offset — the synthesized `open`'s position, and where the prefix starts. */642  private readonly from: number;643  private readonly onViolation: ((reason: string) => void) | undefined;644  /** End of the delivered prefix. */645  private pos: number;646  private lastDurable = 0;647  private opened = false;648  private done = false;649650  constructor(streamId: string, from: number, onViolation?: (reason: string) => void) {651    this.streamId = streamId;652    this.from = from;653    this.pos = from;654    this.onViolation = onViolation;655  }656657  feed(frame: StreamFrame): StreamFrame[] {658    if (this.done) return [];659    switch (frame.type) {660      case "open": {661        if (this.opened) return []; // a reconnecting adapter's second open: absorbed662        if (663          frame.streamId !== this.streamId ||664          !Number.isInteger(frame.from) ||665          !Number.isInteger(frame.seq) ||666          !Number.isInteger(frame.durableSeq) ||667          frame.from < 0 ||668          frame.seq < frame.from669        ) {670          return this.violate(`relay open for ${JSON.stringify(frame.streamId)} at ${frame.from} is malformed`);671        }672        // An open PAST the requested offset is the adapter saying it cannot serve `from` (pub/sub673        // has no history, §5): delivering it would leave a hole in the middle of the response, so674        // the honest answer is the durable plane.675        if (frame.from > this.from) {676          return this.violate(`relay open at ${frame.from} cannot serve the requested ${this.from}`);677        }678        this.opened = true;679        this.pos = frame.from;680        return [frame];681      }682      case "chunk": {683        if (684          !Number.isInteger(frame.from) ||685          !Number.isInteger(frame.seq) ||686          frame.from < 0 ||687          typeof frame.text !== "string" ||688          frame.text.length !== frame.seq - frame.from689        ) {690          return this.violate(`relay chunk ${frame.from}→${frame.seq} does not span exactly its offsets`);691        }692        if (frame.from > this.pos) {693          return this.violate(`relay chunk at ${frame.from} leaves a gap after ${this.pos}`);694        }695        if (frame.seq <= this.pos) return []; // entirely within the delivered prefix (a replay): absorbed696        const out = this.opened ? [] : [this.synthOpen()];697        this.pos = frame.seq;698        out.push(frame);699        return out;700      }701      case "durable": {702        // Purely informational to a reader (the hook ignores it; SSE uses it as a resume id), so a703        // claim that rewinds — or outruns what this subscription has SEEN produced, which P forbids704        // — is dropped rather than downgraded.705        if (!Number.isInteger(frame.seq) || frame.seq < this.lastDurable || frame.seq > this.pos) return [];706        const out = this.opened ? [] : [this.synthOpen()];707        this.lastDurable = frame.seq;708        out.push(frame);709        return out;710      }711      case "end": {712        if (!Number.isInteger(frame.seq) || frame.seq < 0) {713          return this.violate(`relay end at ${String(frame.seq)} is malformed`);714        }715        // An `end` whose durable length outruns the delivered prefix means the adapter LOST text716        // (durable never exceeds produced), so the reader is short and must not be told it saw717        // everything. Downgrading keeps `end` meaning the same thing relayed as local: the whole718        // produced text arrived.719        if (frame.seq > this.pos) {720          return this.violate(`relay end at ${frame.seq} outruns the ${this.pos} characters delivered`);721        }722        this.done = true;723        const out = this.opened ? [] : [this.synthOpen()];724        out.push(frame);725        return out;726      }727      case "stale":728      case "absent": {729        // Legal bare — the local plane's own floor/eviction answers carry no `open` either.730        this.done = true;731        return [frame];732      }733    }734  }735736  /** The source completed without a terminal (a truncated relay): the reader falls back. */737  end(): StreamFrame[] {738    if (this.done) return [];739    this.onViolation?.("relay source ended without a terminal frame");740    return this.terminate();741  }742743  /** The source threw mid-iteration, or the plane is dropping a reader that stopped draining:744   *  a bare `stale` at the delivered position. */745  fail(): StreamFrame[] {746    return this.done ? [] : this.terminate();747  }748749  /** A synthesized join, for an adapter that (correctly, in broadcast mode) never mirrors the750   *  per-subscriber `open`: positioned at the requested offset, which the reader asked from because751   *  its durable view already holds it. */752  private synthOpen(): StreamFrame {753    this.opened = true;754    return { type: "open", streamId: this.streamId, from: this.from, seq: this.from, durableSeq: this.from, ended: false };755  }756757  private terminate(): StreamFrame[] {758    this.done = true;759    return [{ type: "stale", floorSeq: this.pos, durableSeq: this.lastDurable }];760  }761762  private violate(reason: string): StreamFrame[] {763    this.onViolation?.(reason);764    return this.terminate();765  }766}767768// ------------------------------------------------------------------------------- the live stream769770interface Waiter {771  at: number;772  resolve: (seq: number) => void;773  reject: (err: unknown) => void;774}775776class LiveStream<User> {777  /** Retained text; `buf[0]` sits at offset {@link bufFrom}. */778  private buf = "";779  private bufFrom = 0;780  seq = 0;781  durableSeq = 0;782  ended = false;783  cancelled = false;784785  readonly streamId: string;786  readonly user: User;787  readonly meta: unknown;788  private readonly plane: StreamPlane<User>;789790  private readonly subs = new Set<Subscriber>();791  private sealing?: { status: StreamStatus; error?: string };792  private draining = false;793  private forced = false;794  /** The last drain round left a slice uncommitted: wait out the interval instead of re-attempting795   *  immediately, so a down store costs a retry cadence and not a hot loop. */796  private stalled = false;797  private tick: ReturnType<typeof setTimeout> | null = null;798  private waiters: Waiter[] = [];799  private sealed?: { resolve: () => void; reject: (e: unknown) => void; promise: Promise<void> };800  private sealError: unknown;801802  constructor(streamId: string, user: User, meta: unknown, plane: StreamPlane<User>) {803    this.streamId = streamId;804    this.user = user;805    this.meta = meta;806    this.plane = plane;807  }808809  // ---- producer side810811  push(text: string): void {812    if (this.sealing || this.ended) throw new Error(`stream ${this.streamId} is closed`);813    if (text.length === 0) return;814    const from = this.seq;815    this.buf += text;816    this.seq += text.length;817    this.fanout({ type: "chunk", from, seq: this.seq, text });818    if (this.seq - this.durableSeq >= this.plane.chars) this.kick(true);819    else this.arm();820  }821822  flush(): Promise<number> {823    const at = this.seq;824    if (this.durableSeq >= at) return Promise.resolve(this.durableSeq);825    // After a failed seal the shortfall is PERMANENT (the stream ended; nothing will retry it), so a826    // late flush rejects honestly instead of quietly re-running the seal.827    if (this.ended) {828      return Promise.reject(829        new Error(`stream ${this.streamId} ended with only ${this.durableSeq} of ${this.seq} characters durable`),830      );831    }832    const p = new Promise<number>((resolve, reject) => {833      this.waiters.push({ at, resolve, reject });834    });835    this.kick(true);836    return p;837  }838839  /** Seal the stream. Resolves once the terminal checkpoint has committed; rejects if it could not840   *  (the stream still ENDS — subscribers always get their `end` frame — the caller just learns the841   *  store is short of what was produced). */842  seal(status: StreamStatus, error?: string): Promise<void> {843    // Already sealed: hand back the SAME settled promise (already marked handled below), so a second844    // `close()`/`fail()` can neither re-seal nor mint a stray rejection.845    if (this.ended) return this.sealed?.promise ?? Promise.resolve();846    if (!this.sealing) {847      this.sealing = { status, error };848      let resolve!: () => void;849      let reject!: (e: unknown) => void;850      const promise = new Promise<void>((res, rej) => {851        resolve = res;852        reject = rej;853      });854      // A `fail()` nobody awaited must not become an unhandled rejection at process level. This855      // marks the promise handled WITHOUT consuming it — a caller that does await still sees the856      // rejection, because `await` attaches its own handler to the same promise.857      promise.catch(() => {});858      this.sealed = { resolve, reject, promise };859      this.disarm();860      this.kick(true);861    }862    return this.sealed!.promise;863  }864865  // ---- checkpoint driving866867  private arm(): void {868    if (this.tick || this.seq === this.durableSeq) return;869    this.tick = timer(() => {870      this.tick = null;871      this.kick(true);872    }, this.plane.intervalMs);873  }874875  private disarm(): void {876    if (this.tick) clearTimeout(this.tick);877    this.tick = null;878  }879880  private kick(force: boolean): void {881    if (force) this.forced = true;882    if (this.draining) return;883    this.draining = true;884    void this.drain();885  }886887  private async drain(): Promise<void> {888    this.stalled = false;889    try {890      for (;;) {891        // Sealing goes STRAIGHT to the seal: it writes the whole body (§3.4), so committing the892        // outstanding tail as one more chunk first would be pure waste — and the seal repairs any893        // earlier checkpoint that failed, which a tail commit could not.894        if (this.sealing) {895          await this.commitSeal();896          break;897        }898        const behind = this.seq - this.durableSeq;899        if (behind > 0 && (this.forced || behind >= this.plane.chars)) {900          // Consume the force BEFORE the round, not after: a `flush()` that lands while this round901          // is already in flight re-sets it and gets a round of its OWN — clearing it afterwards902          // would swallow that request and leave the flush to the interval cadence.903          this.forced = false;904          this.disarm();905          const failure = await this.commitTail();906          if (failure) {907            this.stalled = true;908            // A seal requested WHILE this checkpoint was in flight outranks the retry cadence: the909            // seal writes the whole body anyway, so it both supersedes and repairs the failed slice.910            // Backing off here instead would make `close()` wait out `intervalMs` for nothing. An911            // explicit re-force during the failed round likewise earns one immediate re-attempt —912            // someone is waiting on it — while an unforced failure stalls to the cadence.913            if (!this.sealing && !this.forced) break;914          }915          continue;916        }917        this.forced = false;918        break;919      }920    } finally {921      this.draining = false;922      if (!this.ended) {923        // A pending seal ALWAYS proceeds at once — including out of a stall (see the loop above);924        // otherwise a stall waits out the interval, and deltas that landed mid-commit may already925        // meet the threshold and deserve an immediate round.926        if (this.sealing) this.kick(false);927        else if (this.stalled) this.arm();928        else if (this.seq - this.durableSeq >= this.plane.chars) this.kick(false);929        else this.arm();930      }931    }932  }933934  /** @returns the failure, or `undefined` when the slice committed. */935  private async commitTail(): Promise<{ err: unknown } | undefined> {936    const from = this.durableSeq;937    const to = this.seq;938    const text = this.buf.slice(from - this.bufFrom, to - this.bufFrom);939    let out: { cancelRequested?: boolean; seq?: number } | void;940    try {941      out = await this.plane.commit({ kind: "append", streamId: this.streamId, from, seq: to, text });942    } catch (err) {943      // The store may hold MORE than we believe — an append that committed while its ack was lost,944      // somewhere in the retry chain. The read-back is the truth, and adopting it is what keeps the945      // NEXT slice contiguous instead of overlapping (§3.3).946      const truth = await this.plane.probeDurableSeq(this.streamId);947      if (truth !== undefined && truth > this.durableSeq) this.advanceDurable(truth);948      this.plane.reportCheckpointError(err, { streamId: this.streamId, from, seq: to });949      // Waiters that asked for THIS prefix learn it did not land; later waiters keep waiting for a950      // later attempt. A durability failure is never silent.951      this.rejectWaiters(to, err);952      return { err };953    }954    if (out?.cancelRequested) this.cancelled = true;955    // `durableSeq` advances to what the store CONFIRMED — the mapped path reads it back, a `commit`956    // callback may report it — never to a plane-side assumption. A confirmation short of `to` means957    // this slice's `from` was stale (a prior ack loss the read-back could not see at the time): the958    // guarded statements made it a no-op, and the next round re-covers from the confirmed offset.959    const confirmed = out?.seq ?? to;960    if (confirmed > this.durableSeq) this.advanceDurable(confirmed);961    if (confirmed < to) {962      const err = new Error(963        `stream ${this.streamId}: the store confirmed ${confirmed} of ${to} — re-covering from there`,964      );965      this.plane.reportCheckpointError(err, { streamId: this.streamId, from, seq: to });966      this.rejectWaiters(to, err);967      return { err };968    }969    return undefined;970  }971972  /** The one place durable progress is recorded: position, `durable` frame, buffer trim, waiters. */973  private advanceDurable(seq: number): void {974    this.durableSeq = seq;975    this.fanout({ type: "durable", seq });976    this.trim();977    this.resolveWaiters();978  }979980  private async commitSeal(): Promise<void> {981    const seal = this.sealing!;982    const from = this.durableSeq;983    try {984      await this.plane.commit({985        kind: "close",986        streamId: this.streamId,987        from,988        seq: this.seq,989        // A trimmed buffer (only reachable in `commit`-callback mode, where the app opted into990        // trimming) cannot supply the whole body: `bodyFrom` says where the retained text starts,991        // so the app can still append exactly the outstanding tail (`body.slice(from - bodyFrom)`).992        body: this.buf,993        bodyFrom: this.bufFrom,994        status: seal.status,995        ...(seal.error !== undefined ? { error: seal.error } : {}),996      });997      this.durableSeq = this.seq;998    } catch (err) {999      this.sealError = err;1000      this.plane.reportCheckpointError(err, { streamId: this.streamId, from, seq: this.seq });1001    }1002    this.ended = true;1003    this.disarm();1004    this.rejectWaiters(1005      Number.POSITIVE_INFINITY,1006      new Error(`stream ${this.streamId} ended with only ${this.durableSeq} of ${this.seq} characters durable`),1007    );1008    this.resolveWaiters();1009    const frame: StreamFrame = {1010      type: "end",1011      seq: this.durableSeq,1012      status: seal.status,1013      ...(seal.error !== undefined ? { error: seal.error } : {}),1014    };1015    // The terminal reaches the relay too (it bypasses `fanout` locally only to bypass the cap).1016    this.plane.publishRelay(this.streamId, frame);1017    for (const sub of [...this.subs]) sub.finish(frame);1018    this.plane.retire(this.streamId);1019    if (this.sealError) this.sealed?.reject(this.sealError);1020    else this.sealed?.resolve();1021  }10221023  /** Keep everything at or above `durableSeq`, plus a slack window below it so a client whose IVM1024   *  view lags one checkpoint can still join without a `stale` round trip (§4). A `retainChars` of1025   *  `Infinity` (the mapped-table default — compaction needs the whole text) never trims. */1026  private trim(): void {1027    if (!Number.isFinite(this.plane.retainChars)) return;1028    const floor = Math.max(0, this.durableSeq - this.plane.retainChars);1029    if (floor <= this.bufFrom) return;1030    this.buf = this.buf.slice(floor - this.bufFrom);1031    this.bufFrom = floor;1032  }10331034  private resolveWaiters(): void {1035    if (this.waiters.length === 0) return;1036    const still: Waiter[] = [];1037    for (const w of this.waiters) {1038      if (w.at <= this.durableSeq) w.resolve(this.durableSeq);1039      else still.push(w);1040    }1041    this.waiters = still;1042  }10431044  private rejectWaiters(upTo: number, err: unknown): void {1045    if (this.waiters.length === 0) return;1046    const still: Waiter[] = [];1047    for (const w of this.waiters) {1048      if (w.at <= upTo && w.at > this.durableSeq) w.reject(err);1049      else still.push(w);1050    }1051    this.waiters = still;1052  }10531054  // ---- subscriber side10551056  private fanout(frame: StreamFrame): void {1057    // Every frame local subscribers get, the relay gets — including when nobody local is attached1058    // (a broadcast relay's whole point). Wrapped so an outage costs the live leg, never this stream.1059    this.plane.publishRelay(this.streamId, frame);1060    for (const sub of [...this.subs]) {1061      if (!sub.offer(frame)) {1062        // Bounded, then dropped: a reader that stopped draining costs itself a rejoin, never the1063        // producer's memory.1064        sub.finish({ type: "stale", floorSeq: this.bufFrom, durableSeq: this.durableSeq });1065        this.subs.delete(sub);1066      }1067    }1068  }10691070  /** Attach a reader at `from`. Runs in ONE synchronous block — snapshot, replay, register — so no1071   *  delta can slip between the replay and the live tail (the gap-free half of contract S). */1072  subscribe(from: number): StreamSubscription {1073    const sub = new Subscriber(this.plane.maxQueuedFrames, (s) => this.subs.delete(s));1074    // Floored as well as clamped: a fractional `from` (a hand-built subscribe input) would otherwise1075    // produce a replay chunk whose `text.length !== seq - from`, breaking the frame invariant.1076    const at = Math.min(Math.max(Math.floor(from), 0), this.seq);1077    if (at < this.bufFrom) {1078      sub.finish({ type: "stale", floorSeq: this.bufFrom, durableSeq: this.durableSeq });1079      return this.wrap(sub);1080    }1081    sub.offer({ type: "open", streamId: this.streamId, from: at, seq: this.seq, durableSeq: this.durableSeq, ended: this.ended });1082    if (this.seq > at) {1083      sub.offer({ type: "chunk", from: at, seq: this.seq, text: this.buf.slice(at - this.bufFrom) });1084    }1085    if (this.durableSeq > at) sub.offer({ type: "durable", seq: this.durableSeq });1086    if (this.ended) {1087      const seal = this.sealing ?? { status: "interrupted" as StreamStatus };1088      sub.finish({1089        type: "end",1090        seq: this.durableSeq,1091        status: seal.status,1092        ...(seal.error !== undefined ? { error: seal.error } : {}),1093      });1094      return this.wrap(sub);1095    }1096    this.subs.add(sub);1097    return this.wrap(sub);1098  }10991100  private wrap(sub: Subscriber): StreamSubscription {1101    return { streamId: this.streamId, frames: sub.frames(), close: () => sub.close() };1102  }11031104  /** Drop every reader without sealing (process teardown — `drainStreams` seals first). */1105  detachAll(): void {1106    for (const sub of [...this.subs]) sub.close();1107    this.subs.clear();1108    this.disarm();1109  }1110}11111112// ------------------------------------------------------------------------------- the plane11131114/** What the plane needs from the api-server to write a checkpoint: the backend's OUTSIDE-transaction1115 *  SQL surface (`batch` is one transaction on every backend) and its dialect. Deliberately narrow so1116 *  `streams.ts` never imports the server (no cycle). */1117export interface StreamSqlSink {1118  readonly dialect: SqlDialect;1119  readonly sql: ServerSql;1120}11211122export class StreamPlane<User> {1123  readonly chars: number;1124  readonly intervalMs: number;1125  readonly retries: number;1126  readonly retainChars: number;1127  readonly lingerMs: number;1128  readonly maxQueuedFrames: number;1129  readonly relayAttachTimeoutMs: number;11301131  private readonly live = new Map<string, LiveStream<User>>();1132  /** Live relayed subscriptions, so teardown ({@link closeSync}) releases their drivers too. */1133  private readonly relayed = new Set<Subscriber>();1134  private readonly opts: RindleStreamOptions<User>;1135  private readonly sink: StreamSqlSink | undefined;1136  private readonly mapped: MappedTableSql | undefined;1137  private readonly tables: StreamTables | undefined;1138  /** The open CAS token (§5.1): `hostId`, or a random per-plane stand-in when only the CAS — not1139   *  routing — needs it. */1140  private readonly openToken: string;11411142  constructor(opts: RindleStreamOptions<User>, sink?: StreamSqlSink) {1143    this.opts = opts;1144    this.sink = sink;1145    this.chars = opts.policy?.chars ?? DEFAULT_CHECKPOINT_CHARS;1146    this.intervalMs = opts.policy?.intervalMs ?? DEFAULT_CHECKPOINT_INTERVAL_MS;1147    this.retries = opts.policy?.retries ?? DEFAULT_CHECKPOINT_RETRIES;1148    this.lingerMs = opts.lingerMs ?? DEFAULT_LINGER_MS;1149    this.maxQueuedFrames = opts.maxQueuedFrames ?? DEFAULT_MAX_QUEUED_FRAMES;1150    this.relayAttachTimeoutMs = opts.relayAttachTimeoutMs ?? DEFAULT_RELAY_ATTACH_TIMEOUT_MS;1151    this.openToken = opts.hostId ?? randomOpenToken();1152    // Relay misconfiguration is refused loudly (the room-profile rule), never ignored.1153    if (opts.relay !== undefined && opts.relay.publish === undefined && opts.relay.attach === undefined) {1154      throw new TypeError("streams.relay implements neither publish nor attach — which topology is this? (LM-STREAM-RELAY §3.1)");1155    }1156    if (opts.relay === undefined && (opts.relayAttachTimeoutMs !== undefined || opts.onRelayError !== undefined)) {1157      throw new TypeError("streams.relayAttachTimeoutMs/onRelayError do nothing without streams.relay");1158    }1159    if ("tables" in opts.checkpoint) {1160      if (!sink) throw new TypeError("streams.checkpoint.tables needs a SQL-capable mutation backend");1161      this.tables = opts.checkpoint.tables;1162      this.mapped = new MappedTableSql(opts.checkpoint.tables, sink.dialect);1163      // Options this mode cannot honour are refused loudly (the room-profile rule), never ignored.1164      if (opts.retainChars !== undefined) {1165        throw new TypeError(1166          "streams.retainChars is `commit`-mode only — mapped tables retain the whole text for compaction (§3.4)",1167        );1168      }1169      if (opts.hostId !== undefined && resolveStreamColumns(opts.checkpoint.tables.columns).host === undefined) {1170        throw new TypeError("streams.hostId does nothing in tables mode until columns.host names where to persist it");1171      }1172      // Compaction writes the WHOLE body at close, so the buffer is retained in full. `retainChars`1173      // is honoured only where the app owns persistence and may not need the tail.1174      this.retainChars = Number.POSITIVE_INFINITY;1175    } else {1176      this.retainChars = opts.retainChars ?? DEFAULT_RETAIN_CHARS;1177    }1178  }11791180  /** Open a stream on an EXISTING message row (§5): the app's own mutator wrote it, alongside the1181   *  user's prompt, so the pointer is already durable and every client's query already shows the1182   *  message. This verifies it and flips it to `streaming`. */1183  async open(input: OpenStreamInput<User>): Promise<StreamHandle> {1184    if (this.live.has(input.streamId)) {1185      throw new Error(`stream ${input.streamId} is already open on this host`);1186    }1187    const stream = new LiveStream<User>(input.streamId, input.user, input.meta, this);1188    this.live.set(input.streamId, stream);1189    try {1190      if (this.mapped) await this.assertOpenable(input.streamId);1191      await this.commit({1192        kind: "open",1193        streamId: input.streamId,1194        meta: input.meta ?? null,1195        ...(this.opts.hostId !== undefined ? { hostId: this.opts.hostId } : {}),1196        startedAt: Date.now(),1197      });1198    } catch (err) {1199      this.live.delete(input.streamId);1200      throw err;1201    }1202    return {1203      streamId: stream.streamId,1204      get seq() {1205        return stream.seq;1206      },1207      get durableSeq() {1208        return stream.durableSeq;1209      },1210      get cancelled() {1211        return stream.cancelled;1212      },1213      push: (text) => stream.push(text),1214      flush: () => stream.flush(),1215      pump: async (deltas) => {1216        // `for await` closes the iterator on `break`, which is what aborts the SDK's underlying1217        // request — the whole point of honouring cancellation here rather than in the caller.1218        for await (const d of deltas) {1219          stream.push(d);1220          if (stream.cancelled) break;1221        }1222      },1223      close: () => stream.seal(stream.cancelled ? "cancelled" : "complete"),1224      fail: (error) => stream.seal("error", errText(error)),1225    };1226  }12271228  /**1229   * The read-only precondition on the app's message row, checked ONCE per `open` (§5.1). Read-only on1230   * purpose: it is a decision about the app's data, so re-deciding it per write attempt would let a1231   * lost ack turn a success into a refusal.1232   *1233   * The `streaming` check is the **single-flight guard**, and it lives at the ROW rather than in this1234   * process's map because the thing it defends against is distributed: the kick that starts a1235   * generation is an at-least-once effect (a retried mutation envelope re-runs its post-commit code,1236   * §10.5), so a second kick can land on another instance, where the in-memory map is empty. Two1237   * producers on one `streamId` would interleave: both CAS the same length, one stalls, and whichever1238   * closes last overwrites the body with ITS buffer. Cheap to refuse; expensive to debug.1239   *1240   * This probe alone is check-then-act — it and the open write are separate round trips, so two1241   * SIMULTANEOUS kicks can both pass it. Mapping a `host` column closes that window: the open write1242   * turns conditional and {@link verifyOpenWinner}'s read-back names the winner.1243   */1244  private async assertOpenable(streamId: string): Promise<void> {1245    const cols = resolveStreamColumns(this.tables!.columns);1246    const rows = await this.sink!.sql.query<{ seq: number | null; status: string | null }>(1247      this.mapped!.probeOpen(),1248      [streamId],1249    );1250    const row = rows[0];1251    if (!row) {1252      throw new StreamOpenRefused(1253        `stream ${streamId}: no row in "${this.tables!.message}" — the app's own mutator must write the message ` +1254          `(with its chatId/role/…) BEFORE opening the stream on it`,1255      );1256    }1257    // A NULL length is refused as loudly as an advanced one, and for a subtler reason: it is the CAS1258    // column, and `WHERE seq = 0` can never match NULL. A nullable column would let every chunk1259    // insert land while its length CAS silently matched nothing — the one failure mode this plane1260    // cannot detect (`batch` reports no row count) and one compaction would paper over (§5.1).1261    if (typeof row.seq !== "number") {1262      throw new StreamOpenRefused(1263        `stream ${streamId}: "${this.tables!.message}"."${cols.seq}" is ${row.seq === null ? "NULL" : typeof row.seq} — ` +1264          `the length column must be NOT NULL DEFAULT 0 (it is compare-and-swapped on every checkpoint, and no ` +1265          `comparison matches NULL)`,1266      );1267    }1268    if (row.seq !== 0) {1269      throw new StreamOpenRefused(1270        `stream ${streamId}: the message row already holds ${row.seq} characters — a regeneration is a NEW message ` +1271          `id, never a resume of an advanced row`,1272      );1273    }1274    if (row.status === STREAM_STATUS_STREAMING) {1275      throw new StreamOpenRefused(1276        `stream ${streamId}: "${cols.status}" is already "${STREAM_STATUS_STREAMING}" — another producer holds this ` +1277          `stream. Two producers on one stream interleave their checkpoints; if the first one's host died, the ` +1278          `sweeper marking it "interrupted" is what releases the row (§7)`,1279      );1280    }1281  }12821283  async subscribe(input: SubscribeStreamInput<User>): Promise<StreamSubscription> {1284    const stream = this.live.get(input.streamId);1285    const from = input.from ?? 0;1286    // Authorize BEFORE existence is consulted: a denial must not double as an existence oracle.1287    const verdict = await this.opts.authorize({1288      user: input.user,1289      streamId: input.streamId,1290      from,1291      meta: stream === undefined ? undefined : stream.meta,1292      request: input.request,1293    });1294    if (verdict === false) throw new StreamForbidden(input.streamId);1295    if (!stream) {1296      // The relay is consulted only on a live-plane miss, and only after `authorize` passed — a1297      // denial must not become an existence probe against the relay either. A local stream always1298      // wins: the producer's own readers keep the lowest-latency path.1299      const relayed = await this.attachRelay(input.streamId, from);1300      return relayed ?? oneFrame(input.streamId, { type: "absent" });1301    }1302    return stream.subscribe(from);1303  }13041305  /** The subscribe-miss leg (LM-STREAM-RELAY §3): ask the app's relay for the frames of a stream1306   *  this process is not hosting. `undefined` — no relay, no `attach`, the adapter declined, timed1307   *  out, or threw — is `absent`, exactly today's answer. */1308  private async attachRelay(streamId: string, from: number): Promise<StreamSubscription | undefined> {1309    const relay = this.opts.relay;1310    if (relay?.attach === undefined) return undefined;1311    // Floored like the local join, but clamped only below: the producer's length is not known here.1312    const at = Number.isFinite(from) ? Math.max(Math.floor(from), 0) : 0;1313    let source: AsyncIterable<StreamFrame> | undefined;1314    try {1315      source = await this.boundedAttach(relay, streamId, at);1316    } catch (err) {1317      this.reportRelayError(err, { streamId, phase: "attach" });1318      return undefined;1319    }1320    if (source === undefined) return undefined;1321    return this.relaySubscription(streamId, at, source);1322  }13231324  /** `attach`, bounded by {@link RindleStreamOptions.relayAttachTimeoutMs}: a hung adapter yields1325   *  `absent`, not a hung HTTP request. A source that resolves after the deadline is closed, not1326   *  leaked. */1327  private boundedAttach(1328    relay: StreamRelay,1329    streamId: string,1330    from: number,1331  ): Promise<AsyncIterable<StreamFrame> | undefined> {1332    // `async` wrapping so a synchronously-throwing adapter is an attach failure, not a plane throw.1333    const attempt = (async () => relay.attach!(streamId, from))();1334    return new Promise((resolve, reject) => {1335      let late = false;1336      const t = timer(() => {1337        late = true;1338        reject(new Error(`stream ${streamId}: relay.attach timed out after ${this.relayAttachTimeoutMs}ms`));1339      }, this.relayAttachTimeoutMs);1340      attempt.then(1341        (source) => {1342          clearTimeout(t);1343          if (!late) return resolve(source);1344          closeFrameSource(source); // too late to serve the reader; don't leak the channel1345        },1346        (err) => {1347          clearTimeout(t);1348          if (!late) reject(err);1349        },1350      );1351    });1352  }13531354  /** Wrap an adapter's frame source as a plane subscription: conform every frame (LM-STREAM-RELAY1355   *  §4), bound the reader with the same queue cap as a local one (§7), and tear the adapter down1356   *  when either side lets go. The driver never throws into the plane: adapter failures become one1357   *  `stale`. */1358  private relaySubscription(streamId: string, from: number, source: AsyncIterable<StreamFrame>): StreamSubscription {1359    const conform = new StreamRelayConform(streamId, from, (reason) =>1360      this.reportRelayError(new Error(reason), { streamId, phase: "frames" }),1361    );1362    let closed = false;1363    let signalClose!: () => void;1364    const closedP = new Promise<void>((resolve) => (signalClose = resolve));1365    const closedTag = closedP.then(() => "closed" as const);1366    const release = (): void => {1367      if (!closed) {1368        closed = true;1369        signalClose();1370      }1371    };1372    const sub = new Subscriber(this.maxQueuedFrames, (s) => {1373      this.relayed.delete(s);1374      release();1375    });1376    this.relayed.add(sub);1377    let it: AsyncIterator<StreamFrame> | undefined;1378    /** @returns false once the subscription finished (terminal delivered, or the reader dropped). */1379    const deliver = (frames: StreamFrame[]): boolean => {1380      for (const frame of frames) {1381        if (frame.type === "end" || frame.type === "stale" || frame.type === "absent") {1382          sub.finish(frame);1383          return false;1384        }1385        if (!sub.offer(frame)) {1386          // The same bound as a local reader: a relayed subscriber that stops draining costs1387          // itself the live leg, never unbounded memory.1388          const [stale] = conform.fail();1389          if (stale) sub.finish(stale);1390          return false;1391        }1392      }1393      return true;1394    };1395    void (async () => {1396      try {1397        // Iterator construction is adapter code too: keep a throwing factory inside the same1398        // stale/report/cleanup boundary as a throwing `next()`.1399        it = source[Symbol.asyncIterator]();1400        for (;;) {1401          // Raced rather than awaited bare: a reader disconnect must release this driver even when1402          // the adapter never yields another frame (its own `return()` may be queued behind the1403          // pending `next()` forever).1404          const res = await Promise.race([it.next(), closedTag]);1405          if (res === "closed") return;1406          if (res.done) {1407            deliver(conform.end());1408            return;1409          }1410          if (!deliver(conform.feed(res.value))) return;1411        }1412      } catch (err) {1413        this.reportRelayError(err, { streamId, phase: "frames" });1414        deliver(conform.fail());1415      } finally {1416        release();1417        // If construction itself threw there is no iterator to return, and asking the source to1418        // construct a second one during cleanup could repeat side effects or throw again.1419        closeFrameSource(undefined, it);1420      }1421    })();1422    return { streamId, frames: sub.frames(), close: () => sub.close() };1423  }14241425  /** Mirror one producer frame outward (LM-STREAM-RELAY §3). Never blocks or breaks the producer:1426   *  a throw or rejected promise is reported and swallowed — a relay outage may cost relayed1427   *  readers the live leg, never the generation or its checkpoints. */1428  publishRelay(streamId: string, frame: StreamFrame): void {1429    const relay = this.opts.relay;1430    if (relay?.publish === undefined) return;1431    try {1432      const published = relay.publish(streamId, frame);1433      if (published !== undefined) {1434        void Promise.resolve(published).catch((err) =>1435          this.reportRelayError(err, { streamId, phase: "publish" }),1436        );1437      }1438    } catch (err) {1439      this.reportRelayError(err, { streamId, phase: "publish" });1440    }1441  }14421443  reportRelayError(err: unknown, info: StreamRelayErrorInfo): void {1444    // Same discipline as reportCheckpointError: a diagnostic must never take its caller down.1445    try {1446      if (this.opts.onRelayError) this.opts.onRelayError(err, info);1447      else console.error(`[rindle api-server] stream ${info.streamId}: relay ${info.phase} failed:`, err);1448    } catch (hookErr) {1449      console.error(`[rindle api-server] stream ${info.streamId}: onRelayError itself threw:`, hookErr);1450    }1451  }14521453  /** Seal every live stream `interrupted`. In mapped-table mode the seal IS the compaction, so a1454   *  graceful drain loses nothing PRODUCED; the status still says the response was cut short rather1455   *  than claiming completion (§5). Wire it to SIGTERM. */1456  async drainStreams(): Promise<void> {1457    await Promise.allSettled([...this.live.values()].map((s) => s.seal("interrupted")));1458  }14591460  /** Teardown: drop readers and timers WITHOUT a durable write (that is `drainStreams`). */1461  closeSync(): void {1462    for (const s of this.live.values()) s.detachAll();1463    this.live.clear();1464    for (const s of [...this.relayed]) s.close();1465    this.relayed.clear();1466  }14671468  /** A sealed stream stays joinable for the linger window, so a subscribe that races the last token1469   *  still gets `end` (and the tail it missed) rather than a bare `absent`. */1470  retire(streamId: string): void {1471    timer(() => {1472      const entry = this.live.get(streamId);1473      if (entry?.ended) {1474        this.live.delete(streamId);1475        entry.detachAll();1476      }1477    }, this.lingerMs);1478  }14791480  reportCheckpointError(err: unknown, info: { streamId: string; from: number; seq: number }): void {1481    // Reporting runs INSIDE the drain loop, so a throwing hook would take the loop down with it and1482    // wedge the stream — the one failure mode a diagnostic must never cause.1483    try {1484      if (this.opts.onCheckpointError) this.opts.onCheckpointError(err, info);1485      else console.error(`[rindle api-server] stream ${info.streamId}: checkpoint ${info.from}→${info.seq} failed:`, err);1486    } catch (hookErr) {1487      console.error(`[rindle api-server] stream ${info.streamId}: onCheckpointError itself threw:`, hookErr);1488    }1489  }14901491  /** Drive ONE checkpoint, retrying on failure. Every statement it emits is idempotent under replay1492   *  — the chunk insert dedups on its deterministic id, the length CAS refuses to apply twice, the1493   *  compaction is a whole-row overwrite — so "retry until it sticks" needs no dedup ledger and no1494   *  `lmid` (§3.3). */1495  async commit(input: StreamCommitInput): Promise<{ cancelRequested?: boolean; seq?: number } | void> {1496    let lastErr: unknown;1497    for (let attempt = 0; attempt <= this.retries; attempt++) {1498      if (attempt > 0) await delay(RETRY_BACKOFF_MS * 2 ** (attempt - 1));1499      try {1500        return await this.commitOnce(input);1501      } catch (err) {1502        if (err instanceof StreamOpenRefused) throw err; // a settled verdict, not a blip1503        lastErr = err;1504      }1505    }1506    throw lastErr;1507  }15081509  private async commitOnce(input: StreamCommitInput): Promise<{ cancelRequested?: boolean; seq?: number } | void> {1510    const target = this.opts.checkpoint;1511    if ("commit" in target) return (await target.commit(input)) ?? undefined;1512    const sql = this.sink!.sql;1513    const mapped = this.mapped!;1514    switch (input.kind) {1515      case "open": {1516        // The PRECONDITION is checked once, in `assertOpenable`, before this write — never here.1517        // Re-reading it per attempt would make a lost ack on the write below refuse its own success1518        // (the row would already say `streaming`).1519        await sql.batch([mapped.markStreaming(input.streamId, this.openToken)]);1520        await this.verifyOpenWinner(input.streamId);1521        return undefined;1522      }1523      case "append": {1524        try {1525          await sql.batch(mapped.append(input.streamId, input.from, input.seq, input.text));1526        } catch (err) {1527          // The batch may have COMMITTED with its ack lost. The read-back is the truth: a row at1528          // (or past) this slice's end means it landed, and the throw was only the reply.1529          const truth = await this.readAppendState(input.streamId).catch(() => undefined);1530          if (truth === undefined || truth.seq < input.seq) throw err;1531          return truth;1532        }1533        // The read-back CONFIRMS the append (the guarded statements report no row count). If it1534        // throws, the retry is safe: a replay of an applied slice no-ops on the guard and confirms1535        // on its own read-back.1536        return await this.readAppendState(input.streamId);1537      }1538      case "close": {1539        await sql.batch(mapped.compact(input.streamId, input.body, input.status, input.error));1540        return undefined;1541      }1542    }1543  }15441545  /** The read-back half of the open CAS, run only when a `host` column is mapped. The probe in1546   *  `assertOpenable` and the open write are separate round trips, so bare check-then-act leaves a1547   *  window where two simultaneous kicks both pass the probe; the conditional `markStreaming`1548   *  matches nothing when a rival got there first, and whichever token the row now holds names the1549   *  winner — a true CAS with no row counts needed. A replayed open (lost ack) reads back its OWN1550   *  token and proceeds. */1551  private async verifyOpenWinner(streamId: string): Promise<void> {1552    const probe = this.mapped!.probeHost();1553    if (probe === undefined) return;1554    const rows = await this.sink!.sql.query<{ host: unknown }>(probe, [streamId]);1555    if (rows[0]?.host !== this.openToken) {1556      throw new StreamOpenRefused(1557        `stream ${streamId}: another producer won the open race (the row's host is ` +1558          `${JSON.stringify(rows[0]?.host ?? null)}) — two producers on one stream interleave their checkpoints, ` +1559          `so the loser stands down`,1560      );1561    }1562  }15631564  /** The post-append read-back (§3.2): the row's authoritative length — which both CONFIRMS an1565   *  append (the guarded batch reports no row count) and absorbs a committed-but-ack-lost one —1566   *  plus the reader's cancel flag when mapped (§6), riding the same indexed point read. This read1567   *  is LOAD-BEARING: a failure here fails the append attempt (retried, then stalled), because1568   *  claiming durability the store did not confirm is the one dishonesty this plane refuses. */1569  private async readAppendState(streamId: string): Promise<{ seq: number; cancelRequested?: boolean }> {1570    const rows = await this.sink!.sql.query<{ seq: unknown; cancel?: unknown }>(this.mapped!.probeState(), [1571      streamId,1572    ]);1573    const row = rows[0];1574    if (row === undefined || typeof row.seq !== "number") {1575      throw new Error(`stream ${streamId}: the read-back found no usable message row`);1576    }1577    return { seq: row.seq, ...(this.mapped!.hasCancel ? { cancelRequested: Boolean(row.cancel) } : {}) };1578  }15791580  /** The store's word on how much is durable — for resynchronizing after a FAILED append, where a1581   *  lost ack may have left the store ahead of the plane. `undefined` in `commit` mode (no readable1582   *  authority) or when the read itself fails (the next attempt retries the resync too). */1583  async probeDurableSeq(streamId: string): Promise<number | undefined> {1584    if (!this.mapped) return undefined;1585    try {1586      return (await this.readAppendState(streamId)).seq;1587    } catch {1588      return undefined;1589    }1590  }1591}15921593/** The open probe's verdict: the message row is missing or already advanced. Not retried — it is a1594 *  settled statement about the app's data, and retrying re-asks a question already answered. */1595export class StreamOpenRefused extends Error {1596  constructor(message: string) {1597    super(message);1598    this.name = "StreamOpenRefused";1599  }1600}16011602/** Refused by {@link RindleStreamOptions.authorize}. Distinct from the api-server's own1603 *  `RindleApiError` so this module stays importable without the server (no cycle); the server1604 *  translates it to a 403 at the handler seam. */1605export class StreamForbidden extends Error {1606  readonly streamId: string;16071608  constructor(streamId: string) {1609    super(`stream ${streamId}: forbidden`);1610    this.name = "StreamForbidden";1611    this.streamId = streamId;1612  }1613}16141615/** Best-effort adapter teardown (the `for await` discipline, by hand): never awaited into the1616 *  plane — a hung `return()` must not hold anything — and a synchronously-throwing one is the1617 *  adapter's bug, not the plane's problem. */1618function closeFrameSource(source: AsyncIterable<StreamFrame> | undefined, it?: AsyncIterator<StreamFrame>): void {1619  try {1620    const iter = it ?? source?.[Symbol.asyncIterator]();1621    void Promise.resolve(iter?.return?.()).catch(() => {});1622  } catch {1623    // ignored — see above1624  }1625}16261627function oneFrame(streamId: string, frame: StreamFrame): StreamSubscription {1628  let taken = false;1629  return {1630    streamId,1631    frames: {1632      [Symbol.asyncIterator](): AsyncIterator<StreamFrame> {1633        return {1634          next(): Promise<IteratorResult<StreamFrame>> {1635            if (taken) return Promise.resolve({ value: undefined as never, done: true });1636            taken = true;1637            return Promise.resolve({ value: frame, done: false });1638          },1639        };1640      },1641    },1642    close: () => {1643      taken = true;1644    },1645  };1646}16471648// ------------------------------------------------------------------------------- SSE transport16491650/** Headers for the SSE response. `x-accel-buffering` is the nginx-family opt-out — without it a1651 *  buffering proxy holds the tokens and hands the user a paragraph at a time. */1652export const STREAM_SSE_HEADERS: Record<string, string> = {1653  "content-type": "text/event-stream; charset=utf-8",1654  "cache-control": "no-cache, no-transform",1655  connection: "keep-alive",1656  "x-accel-buffering": "no",1657};16581659/** Pull a subscribe request out of a fetch-style GET: `?streamId=…&from=…`, with `Last-Event-ID`1660 *  winning over an explicit `from` (a reconnecting `EventSource` knows better than its own URL —1661 *  the URL is the ORIGINAL join point, the header is where it actually got to). */1662export function streamRequestFromHttp(req: {1663  url: string;1664  headers: { get(name: string): string | null };1665}): { streamId: string; from: number } {1666  const url = new URL(req.url);1667  const streamId = url.searchParams.get("streamId") ?? "";1668  const lastEventId = req.headers.get("last-event-id");1669  const raw = lastEventId ?? url.searchParams.get("from") ?? "0";1670  const from = Number.parseInt(raw, 10);1671  return { streamId, from: Number.isFinite(from) && from > 0 ? from : 0 };1672}16731674/**1675 * Encode a subscription as an SSE body. Each positional frame carries `id: <seq>`, so a browser1676 * `EventSource` that drops the connection resumes at exactly the right offset with no application1677 * code — its own `Last-Event-ID` header is the `from` of the next subscribe ({@link1678 * streamRequestFromHttp}).1679 *1680 * The reader must close the `EventSource` on the `end` frame: `EventSource` reconnects on ANY close,1681 * including a clean one.1682 */1683export function streamFramesToSse(1684  sub: StreamSubscription,1685  opts?: { keepAliveMs?: number },1686): ReadableStream<Uint8Array> {1687  const encoder = new TextEncoder();1688  const keepAliveMs = opts?.keepAliveMs ?? 15_000;1689  let ping: ReturnType<typeof setTimeout> | null = null;1690  let cancelled = false;1691  let iter: AsyncIterator<StreamFrame>;1692  // PULL-based on purpose: a frame is taken from the subscription only when the consumer has1693  // demand, so a slow HTTP client backs frames up in the SUBSCRIBER's bounded queue — where1694  // `maxQueuedFrames` drops it with `stale` (§4) — instead of unboundedly in this stream's own.1695  return new ReadableStream<Uint8Array>({1696    start(controller) {1697      iter = sub.frames[Symbol.asyncIterator]();1698      const beat = (): void => {1699        if (cancelled) return;1700        // A comment line: keeps intermediaries from reaping an idle connection, costs 8 bytes.1701        controller.enqueue(encoder.encode(": ping\n\n"));1702        ping = timer(beat, keepAliveMs);1703      };1704      ping = timer(beat, keepAliveMs);1705    },1706    async pull(controller) {1707      const next = await iter.next();1708      if (cancelled) return;1709      if (next.done) {1710        if (ping) clearTimeout(ping);1711        sub.close();1712        controller.close();1713        return;1714      }1715      const id = frameResumePoint(next.value);1716      const head = id === undefined ? "" : `id: ${id}\n`;1717      controller.enqueue(encoder.encode(`${head}data: ${JSON.stringify(next.value)}\n\n`));1718    },1719    cancel() {1720      cancelled = true;1721      if (ping) clearTimeout(ping);1722      sub.close();1723    },1724  });1725}1726