@tanstack/ai-durable-stream 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-stream.js","names":[],"sources":["../../src/durable-stream.ts"],"sourcesContent":["import { resolveResumeRunId } from '@tanstack/ai'\nimport type { StreamChunk, StreamDurability } from '@tanstack/ai'\n\ndeclare const durableStreamCursorBrand: unique symbol\n\n/** A validated, versioned offset produced by this adapter. */\ntype DurableStreamCursor = string & {\n readonly [durableStreamCursorBrand]: true\n}\n\n/** Adapter offsets also include the Durable Streams protocol sentinels. */\nexport type DurableStreamOffset = DurableStreamCursor | '-1' | 'now'\n\nexport interface DurableStreamOptions {\n /**\n * Base URL of the Durable Streams server (no trailing slash needed).\n * Optional when `fetch` is supplied — e.g. a Cloudflare service binding that\n * ignores the host and dispatches to the bound Worker by path — in which case\n * an internal placeholder base is used and only the `/streams/...` path\n * matters.\n */\n server?: string\n /** Stream-name prefix. Defaults to `runs`. */\n streamPrefix?: string\n /** Fetch implementation. Defaults to the global fetch. */\n fetch?: typeof globalThis.fetch\n /**\n * Headers applied to every create, append, read, and close request. A\n * resolver is called for every request so credentials can rotate.\n */\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>)\n /**\n * Bounding for the read reconnect loop. After a response-body read failure\n * mid-window, `read` retries from the last valid position; these cap\n * consecutive retries and throttle them so a persistently failing backend\n * surfaces the error instead of looping without end. Normal window\n * advancement (long-poll) is never throttled.\n */\n reconnect?: {\n /**\n * Consecutive body-read-failure retries before surfacing the underlying\n * read error. Default 10.\n */\n maxReadFailures?: number\n /** Delay between read retries, in ms. Default 250. */\n delayMs?: number\n }\n /**\n * Timeout (ms) for a single create / append / close request to the backend.\n * A stalled backend would otherwise hang chunk delivery or terminalization\n * indefinitely. Default 30000. Long-poll `read` window advancement is NOT\n * bounded by this — a caught-up reader may legitimately wait. `snapshot`,\n * which must always return, IS bounded by it.\n */\n operationTimeoutMs?: number\n /**\n * Producer fencing epoch sent as `Producer-Epoch` on every append.\n *\n * A backend that fences producers rejects an append whose epoch is below the\n * highest it has seen, so a zombie host that lost its claim cannot keep\n * writing to a run a newer host took over. Callers that track a monotonic\n * per-run driver epoch (`RunRecord.driverEpoch`) should pass it here; the\n * default of `0` makes every producer look equally current to the backend,\n * which leaves fencing entirely to the caller's own run claim.\n */\n producerEpoch?: number\n}\n\n/** Resolve after `ms`, or immediately once `signal` aborts. Never rejects. */\nfunction abortableDelay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve()\n return new Promise((resolve) => {\n const onAbort = () => {\n clearTimeout(timer)\n resolve()\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort)\n resolve()\n }, ms)\n signal?.addEventListener('abort', onAbort, { once: true })\n })\n}\n\nexport class DurableStreamError extends Error {\n override name = 'DurableStreamError'\n\n constructor(message: string) {\n super(`durableStream: ${message}`)\n }\n}\n\ninterface SseEvent {\n event?: string\n data?: string\n}\n\ninterface WireRecord {\n v: 1\n seq: number\n chunk: StreamChunk\n}\n\ninterface CursorPayload {\n v: 1\n backendOffset: string\n seq: number\n}\n\ninterface ControlFrame {\n streamNextOffset: string\n streamCursor?: string\n upToDate?: boolean\n streamClosed?: boolean\n}\n\nconst CURSOR_PREFIX = 'tanstack-ai-ds:v1:'\nconst READ_ABORTED = Symbol('read aborted')\n\n/**\n * Hard ceiling on the SSE windows a single `snapshot` will pull before it gives\n * up. A snapshot stops at the first control frame that reports the reader caught\n * up (`upToDate`), so a conforming backend ends it in one or two windows. This\n * only fires for a backend that keeps handing out advancing windows without ever\n * reporting `upToDate`, where the alternative is a read that never returns.\n */\nconst SNAPSHOT_MAX_WINDOWS = 1000\n\nclass ResponseBodyReadFailure extends Error {\n override name = 'ResponseBodyReadFailure'\n\n constructor(readonly readError: unknown) {\n super('response body read failed')\n }\n}\n\nfunction assertTransportField(value: string, name: string): string {\n if (value.trim().length === 0 || /[\\r\\n]/.test(value)) {\n throw new DurableStreamError(\n `${name} must be non-empty and contain no CR/LF`,\n )\n }\n return value\n}\n\nfunction assertRunId(value: string): string {\n return assertTransportField(value, 'runId')\n}\n\nfunction isDurableStreamCursor(value: string): value is DurableStreamCursor {\n return value.startsWith(CURSOR_PREFIX)\n}\n\nfunction isCursorPayload(value: unknown): value is CursorPayload {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'v' in value &&\n value.v === 1 &&\n 'backendOffset' in value &&\n typeof value.backendOffset === 'string' &&\n 'seq' in value &&\n typeof value.seq === 'number' &&\n Number.isSafeInteger(value.seq) &&\n value.seq > 0\n )\n}\n\nfunction encodeCursor(payload: CursorPayload): DurableStreamCursor {\n assertTransportField(payload.backendOffset, 'backend offset')\n if (!Number.isSafeInteger(payload.seq) || payload.seq < 1) {\n throw new DurableStreamError(`invalid record sequence: ${payload.seq}`)\n }\n const cursor = `${CURSOR_PREFIX}${encodeURIComponent(JSON.stringify(payload))}`\n if (!isDurableStreamCursor(cursor)) {\n throw new DurableStreamError('failed to encode cursor')\n }\n return cursor\n}\n\nfunction decodeCursor(cursor: string): CursorPayload {\n if (!isDurableStreamCursor(cursor)) {\n throw new DurableStreamError('invalid or unsupported resume offset')\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(decodeURIComponent(cursor.slice(CURSOR_PREFIX.length)))\n } catch {\n throw new DurableStreamError('invalid or unsupported resume offset')\n }\n if (!isCursorPayload(parsed)) {\n throw new DurableStreamError('invalid or unsupported resume offset')\n }\n assertTransportField(parsed.backendOffset, 'backend offset')\n return parsed\n}\n\nfunction safeSearchParam(request: Request, key: string): string | null {\n try {\n return new URL(request.url).searchParams.get(key)\n } catch {\n return null\n }\n}\n\nfunction parseResumeOffset(raw: string | null): DurableStreamOffset | null {\n if (raw === null || raw === '-1' || raw === 'now') return raw\n decodeCursor(raw)\n if (!isDurableStreamCursor(raw)) {\n throw new DurableStreamError('invalid or unsupported resume offset')\n }\n return raw\n}\n\nasync function* readLines(\n body: ReadableStream<Uint8Array>,\n signal?: AbortSignal,\n): AsyncGenerator<string> {\n const reader = body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n let completed = false\n let cancelled = false\n let readFailed = false\n try {\n for (;;) {\n let result: ReadableStreamReadResult<Uint8Array> | typeof READ_ABORTED\n try {\n result = await readWithAbort(reader, signal)\n } catch (error) {\n readFailed = true\n throw new ResponseBodyReadFailure(error)\n }\n if (result === READ_ABORTED) {\n cancelled = true\n await reader.cancel(signal?.reason)\n return\n }\n if (result.done) {\n completed = true\n break\n }\n buffer += decoder.decode(result.value, { stream: true })\n const parts = buffer.split('\\n')\n buffer = parts.pop() ?? ''\n for (const raw of parts) {\n yield raw.endsWith('\\r') ? raw.slice(0, -1) : raw\n }\n }\n buffer += decoder.decode()\n if (buffer.length > 0) {\n yield buffer.endsWith('\\r') ? buffer.slice(0, -1) : buffer\n }\n } finally {\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- readFailed is set in the catch before its throw; CFA can't see that from finally\n if (!completed && !cancelled && !readFailed) await reader.cancel()\n } finally {\n reader.releaseLock()\n }\n }\n}\n\nfunction readWithAbort(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n signal: AbortSignal | undefined,\n): Promise<ReadableStreamReadResult<Uint8Array> | typeof READ_ABORTED> {\n if (!signal) return reader.read()\n if (signal.aborted) return Promise.resolve(READ_ABORTED)\n\n return new Promise((resolve, reject) => {\n const onAbort = () => {\n signal.removeEventListener('abort', onAbort)\n resolve(READ_ABORTED)\n }\n signal.addEventListener('abort', onAbort, { once: true })\n reader.read().then(\n (result) => {\n signal.removeEventListener('abort', onAbort)\n resolve(result)\n },\n (error: unknown) => {\n signal.removeEventListener('abort', onAbort)\n reject(error)\n },\n )\n })\n}\n\nasync function* parseSseEvents(\n body: ReadableStream<Uint8Array>,\n signal?: AbortSignal,\n): AsyncGenerator<SseEvent> {\n let current: SseEvent = {}\n let hasField = false\n\n for await (const line of readLines(body, signal)) {\n if (line === '') {\n if (hasField) yield current\n current = {}\n hasField = false\n continue\n }\n if (line.startsWith(':')) continue\n\n const colon = line.indexOf(':')\n const field = colon === -1 ? line : line.slice(0, colon)\n let value = colon === -1 ? '' : line.slice(colon + 1)\n if (value.startsWith(' ')) value = value.slice(1)\n if (field === 'event') {\n current.event = value\n hasField = true\n } else if (field === 'data') {\n current.data =\n current.data === undefined ? value : `${current.data}\\n${value}`\n hasField = true\n }\n }\n if (hasField) yield current\n}\n\nfunction isStreamChunk(value: unknown): value is StreamChunk {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'type' in value &&\n typeof value.type === 'string'\n )\n}\n\nfunction isWireRecord(value: unknown): value is WireRecord {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'v' in value &&\n value.v === 1 &&\n 'seq' in value &&\n typeof value.seq === 'number' &&\n Number.isSafeInteger(value.seq) &&\n value.seq > 0 &&\n 'chunk' in value &&\n isStreamChunk(value.chunk)\n )\n}\n\nfunction parseDataRecords(data: string | undefined): Array<WireRecord> {\n if (data === undefined) {\n throw new DurableStreamError('data event had no payload')\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(data)\n } catch {\n throw new DurableStreamError('data event contained invalid JSON')\n }\n if (!Array.isArray(parsed)) {\n throw new DurableStreamError('data event payload must be a JSON array')\n }\n const records: Array<WireRecord> = []\n for (const value of parsed) {\n if (!isWireRecord(value)) {\n throw new DurableStreamError('data event contained an invalid record')\n }\n records.push(value)\n }\n return records\n}\n\nfunction optionalBoolean(\n value: object,\n name: 'upToDate' | 'streamClosed',\n): boolean | undefined {\n const field =\n name === 'upToDate'\n ? 'upToDate' in value\n ? value.upToDate\n : undefined\n : 'streamClosed' in value\n ? value.streamClosed\n : undefined\n if (field === undefined) return undefined\n if (typeof field !== 'boolean') {\n throw new DurableStreamError(`control field ${name} must be boolean`)\n }\n return field\n}\n\nfunction parseControlFrame(data: string | undefined): ControlFrame {\n if (data === undefined) {\n throw new DurableStreamError('control event had no payload')\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(data)\n } catch {\n throw new DurableStreamError('control event contained invalid JSON')\n }\n if (typeof parsed !== 'object' || parsed === null) {\n throw new DurableStreamError('control event payload must be an object')\n }\n if (\n !('streamNextOffset' in parsed) ||\n typeof parsed.streamNextOffset !== 'string'\n ) {\n throw new DurableStreamError(\n 'control event requires string streamNextOffset',\n )\n }\n const streamNextOffset = assertTransportField(\n parsed.streamNextOffset,\n 'control streamNextOffset',\n )\n let streamCursor: string | undefined\n if ('streamCursor' in parsed) {\n if (typeof parsed.streamCursor !== 'string') {\n throw new DurableStreamError('control streamCursor must be a string')\n }\n streamCursor = assertTransportField(\n parsed.streamCursor,\n 'control streamCursor',\n )\n }\n const upToDate = optionalBoolean(parsed, 'upToDate')\n const streamClosed = optionalBoolean(parsed, 'streamClosed')\n if (streamClosed !== true && streamCursor === undefined) {\n throw new DurableStreamError(\n 'open control event requires string streamCursor',\n )\n }\n return {\n streamNextOffset,\n ...(streamCursor === undefined ? {} : { streamCursor }),\n ...(upToDate === undefined ? {} : { upToDate }),\n ...(streamClosed === undefined ? {} : { streamClosed }),\n }\n}\n\nfunction requireNextOffset(response: Response, operation: string): string {\n const offset = response.headers.get('Stream-Next-Offset')\n if (offset === null || offset.trim().length === 0) {\n throw new DurableStreamError(\n `${operation} response missing non-empty Stream-Next-Offset`,\n )\n }\n return assertTransportField(offset, `${operation} Stream-Next-Offset`)\n}\n\nfunction httpFailure(\n operation: string,\n response: Response,\n): DurableStreamError {\n return new DurableStreamError(\n `failed to ${operation} (${response.status} ${response.statusText})`,\n )\n}\n\n/**\n * External-URL Durable Streams protocol adapter.\n *\n * `request` must name a run — `X-Run-Id` header (what a `@tanstack/ai-client`\n * POST sends) or `?runId` (what a GET attach sends), resolved by core's\n * `resolveResumeRunId`. A request that names neither throws rather than\n * silently producing into an unaddressable stream.\n *\n * Returns a plain `StreamDurability`, not an `UpsertableStreamDurability`.\n * This adapter's offsets embed a backend-assigned Next-Offset cursor, so a\n * caller cannot choose them; there is no `upsert` implementation to supply.\n * Omitting `upsert` is the type-level statement that this adapter does not\n * support caller-supplied offsets, so a consumer requiring that capability\n * gets a compile error at the wiring site instead of a runtime failure.\n */\nexport function durableStream(\n request: Request,\n options: DurableStreamOptions,\n): StreamDurability<DurableStreamOffset> {\n const fetchFn = options.fetch ?? globalThis.fetch\n if (options.server === undefined && options.fetch === undefined) {\n throw new DurableStreamError(\n 'server is required unless a fetch implementation is provided',\n )\n }\n // When a custom fetch routes by path (e.g. a service binding), the host is\n // irrelevant; a reserved `.internal` base parses without ever resolving.\n const rawServer = options.server ?? 'https://durable-streams.internal'\n assertTransportField(rawServer, 'server URL')\n try {\n void new URL(rawServer)\n } catch {\n throw new DurableStreamError(\n `invalid server URL: ${JSON.stringify(rawServer)}`,\n )\n }\n const server = rawServer.replace(/\\/+$/, '')\n const maxReadFailures = options.reconnect?.maxReadFailures ?? 10\n const readRetryDelayMs = options.reconnect?.delayMs ?? 250\n const operationTimeoutMs = options.operationTimeoutMs ?? 30_000\n\n // create / append / close go through this so a stalled backend can't hang the\n // operation forever. Each call gets a fresh timeout; long-poll `read` calls\n // deliberately do NOT use it (they may wait for the producer to advance).\n const fetchWithTimeout = async (\n url: string | URL,\n init: RequestInit,\n ): Promise<Response> => {\n const controller = new AbortController()\n const timer = setTimeout(() => {\n controller.abort(\n new DurableStreamError(\n `request exceeded operationTimeoutMs (${operationTimeoutMs}ms)`,\n ),\n )\n }, operationTimeoutMs)\n try {\n return await fetchFn(url, { ...init, signal: controller.signal })\n } finally {\n clearTimeout(timer)\n }\n }\n const prefix = assertTransportField(\n options.streamPrefix ?? 'runs',\n 'streamPrefix',\n )\n const rawResumeOffset =\n request.headers.get('Last-Event-ID') ?? safeSearchParam(request, 'offset')\n const resumeOffset = parseResumeOffset(rawResumeOffset)\n // Resolved through core's `resolveResumeRunId` — `X-Run-Id` header first,\n // then `?runId` — the same single implementation `memoryStream` and the\n // resume response helpers use, so no two durability adapters can disagree\n // about which run a request names. It has to be the header first: a\n // `@tanstack/ai-client` POST keeps its URL byte-identical to a plain chat\n // request and carries the run id in `X-Run-Id`, so a query-only adapter\n // would name a different stream than the GET attach route addresses.\n const requestedRunId = resolveResumeRunId(request)\n if (requestedRunId === null) {\n // Never mint a random id here. The backend stream name is derived from the\n // run id, so a generated one addresses a stream no attach request could\n // ever name: the producer would appear to work while writing where nobody\n // can read. Refuse up front instead, matching `DurableRunIdRequiredError`\n // in `@tanstack/ai-sandbox`.\n throw new DurableStreamError(\n resumeOffset === null\n ? 'a runId is required: send it as an X-Run-Id header or a ?runId query param'\n : 'resume offset requires a runId',\n )\n }\n const runId = assertRunId(requestedRunId)\n\n const streamUrl = `${server}/streams/${encodeURIComponent(`${prefix}/${runId}`)}`\n let createPromise: Promise<string> | undefined\n // Set only when the create PUT answered `201 Created`, which RFC 9110 §9.3.4\n // reserves for a PUT that brought the resource into existence: an existing\n // stream is a replace/no-op and answers 200 or 204. A stream this instance\n // created cannot already hold records, which is what makes the tail read below\n // skippable. Defaulting to `false` keeps the conservative side: anything other\n // than a proven creation still pays for the seeding read.\n let createdHere = false\n let appendTailOffset: string | undefined\n // `seq` is a single per-run counter, not a per-instance one: the read side\n // dedups and orders by it across every window and every producer. A takeover\n // host therefore MUST continue the log's existing sequence instead of\n // restarting at 1, or its records collide with the prefix an earlier host\n // stored and get silently dropped by the reader's dedup. `seqSeeded` tracks\n // whether this instance has learned where the log ends.\n let nextSeq = 1\n let seqSeeded = false\n let seedPromise: Promise<void> | undefined\n const producerId = crypto.randomUUID()\n const producerEpoch = options.producerEpoch ?? 0\n if (!Number.isSafeInteger(producerEpoch) || producerEpoch < 0) {\n throw new DurableStreamError(\n `producerEpoch must be a non-negative safe integer: ${producerEpoch}`,\n )\n }\n let producerSeq = 0\n let closePromise: Promise<void> | undefined\n\n /**\n * Raise the append counter past a sequence already present in the log.\n *\n * Called for every record any read observes — including records the reader\n * then dedups away — so both `snapshot` (the alignment path a takeover\n * already runs) and a plain `read` teach this instance the log's tail.\n */\n const observeSeq = (seq: number): void => {\n if (seq >= nextSeq) nextSeq = seq + 1\n }\n\n const resolveHeaders = async (required?: HeadersInit): Promise<Headers> => {\n const configured =\n typeof options.headers === 'function'\n ? await options.headers()\n : options.headers\n const headers = new Headers(configured)\n if (required) {\n new Headers(required).forEach((value, key) => headers.set(key, value))\n }\n return headers\n }\n\n const ensureCreated = (): Promise<string> => {\n if (createPromise) return createPromise\n createPromise = (async () => {\n const response = await fetchWithTimeout(streamUrl, {\n method: 'PUT',\n headers: await resolveHeaders({ 'Content-Type': 'application/json' }),\n })\n if (!response.ok) throw httpFailure('create stream', response)\n const offset = requireNextOffset(response, 'create')\n createdHere = response.status === 201\n appendTailOffset = offset\n return offset\n })().catch((error: unknown) => {\n createPromise = undefined\n throw error\n })\n return createPromise\n }\n\n /**\n * The one window-pulling loop behind both `read` and `snapshot`.\n *\n * `stopWhenUpToDate` is the only difference between them. The protocol's\n * control frame carries `upToDate: true` when the backend has handed the\n * reader everything the stream currently holds; a live `read` ignores that and\n * keeps long-polling for more, while a `snapshot` returns there. That makes a\n * snapshot bounded even on a stream nobody ever closed.\n */\n const readWindows = async function* (\n offset: DurableStreamOffset,\n signal: AbortSignal | undefined,\n stopWhenUpToDate: boolean,\n ): AsyncGenerator<{ offset: DurableStreamOffset; chunk: StreamChunk }> {\n let backendOffset: string\n let deliveredThroughSeq = 0\n if (offset === '-1' || offset === 'now') {\n backendOffset = offset\n } else {\n const cursor = decodeCursor(offset)\n backendOffset = cursor.backendOffset\n deliveredThroughSeq = cursor.seq\n }\n let streamCursor: string | undefined\n let consecutiveReadFailures = 0\n let windowsPulled = 0\n\n for (;;) {\n if (signal?.aborted) return\n windowsPulled += 1\n if (stopWhenUpToDate && windowsPulled > SNAPSHOT_MAX_WINDOWS) {\n throw new DurableStreamError(\n `snapshot read ${SNAPSHOT_MAX_WINDOWS} windows without the backend reporting upToDate`,\n )\n }\n const requestOffset = backendOffset\n const requestCursor = streamCursor\n const url = new URL(streamUrl)\n url.searchParams.set('offset', backendOffset)\n url.searchParams.set('live', 'sse')\n if (streamCursor !== undefined) {\n url.searchParams.set('cursor', streamCursor)\n }\n\n let response: Response\n try {\n response = await fetchFn(url, {\n method: 'GET',\n headers: await resolveHeaders(),\n signal,\n })\n } catch (error) {\n if (signal?.aborted) return\n throw error\n }\n if (!response.ok) throw httpFailure('read', response)\n if (!response.body) {\n throw new DurableStreamError('read response had no body')\n }\n\n let dataStartOffset = backendOffset\n let sawControl = false\n let dataAwaitingControl = false\n let yieldedData = false\n // Guards intra-response ordering: seqs must strictly increase across the\n // whole response (including across data frames and control frames — seq\n // is per-run, not per-window). Starts at 0 so a legitimate replay of\n // already-delivered records still passes, then the dedup below drops\n // them; the throw catches a genuinely malformed [seq 2, seq 1] or a\n // duplicate seq that would otherwise be silently discarded.\n let previousResponseSeq = 0\n try {\n for await (const event of parseSseEvents(response.body, signal)) {\n if (signal?.aborted) return\n if (event.event === 'data') {\n dataAwaitingControl = true\n for (const record of parseDataRecords(event.data)) {\n if (record.seq <= previousResponseSeq) {\n throw new DurableStreamError(\n 'data records must have strictly increasing sequences',\n )\n }\n previousResponseSeq = record.seq\n observeSeq(record.seq)\n if (record.seq <= deliveredThroughSeq) continue\n deliveredThroughSeq = record.seq\n yieldedData = true\n yield {\n offset: encodeCursor({\n v: 1,\n backendOffset: dataStartOffset,\n seq: record.seq,\n }),\n chunk: record.chunk,\n }\n }\n continue\n }\n if (event.event === 'control') {\n const control = parseControlFrame(event.data)\n backendOffset = control.streamNextOffset\n streamCursor = control.streamCursor\n dataStartOffset = backendOffset\n sawControl = true\n dataAwaitingControl = false\n if (control.streamClosed === true) return\n // A snapshot has now been handed everything the backend holds.\n // Returning here abandons the rest of the SSE body, which the\n // reader's `finally` cancels.\n if (stopWhenUpToDate && control.upToDate === true) return\n continue\n }\n throw new DurableStreamError(\n `unexpected SSE event type: ${JSON.stringify(event.event)}`,\n )\n }\n } catch (error) {\n if (signal?.aborted) return\n if (error instanceof ResponseBodyReadFailure) {\n if (\n yieldedData ||\n (sawControl &&\n (backendOffset !== requestOffset ||\n streamCursor !== requestCursor))\n ) {\n // Made progress before the body failed — retry from the last valid\n // position, but cap consecutive failures and throttle so a\n // persistently failing backend surfaces the error, not a hot loop.\n consecutiveReadFailures += 1\n if (consecutiveReadFailures > maxReadFailures) throw error.readError\n await abortableDelay(readRetryDelayMs, signal)\n continue\n }\n throw error.readError\n }\n throw error\n }\n\n // A window read to completion (no body failure) clears the streak; only\n // consecutive failures accumulate toward the ceiling.\n consecutiveReadFailures = 0\n\n if (signal?.aborted) return\n if (dataAwaitingControl || !sawControl) {\n throw new DurableStreamError(\n 'read SSE window ended without a matching control event',\n )\n }\n if (backendOffset === requestOffset && streamCursor === requestCursor) {\n throw new DurableStreamError(\n 'read SSE window ended without advancing offset or cursor',\n )\n }\n }\n }\n\n /**\n * One bounded pass over everything the log currently holds.\n *\n * Two things bound it. `readWindows(..., stopWhenUpToDate=true)` returns at\n * the first control frame reporting the reader caught up, and\n * `SNAPSHOT_MAX_WINDOWS` catches a backend that keeps handing out advancing\n * windows without ever saying so. Neither covers a backend that simply never\n * answers — `upToDate` is an optional protocol field, read windows\n * deliberately skip `fetchWithTimeout` (a caught-up live reader may wait),\n * and an empty still-open log has nothing to send. That shape would park the\n * fetch forever, so the snapshot carries its own `operationTimeoutMs`\n * deadline. Timing out is a loud failure, never a truncated result: an\n * aborted `readWindows` ends its iteration quietly, so the flag is rechecked\n * after the loop and thrown.\n *\n * `ensureCreated()` first, exactly as `append` and `read` do. A snapshot of a\n * stream the backend does not hold yet must answer \"nothing has been\n * delivered\", not reject: `sandboxRunDriver`'s `pipe` calls\n * `awaitLogQuiescence` — two `snapshot()` reads — BEFORE the first append, so\n * the very first producer of every durable run snapshots a stream no `PUT` has\n * created. Reading straight through would surface that as\n * `httpFailure('read', ...)` and fail the run at its first chunk. It also keeps\n * this adapter's contract identical to core's `memoryStream`, which resolves to\n * `[]` for an unknown run; two `StreamDurability` implementations must not\n * disagree about so basic a case. `ensureCreated` is idempotent and memoised,\n * so this costs nothing once the stream exists.\n */\n const collectSnapshot = async (): Promise<\n Array<{ offset: DurableStreamOffset; chunk: StreamChunk }>\n > => {\n await ensureCreated()\n const controller = new AbortController()\n let timedOut = false\n const timer = setTimeout(() => {\n timedOut = true\n controller.abort(\n new DurableStreamError(\n `snapshot exceeded operationTimeoutMs (${operationTimeoutMs}ms)`,\n ),\n )\n }, operationTimeoutMs)\n try {\n const entries: Array<{\n offset: DurableStreamOffset\n chunk: StreamChunk\n }> = []\n for await (const entry of readWindows('-1', controller.signal, true)) {\n entries.push(entry)\n }\n if (timedOut) {\n throw new DurableStreamError(\n `snapshot exceeded operationTimeoutMs (${operationTimeoutMs}ms) before the backend reported upToDate`,\n )\n }\n // A completed snapshot has seen every record stored, so `observeSeq` has\n // already moved `nextSeq` past the log's tail.\n seqSeeded = true\n return entries\n } finally {\n clearTimeout(timer)\n }\n }\n\n /**\n * Learn where the log ends before this instance appends to it for the first\n * time.\n *\n * A takeover host is handed a fresh adapter for a run whose log already holds\n * `seq 1..N`, and nothing in the protocol reports a record count, so the tail\n * has to be read. One bounded read per instance, and the alignment `snapshot()`\n * a takeover already performs satisfies it.\n *\n * A brand-new run pays nothing, and must not: the seeding read cannot be on the\n * producer's critical path. `upToDate` is an optional protocol field and an\n * empty still-open log has nothing to send, so on a backend that omits it the\n * read has to run its `operationTimeoutMs` deadline out and then fail — the\n * producer would wait on a reader that is waiting on the producer, and every\n * fresh run on such a backend would die of a synthetic error. `createdHere`\n * settles it without a request: a stream this instance brought into existence\n * provably holds no records, so `nextSeq` is already correct at 1.\n */\n const ensureSeqSeeded = (): Promise<void> => {\n if (seqSeeded) return Promise.resolve()\n if (seedPromise) return seedPromise\n seedPromise = (async () => {\n await ensureCreated()\n if (createdHere) {\n seqSeeded = true\n return\n }\n await collectSnapshot()\n })().catch((error: unknown) => {\n seedPromise = undefined\n throw error\n })\n return seedPromise\n }\n\n return {\n resumeFrom: () => resumeOffset,\n append: async (chunks) => {\n if (chunks.length === 0) return []\n await ensureSeqSeeded()\n const batchStartOffset = appendTailOffset ?? (await ensureCreated())\n const firstSeq = nextSeq\n const records = chunks.map(\n (chunk, index): WireRecord => ({\n v: 1,\n seq: firstSeq + index,\n chunk,\n }),\n )\n // Through `observeSeq`, so a concurrent read that already pushed the\n // counter further cannot be walked backwards.\n observeSeq(firstSeq + records.length - 1)\n const requestProducerSeq = producerSeq\n producerSeq += 1\n const requestInit: RequestInit = {\n method: 'POST',\n headers: await resolveHeaders({\n 'Content-Type': 'application/json',\n 'Producer-Id': producerId,\n 'Producer-Epoch': String(producerEpoch),\n 'Producer-Seq': String(requestProducerSeq),\n }),\n body: JSON.stringify(records),\n }\n let response: Response\n try {\n response = await fetchWithTimeout(streamUrl, requestInit)\n } catch (firstError) {\n try {\n response = await fetchWithTimeout(streamUrl, requestInit)\n } catch (retryError) {\n throw new AggregateError(\n [firstError, retryError],\n 'durableStream: append failed before its outcome could be confirmed',\n )\n }\n }\n if (!response.ok) throw httpFailure('append', response)\n const nextOffset = requireNextOffset(response, 'append')\n appendTailOffset = nextOffset\n return records.map((record) =>\n encodeCursor({\n v: 1,\n backendOffset: batchStartOffset,\n seq: record.seq,\n }),\n )\n },\n close: () => {\n if (closePromise) return closePromise\n closePromise = (async () => {\n await ensureCreated()\n const response = await fetchWithTimeout(streamUrl, {\n method: 'POST',\n headers: await resolveHeaders({ 'Stream-Closed': 'true' }),\n })\n if (!response.ok) throw httpFailure('close', response)\n const nextOffset = requireNextOffset(response, 'close')\n if (response.headers.get('Stream-Closed')?.toLowerCase() !== 'true') {\n throw new DurableStreamError(\n 'close response missing Stream-Closed: true',\n )\n }\n appendTailOffset = nextOffset\n })().catch((error: unknown) => {\n closePromise = undefined\n throw error\n })\n return closePromise\n },\n read: (offset, signal) => readWindows(offset, signal, false),\n snapshot: () => collectSnapshot(),\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `@tanstack/ai-durable-stream` — a {@link StreamDurability} backend over the
3
+ * durable-streams HTTP protocol, for production-grade delivery durability.
4
+ */
5
+ export { durableStream } from './durable-stream.js';
6
+ export { DurableStreamError } from './durable-stream.js';
7
+ export type { DurableStreamOffset, DurableStreamOptions, } from './durable-stream.js';
@@ -0,0 +1,2 @@
1
+ import { DurableStreamError, durableStream } from "./durable-stream.js";
2
+ export { DurableStreamError, durableStream };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@tanstack/ai-durable-stream",
3
+ "version": "0.0.0",
4
+ "description": "Delivery durability for TanStack AI over the durable-streams HTTP protocol — a resumable StreamDurability transport sink (append/read/resume) that stores zero delivery events itself.",
5
+ "author": "",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/TanStack/ai.git",
13
+ "directory": "packages/ai-durable-stream"
14
+ },
15
+ "keywords": [
16
+ "ai",
17
+ "tanstack",
18
+ "durable-streams",
19
+ "delivery-durability",
20
+ "resumable",
21
+ "sse"
22
+ ],
23
+ "type": "module",
24
+ "module": "./dist/esm/index.js",
25
+ "types": "./dist/esm/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/esm/index.d.ts",
29
+ "import": "./dist/esm/index.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src"
35
+ ],
36
+ "scripts": {
37
+ "build": "vite build",
38
+ "clean": "premove ./build ./dist",
39
+ "lint:fix": "oxlint src --type-aware --fix",
40
+ "test:build": "publint --strict",
41
+ "test:oxlint": "oxlint src --type-aware",
42
+ "test:lib": "vitest run",
43
+ "test:lib:dev": "pnpm test:lib --watch",
44
+ "test:types": "tsc"
45
+ },
46
+ "peerDependencies": {
47
+ "@tanstack/ai": "workspace:^"
48
+ },
49
+ "devDependencies": {
50
+ "@tanstack/ai": "workspace:*",
51
+ "@tanstack/ai-sandbox": "workspace:*",
52
+ "@vitest/coverage-v8": "4.0.14"
53
+ }
54
+ }