/** * Zero-dependency Server-Sent Events reader for **one-shot POST streams** — the * pattern where a SvelteKit API route relays an LLM (or any) stream to the * browser as `text/event-stream`. It replaces the hand-rolled * `fetch` + frame-splitting loop that grows around every such endpoint. * * This is deliberately **not** an `EventSource` replacement: * - It POSTs a body (EventSource is GET-only) and takes an injectable `fetch`, * so it also runs inside a SvelteKit `load`. * - It does **not** reconnect. The `retry:` field (and any unknown field) is * parsed and ignored — a dropped connection surfaces as the underlying * `fetch`/read error, which the consumer sees and decides what to do about. * * The parser implements the core of the WHATWG SSE stream-interpretation * algorithm and is **chunk-decomposition-invariant**: the emitted event * sequence is identical no matter how the byte stream is split into chunks — * including a split inside a CRLF pair or in the middle of a multi-byte UTF-8 * character (a `TextDecoder` with `stream: true` coalesces the latter). * * Supported line terminators: `\r\n`, `\n`, and a lone `\r` (a `\r` followed by * a non-`\n` char is its own terminator). Fields: `data` (accumulated; multiple * `data:` lines join with `\n`), `event` (event name), `id` (event id; a value * containing a NUL is ignored per spec). Comment lines (starting with `:`) and * `retry:`/unknown fields are ignored. An event is dispatched on a blank line * and only if it carried at least one `data` line; a trailing buffer without a * final blank line is not dispatched. A single leading UTF-8 BOM is stripped. */ /** One parsed Server-Sent Event. */ export interface SseEvent { /** Event name from the `event:` field; `'message'` when omitted. */ event: string; /** Data payload — all `data:` lines of the event joined with `\n`. */ data: string; /** Last `id:` field seen at dispatch time, when the stream sets one. */ id?: string; } /** Options for {@link streamSse}. */ export interface StreamSseOptions { /** * HTTP method for the request. * @default 'POST' */ method?: string; /** * Extra request headers. These win over the defaults: `accept: * text/event-stream` is always sent, and `content-type: application/json` is * added for a non-string {@link body} — pass either key here to override. */ headers?: Record; /** * Request body. A string is sent verbatim (no `content-type` is forced); any * other value is JSON-stringified and sent with `content-type: * application/json`. `undefined`/`null` sends no body. */ body?: unknown; /** Abort signal; aborting rejects the in-flight read with an `AbortError`. */ signal?: AbortSignal; /** * `fetch` implementation to use — pass SvelteKit's `load` fetch to stream * during SSR / to inherit its request context. * @default globalThis.fetch */ fetch?: typeof globalThis.fetch; } /** * Thrown by {@link streamSse} when the response is not usable: a non-2xx status, * or a 2xx response with no readable body. Carries the HTTP {@link status} and a * best-effort {@link body} text (raw — the consumer decides how to interpret it, * e.g. extract a JSON `message`). */ export declare class SseRequestError extends Error { /** HTTP status of the failing response. */ readonly status: number; /** Best-effort response body text (`''` when there was nothing to read). */ readonly body: string; constructor(status: number, body: string, message?: string); } /** * Stream a POST (or other-method) endpoint that answers `text/event-stream` and * yield each parsed {@link SseEvent} as it is dispatched. * * The generator does not resolve until the server closes the stream (or it is * aborted / the consumer `break`s). On teardown — normal completion, early * `break`, `throw`, or abort — the underlying body reader is cancelled in a * `finally`, so a `break` out of the `for await` closes the HTTP connection * rather than leaking it. An abort propagates as an `AbortError`; it is not * swallowed. * * @param url - Endpoint to stream from. * @param options - Method, headers, body, abort signal, injectable `fetch`. * @returns An async generator of {@link SseEvent}s. * @throws {SseRequestError} when the response is non-2xx or has no body. * @example * ```typescript * import { streamSse } from '@urbicon-ui/sveltekit-utils/sse'; * * const controller = new AbortController(); * for await (const ev of streamSse('/api/chat', { * body: { messages }, * signal: controller.signal * })) { * if (ev.event === 'token') appendToken(JSON.parse(ev.data).text); * else if (ev.event === 'error') throw new Error(JSON.parse(ev.data).message); * } * ``` */ export declare function streamSse(url: string, options?: StreamSseOptions): AsyncGenerator;