API index and search · Build metadata
Supporting declarations
packages/api-server/src/streams.ts. These declarations explain referenced types. Only package-page symbols are package exports.
StreamCommitInput
/** What a checkpoint hands the durable plane when the app supplies its own {@link StreamCommit}. */
export type StreamCommitInput =
/** The pointer is marked live. The app's own mutator created the row (it owns `chatId`, `role`,
* the model name…); this only flips it to `streaming`. */
{
kind: "open";
streamId: string;
meta: unknown;
hostId?: string;
startedAt: number;
}
/** A prefix advance carrying ONLY its own slice. `text.length === seq - from`, and `from` is the
* last append the PLANE saw confirmed — so appends are contiguous in the fault-free run, but an
* append that committed while its ack was lost makes the next one RE-COVER (its `from` lags what
* the app already applied). See {@link StreamCommit} for the two ways to stay idempotent. */
| {
kind: "append";
streamId: string;
from: number;
seq: number;
text: string;
}
/** The seal. `body` is the producer's retained text and `bodyFrom` its absolute start offset:
* `bodyFrom === 0` — and `body` is the WHOLE response — unless the app opted into trimming via
* `retainChars` (`commit` mode only). A compacting app requires `bodyFrom === 0` and writes
* `body` wholesale; a non-compacting app appends the outstanding tail —
* `body.slice(from - bodyFrom)` — as a final chunk. `seq === bodyFrom + body.length`, always. */
| {
kind: "close";
streamId: string;
from: number;
seq: number;
body: string;
bodyFrom: number;
status: StreamStatus;
error?: string;
};StreamCommit
/** The escape hatch: persist the checkpoint however the app likes. Retried on throw (§3.3), so it
* must be idempotent under a repeated `(from, seq)` — AND under a RE-COVER: an append whose write
* committed but whose ack was lost leaves the plane believing less than the store holds, so the
* next append's `(from, seq)` can OVERLAP text already applied. Apply only the unseen suffix
* (`text.slice(applied - from)` when `applied > from`), or better, return `{seq: applied}` — the
* authoritative applied length — and the plane resynchronizes instead of re-covering at all.
* Return `{cancelRequested: true}` to tell the producer the reader asked it to stop (§6). */
export type StreamCommit = (input: StreamCommitInput) => Promise<void | {
cancelRequested?: boolean;
seq?: number;
}>;StreamColumns
/** Which columns of the app's own tables the plane reads and writes. Every entry has a default
* except `cancel`, `error`, and `host`, which are opt-in BY NAMING: the plane never emits SQL
* against a column the app did not ask it to use. */
export interface StreamColumns {
/** The message row's primary key, matched against `streamId`. Default `id`. */
key?: string;
/** The compacted response text. Default `body`. */
body?: string;
/** {@link STREAM_STATUS_STREAMING} then a {@link StreamStatus}. Default `status`. */
status?: string;
/** Total durable length — `length(body) + Σ chunk lengths`. The CAS column (§3.2). Default `seq`. */
seq?: string;
/** Opt-in (§6): a truthy value here stops the generation at the next checkpoint. No default —
* naming it is what turns cancellation on. */
cancel?: string;
/** Opt-in: where a failed generation's message is recorded. No default. */
error?: string;
/** Opt-in: where the open write records this producer's identity ({@link RindleStreamOptions.hostId}).
* Naming it upgrades the row-level single-flight guard from check-then-act to a true
* compare-and-swap — the open write turns conditional and a read-back names the winner (§5.1) —
* and gives multi-instance subscribe routing a column to read (§4). No default. */
host?: string;
/** Chunk primary key; the plane writes the deterministic `"<streamId>:<seq>"`. Default `id`. */
chunkKey?: string;
/** Chunk → message reference. Default `streamId`. */
chunkStream?: string;
/** The chunk's END offset — the ordering key. Default `seq`. */
chunkSeq?: string;
/** The chunk's slice of the response. Default `text`. */
chunkText?: string;
}StreamTables
/**
* The app's tables (§5). The app authors and migrates BOTH — the message row is unambiguously
* app-owned (it has `chatId`, `role`, token counts) and the chunk row must be reachable from the
* app's own query as a `related` subquery, which a Rindle system table would make awkward. The plane
* only needs to be told where things live. Use {@link streamChunkTableDdl} for the chunk table's
* migration.
*
* The message table carries WHATEVER ELSE the app wants; the plane's hard requirements are only:
*
* | mapped column | requirement | why |
* | --- | --- | --- |
* | `key` | UNIQUE (normally the pk) | every checkpoint targets one row by it |
* | `seq` | integer, **NOT NULL DEFAULT 0** | the CAS column — nothing matches NULL (§3.2) |
* | `body` | text, empty at open | compaction overwrites it with the whole response |
* | `status` | text accepting `streaming`/`complete`/`cancelled`/`error`/`interrupted` | a CHECK
* constraint that omits one of these turns a seal into an infra failure |
* | `cancel` | truthy-readable, if mapped | read on the checkpoint round-trip (§6) |
* | `error` | nullable text, if mapped | compaction writes `NULL` when there is no error |
* | `host` | text, if mapped | written at open with the producer's token; the read-back decides the open race (§5.1) |
*
* `body` is PLANE-OWNED and always a bare string. Rich content (an array of content blocks, tool
* calls, attachments) belongs in SIBLING columns the app's own mutators write — `flush()` orders the
* text before them. For genuinely multi-block streaming, point `message` at a per-BLOCK table
* instead: `streamId` is just an app key, so one stream per block needs nothing from this plane.
*/
export interface StreamTables {
/** The app's message table. Must already contain the row when {@link StreamPlane.open} runs. */
message: string;
/** The append-only chunk table. */
chunks: string;
columns?: StreamColumns;
}StreamCheckpointTarget
export type StreamCheckpointTarget = {
tables: StreamTables;
} | {
commit: StreamCommit;
};StreamCheckpointPolicy
/** When a checkpoint fires — the FIRST of these wins (§3.1). Checkpoints are serialized, so a slow
* store degrades to fewer, larger checkpoints, never to a queue of them. */
export interface StreamCheckpointPolicy {
/** Produced-but-uncommitted characters that force a checkpoint. Default 512. */
chars?: number;
/** Milliseconds since the last checkpoint that force one. Default 750. */
intervalMs?: number;
/** Retries for a failing commit before the slice is left for the next trigger. Default 3. */
retries?: number;
}AuthorizeStreamInput
export interface AuthorizeStreamInput<User> {
user: User;
streamId: string;
/** Where the subscriber claims to be. */
from: number;
/** The `meta` this stream was opened with — `undefined` when the stream is not hosted here, which
* is precisely when the app must decide from `streamId` and its own durable state. */
meta: unknown;
request?: unknown;
}StreamRelay
/**
* Optional cross-process transport for the LIVE plane
* (designs-implemented/LM-STREAM-RELAY-DESIGN.md). Both methods are independently optional; which
* ones you implement is which topology you built — an addressing adapter (a Durable Object named by
* `streamId`, `fly-replay`) implements only `attach`; a broadcast adapter (Redis pub/sub, NATS)
* mirrors with `publish` and subscribes with `attach`; a log adapter (Redis Streams, Kafka) appends
* and replays. Never consulted for the durable plane — checkpoints are unaffected by any of this.
*
* The plane does not trust what `attach` yields: frames are run through the conform pass
* ({@link StreamRelayConform}) and any contract violation downgrades the subscription to `stale`,
* which already means "you are on the durable plane now". A broken relay costs a reader smooth
* tokens, never corrupted text — and can never reach the producer.
*/
export interface StreamRelay {
/** Producer side: every frame this process's producer fans out (`chunk`, `durable`, and the
* terminal `end`), mirrored outward. MUST NOT block; a throw or rejected promise is caught and
* routed to {@link RindleStreamOptions.onRelayError} — a relay outage may cost the live leg,
* never the generation. Returned promises are observed but never awaited. */
publish?(streamId: string, frame: StreamFrame): void | PromiseLike<void>;
/** Subscriber side: this process is not hosting `streamId`. Return a frame source, or `undefined`
* for `absent` — exactly the no-relay answer. Consulted only AFTER `authorize` has passed, and
* only on a live-plane miss (a local stream always wins). The plane closes the source
* (`return()`) when the reader disconnects. An adapter that cannot serve `from` (pub/sub has no
* history) yields `stale` and stops — the reader converges on the durable plane (§5). */
attach?(streamId: string, from: number): Promise<AsyncIterable<StreamFrame> | undefined>;
}StreamRelayErrorInfo
export interface StreamRelayErrorInfo {
streamId: string;
/** Where it failed: mirroring a frame out (`publish`), dialing the adapter (`attach`), or
* consuming/conforming its frames (`frames`). */
phase: "publish" | "attach" | "frames";
}RindleStreamOptions
export interface RindleStreamOptions<User> {
/** Where checkpoints land: the app's tables (the default path) or a raw `commit` callback. */
checkpoint: StreamCheckpointTarget;
/** REQUIRED. Subscribing to a stream is reading someone's chat, so there is no default-allow.
* Runs BEFORE existence is checked, so a denial cannot be used to probe for stream ids. */
authorize: Authorizer<AuthorizeStreamInput<User>>;
policy?: StreamCheckpointPolicy;
/** This process's identity — it must be UNIQUE per producer process, because the open CAS trusts
* it to distinguish rivals (§5.1). In `tables` mode, map {@link StreamColumns.host} and the open
* write persists it on the message row (so the app can route later subscribers to the hosting
* instance, §4) and uses it as the single-flight token; setting it WITHOUT a mapped `host` column
* is refused at construction. In `commit` mode it rides the `open` input. When a `host` column is
* mapped and no hostId is given, a random per-plane token is used — the CAS still holds, routing
* just has no stable name to read. */
hostId?: string;
/** Slack retained BELOW `durableSeq` so a client whose IVM view lags a checkpoint can still join
* without a `stale` round trip. Text at or above `durableSeq` is never trimmed. Default 64 KiB.
* `commit`-mode only — REFUSED (a construction-time `TypeError`) in `tables` mode, where
* compaction needs the whole produced text at close (§3.4), so the buffer is retained in full. */
retainChars?: number;
/** How long a sealed stream stays joinable before eviction. Default 30s. */
lingerMs?: number;
/** Per-subscriber frame queue cap; overflow drops that subscriber with `stale` (§4). Default 1024.
* Relayed readers reuse the same bound: a slow reader on a relayed stream costs itself the live
* leg exactly as a local one does. */
maxQueuedFrames?: number;
/** A checkpoint that exhausted its retries. The stream keeps streaming — this is a durability
* stall, not a stream stall — so the error must not vanish. Absent ⇒ `console.error`. */
onCheckpointError?: (err: unknown, info: {
streamId: string;
from: number;
seq: number;
}) => void;
/** Cross-process transport for the live plane ({@link StreamRelay}). Without one, a subscriber
* that lands on a process not hosting its stream gets `absent` and reads the durable plane at
* checkpoint granularity — correct, just chunky. */
relay?: StreamRelay;
/** Bound on `relay.attach`: a hung adapter yields `absent`, not a hung HTTP request. An
* addressing adapter MAY deliberately spend this window waiting out a subscribe that races its
* own kick. Default 2000. */
relayAttachTimeoutMs?: number;
/** A diagnostic, never a control path: relay failures (a throwing or rejecting `publish`, a failed
* or timed-out `attach`, a conform violation in the frames) land here, wrapped so a throwing hook
* cannot reach the plane. The reader-facing outcome is always the same legal `absent`/`stale`.
* Absent ⇒ `console.error`. */
onRelayError?: (err: unknown, info: StreamRelayErrorInfo) => void;
}OpenStreamInput
export interface OpenStreamInput<User> {
user: User;
/** The app's message-row id: the durable pointer AND the live plane's key. The row must already
* exist (the app's own mutator wrote it, alongside the user's prompt) with `seq = 0`. */
streamId: string;
/** Opaque app payload, passed through to a `commit` callback. Unused in `tables` mode. */
meta?: unknown;
request?: unknown;
}StreamHandle
/** The producer's handle (§2). One writer per stream, by construction. */
export interface StreamHandle {
readonly streamId: string;
/** Total produced code units. */
readonly seq: number;
/** Total code units the store has committed. Never exceeds {@link seq} (contract P). */
readonly durableSeq: number;
/** True once a checkpoint round-trip has seen the reader's cancel flag (§6). `pump` stops on it;
* a hand-rolled generation loop should check it. */
readonly cancelled: boolean;
/** Append a delta: fanned to subscribers synchronously, checkpointed on policy. */
push(text: string): void;
/** Force a checkpoint and resolve once the store holds every character produced so far. This is
* the ORDERING primitive: `await flush()` before writing a discrete row (a tool call, a stop
* reason) so the text precedes it in the store (§1). Rejects if the checkpoint cannot commit. */
flush(): Promise<number>;
/** Drain a delta iterable into the stream (the shape every LLM SDK's text stream already has),
* stopping early — and closing the iterator, which aborts the underlying request — once the
* reader has cancelled. */
pump(deltas: AsyncIterable<string>): Promise<void>;
/** Seal the stream: `cancelled` if the reader asked it to stop, else `complete`. In `tables` mode
* this is the compaction (§3.4) — it writes the whole body and drops the chunk rows in one
* transaction, so it also REPAIRS any checkpoint that failed along the way. */
close(): Promise<void>;
/** Seal `error` at whatever was produced. Never throws for the reason it is sealing. */
fail(error: unknown): Promise<void>;
}SubscribeStreamInput
export interface SubscribeStreamInput<User> {
user: User;
streamId: string;
/** "I already have this many characters" — from the client's IVM view, or a `Last-Event-ID`.
* A non-negative integer; default 0. */
from?: number;
request?: unknown;
}StreamSubscription
export interface StreamSubscription {
readonly streamId: string;
/** Terminates after exactly one of `end` / `stale` / `absent`. */
readonly frames: AsyncIterable<StreamFrame>;
/** Detach early (a disconnected client). Idempotent. */
close(): void;
}ResolvedStreamColumns
/** Every mapped column, defaults applied. `cancel`/`error` stay `undefined` unless named. */
export interface ResolvedStreamColumns {
key: string;
body: string;
status: string;
seq: string;
cancel?: string;
error?: string;
host?: string;
chunkKey: string;
chunkStream: string;
chunkSeq: string;
chunkText: string;
}resolveStreamColumns
export declare function resolveStreamColumns(columns: StreamColumns | undefined): ResolvedStreamColumns;streamChunkId
/** The chunk row's deterministic id: a replayed checkpoint collides with itself and is absorbed by
* `ON CONFLICT DO NOTHING` — idempotency without an envelope or a dedup ledger (§3.3). */
export declare function streamChunkId(streamId: string, seq: number): string;streamChunkTableDdl
/**
* The chunk table's DDL, for the app's migration. The app owns the message table (this only states
* the three columns the plane needs on it); the chunk table is entirely protocol-shaped, so it is
* generated rather than hand-written.
*/
export declare function streamChunkTableDdl(tables: StreamTables, dialect: SqlDialect): string[];StreamRelayConform
/**
* One frame source arriving over a relay, conformed to the CP §4 contract
* (designs-implemented/LM-STREAM-RELAY-DESIGN.md §4).
*
* An adapter is app code talking to Redis or a socket, and its frames feed `spliceStreamText` on a
* browser — so the plane does not trust them. This pass enforces the frame invariants against the
* prefix actually delivered and downgrades EVERY violation to a legal `stale` and nothing else:
* `stale` already means "you are on the durable plane now, the store is the whole truth", so a
* broken relay costs a reader smooth tokens, never corrupted text — and cannot wedge a producer.
*
* Replayed spans (a reconnecting adapter re-delivering what it already sent) are ABSORBED rather
* than punished — deduping against the delivered prefix is what makes reconnect-replay safe without
* every adapter hand-rolling it. Spans that overlap the prefix but extend past it pass through
* whole: the client splices at the frame's own offset, so an exact overlap re-covers and appends.
*
* Pure state, no I/O, no plane: `feed` maps one incoming frame to 0-2 outgoing frames (a missing
* `open` is synthesized at the join offset); `end`/`fail` close out a source that finished or threw
* without a terminal. After a terminal, every method returns `[]`.
*/
export declare class StreamRelayConform {
private readonly streamId;
/** The requested join offset — the synthesized `open`'s position, and where the prefix starts. */
private readonly from;
private readonly onViolation;
/** End of the delivered prefix. */
private pos;
private lastDurable;
private opened;
private done;
constructor(streamId: string, from: number, onViolation?: (reason: string) => void);
feed(frame: StreamFrame): StreamFrame[];
/** The source completed without a terminal (a truncated relay): the reader falls back. */
end(): StreamFrame[];
/** The source threw mid-iteration, or the plane is dropping a reader that stopped draining:
* a bare `stale` at the delivered position. */
fail(): StreamFrame[];
/** A synthesized join, for an adapter that (correctly, in broadcast mode) never mirrors the
* per-subscriber `open`: positioned at the requested offset, which the reader asked from because
* its durable view already holds it. */
private synthOpen;
private terminate;
private violate;
}StreamSqlSink
/** What the plane needs from the api-server to write a checkpoint: the backend's OUTSIDE-transaction
* SQL surface (`batch` is one transaction on every backend) and its dialect. Deliberately narrow so
* `streams.ts` never imports the server (no cycle). */
export interface StreamSqlSink {
readonly dialect: SqlDialect;
readonly sql: ServerSql;
}StreamPlane
export declare class StreamPlane<User> {
readonly chars: number;
readonly intervalMs: number;
readonly retries: number;
readonly retainChars: number;
readonly lingerMs: number;
readonly maxQueuedFrames: number;
readonly relayAttachTimeoutMs: number;
private readonly live;
/** Live relayed subscriptions, so teardown ({@link closeSync}) releases their drivers too. */
private readonly relayed;
private readonly opts;
private readonly sink;
private readonly mapped;
private readonly tables;
/** The open CAS token (§5.1): `hostId`, or a random per-plane stand-in when only the CAS — not
* routing — needs it. */
private readonly openToken;
constructor(opts: RindleStreamOptions<User>, sink?: StreamSqlSink);
/** Open a stream on an EXISTING message row (§5): the app's own mutator wrote it, alongside the
* user's prompt, so the pointer is already durable and every client's query already shows the
* message. This verifies it and flips it to `streaming`. */
open(input: OpenStreamInput<User>): Promise<StreamHandle>;
/**
* The read-only precondition on the app's message row, checked ONCE per `open` (§5.1). Read-only on
* purpose: it is a decision about the app's data, so re-deciding it per write attempt would let a
* lost ack turn a success into a refusal.
*
* The `streaming` check is the **single-flight guard**, and it lives at the ROW rather than in this
* process's map because the thing it defends against is distributed: the kick that starts a
* generation is an at-least-once effect (a retried mutation envelope re-runs its post-commit code,
* §10.5), so a second kick can land on another instance, where the in-memory map is empty. Two
* producers on one `streamId` would interleave: both CAS the same length, one stalls, and whichever
* closes last overwrites the body with ITS buffer. Cheap to refuse; expensive to debug.
*
* This probe alone is check-then-act — it and the open write are separate round trips, so two
* SIMULTANEOUS kicks can both pass it. Mapping a `host` column closes that window: the open write
* turns conditional and {@link verifyOpenWinner}'s read-back names the winner.
*/
private assertOpenable;
subscribe(input: SubscribeStreamInput<User>): Promise<StreamSubscription>;
/** The subscribe-miss leg (LM-STREAM-RELAY §3): ask the app's relay for the frames of a stream
* this process is not hosting. `undefined` — no relay, no `attach`, the adapter declined, timed
* out, or threw — is `absent`, exactly today's answer. */
private attachRelay;
/** `attach`, bounded by {@link RindleStreamOptions.relayAttachTimeoutMs}: a hung adapter yields
* `absent`, not a hung HTTP request. A source that resolves after the deadline is closed, not
* leaked. */
private boundedAttach;
/** Wrap an adapter's frame source as a plane subscription: conform every frame (LM-STREAM-RELAY
* §4), bound the reader with the same queue cap as a local one (§7), and tear the adapter down
* when either side lets go. The driver never throws into the plane: adapter failures become one
* `stale`. */
private relaySubscription;
/** Mirror one producer frame outward (LM-STREAM-RELAY §3). Never blocks or breaks the producer:
* a throw or rejected promise is reported and swallowed — a relay outage may cost relayed
* readers the live leg, never the generation or its checkpoints. */
publishRelay(streamId: string, frame: StreamFrame): void;
reportRelayError(err: unknown, info: StreamRelayErrorInfo): void;
/** Seal every live stream `interrupted`. In mapped-table mode the seal IS the compaction, so a
* graceful drain loses nothing PRODUCED; the status still says the response was cut short rather
* than claiming completion (§5). Wire it to SIGTERM. */
drainStreams(): Promise<void>;
/** Teardown: drop readers and timers WITHOUT a durable write (that is `drainStreams`). */
closeSync(): void;
/** A sealed stream stays joinable for the linger window, so a subscribe that races the last token
* still gets `end` (and the tail it missed) rather than a bare `absent`. */
retire(streamId: string): void;
reportCheckpointError(err: unknown, info: {
streamId: string;
from: number;
seq: number;
}): void;
/** Drive ONE checkpoint, retrying on failure. Every statement it emits is idempotent under replay
* — the chunk insert dedups on its deterministic id, the length CAS refuses to apply twice, the
* compaction is a whole-row overwrite — so "retry until it sticks" needs no dedup ledger and no
* `lmid` (§3.3). */
commit(input: StreamCommitInput): Promise<{
cancelRequested?: boolean;
seq?: number;
} | void>;
private commitOnce;
/** The read-back half of the open CAS, run only when a `host` column is mapped. The probe in
* `assertOpenable` and the open write are separate round trips, so bare check-then-act leaves a
* window where two simultaneous kicks both pass the probe; the conditional `markStreaming`
* matches nothing when a rival got there first, and whichever token the row now holds names the
* winner — a true CAS with no row counts needed. A replayed open (lost ack) reads back its OWN
* token and proceeds. */
private verifyOpenWinner;
/** The post-append read-back (§3.2): the row's authoritative length — which both CONFIRMS an
* append (the guarded batch reports no row count) and absorbs a committed-but-ack-lost one —
* plus the reader's cancel flag when mapped (§6), riding the same indexed point read. This read
* is LOAD-BEARING: a failure here fails the append attempt (retried, then stalled), because
* claiming durability the store did not confirm is the one dishonesty this plane refuses. */
private readAppendState;
/** The store's word on how much is durable — for resynchronizing after a FAILED append, where a
* lost ack may have left the store ahead of the plane. `undefined` in `commit` mode (no readable
* authority) or when the read itself fails (the next attempt retries the resync too). */
probeDurableSeq(streamId: string): Promise<number | undefined>;
}StreamOpenRefused
/** The open probe's verdict: the message row is missing or already advanced. Not retried — it is a
* settled statement about the app's data, and retrying re-asks a question already answered. */
export declare class StreamOpenRefused extends Error {
constructor(message: string);
}StreamForbidden
/** Refused by {@link RindleStreamOptions.authorize}. Distinct from the api-server's own
* `RindleApiError` so this module stays importable without the server (no cycle); the server
* translates it to a 403 at the handler seam. */
export declare class StreamForbidden extends Error {
readonly streamId: string;
constructor(streamId: string);
}STREAM_SSE_HEADERS
/** Headers for the SSE response. `x-accel-buffering` is the nginx-family opt-out — without it a
* buffering proxy holds the tokens and hands the user a paragraph at a time. */
export declare const STREAM_SSE_HEADERS: Record<string, string>;streamRequestFromHttp
/** Pull a subscribe request out of a fetch-style GET: `?streamId=…&from=…`, with `Last-Event-ID`
* winning over an explicit `from` (a reconnecting `EventSource` knows better than its own URL —
* the URL is the ORIGINAL join point, the header is where it actually got to). */
export declare function streamRequestFromHttp(req: {
url: string;
headers: {
get(name: string): string | null;
};
}): {
streamId: string;
from: number;
};streamFramesToSse
/**
* Encode a subscription as an SSE body. Each positional frame carries `id: <seq>`, so a browser
* `EventSource` that drops the connection resumes at exactly the right offset with no application
* code — its own `Last-Event-ID` header is the `from` of the next subscribe ({@link
* streamRequestFromHttp}).
*
* The reader must close the `EventSource` on the `end` frame: `EventSource` reconnects on ANY close,
* including a clean one.
*/
export declare function streamFramesToSse(sub: StreamSubscription, opts?: {
keepAliveMs?: number;
}): ReadableStream<Uint8Array>;