UNPKG

assistant-stream

Version:

Streaming utilities for AI assistants

101 lines (92 loc) • 2.75 kB
import { SSEEventDecoderStream, type PipelineSSEEvent, } from "./SSEEventDecoderStream"; export const SSE_HEADERS = { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }; type SSEJsonDoneOptions = { marker: string; onMissing?: () => void; }; type SSEJsonDecoderOptions<T> = { parse: ( data: string, controller: TransformStreamDefaultController<T>, ) => void; done?: SSEJsonDoneOptions; maxLineLength?: number | undefined; maxEventLength?: number | undefined; } & ( | { strict?: true } | { strict: false; onUnknownEvent?: (event: string) => void; } ); export const createSSEJsonEncoder = <T>(doneMarker?: string) => (readable: ReadableStream<T>) => readable .pipeThrough( new TransformStream<T, string>({ transform(chunk, controller) { controller.enqueue(`data: ${JSON.stringify(chunk)}\n\n`); }, flush(controller) { if (doneMarker !== undefined) { controller.enqueue(`data: ${doneMarker}\n\n`); } }, }), ) .pipeThrough(new TextEncoderStream()); export const createSSEJsonDecoder = <T>(options: SSEJsonDecoderOptions<T>) => (readable: ReadableStream<Uint8Array<ArrayBuffer>>) => { let receivedDone = false; const done = options.done; // Let the SSE parser consume the single permitted leading BOM. return readable .pipeThrough(new TextDecoderStream("utf-8", { ignoreBOM: true })) .pipeThrough( new SSEEventDecoderStream({ maxLineLength: options.maxLineLength, maxEventLength: options.maxEventLength, stopAfter: done ? (event) => (event.event || "message") === "message" && event.data === done.marker : undefined, }), ) .pipeThrough( new TransformStream<PipelineSSEEvent, T>({ transform(event, controller) { if (event.event !== "message") { if (options.strict !== false) { throw new Error(`Unknown SSE event type: ${event.event}`); } options.onUnknownEvent?.(event.event); return; } if ( options.done !== undefined && event.data === options.done.marker ) { receivedDone = true; controller.terminate(); return; } options.parse(event.data, controller); }, flush() { if (options.done !== undefined && !receivedDone) { options.done.onMissing?.(); } }, }), ); };