export type SseTestStreamOptions = { /** Milliseconds to wait before enqueueing each chunk after the first. */ chunkDelayMs?: number; /** * When true, closes the body as soon as all initial chunks have been sent. * Use this to simulate a server that sends events and then ends the stream. */ closeWhenDone?: boolean; }; /** * Test double for an SSE HTTP response body. * * Enqueues fixture chunks in order and stays open until {@link close} is called, * matching real servers that keep the connection alive after each event's trailing * `\n\n` frame boundary. */ export class SseTestStream { readonly stream: ReadableStream; private controller: ReadableStreamDefaultController | null = null; private closed = false; /** * @param chunks - Fixture `raw` strings, whole or split, to simulate chunk boundaries. * @param options - Delivery timing and optional auto-close after the initial chunks. */ constructor(chunks: string[], options: SseTestStreamOptions = {}) { const { chunkDelayMs = 0, closeWhenDone = false } = options; const encoder = new TextEncoder(); this.stream = new ReadableStream({ start: async (controller): Promise => { this.controller = controller; for (let i = 0; i < chunks.length; i++) { if (chunkDelayMs > 0 && i > 0) { await new Promise((resolve) => setTimeout(resolve, chunkDelayMs)); } controller.enqueue(encoder.encode(chunks[i]!)); } if (closeWhenDone) { this.close(); } }, }); } /** * Ends the HTTP body the way a server closing the SSE connection would. */ close(): void { this.closed = true; this.controller?.close(); } }