UNPKG

opinionated-machine

Version:

Very opinionated DI framework for fastify, built on top of awilix

204 lines (203 loc) 7.88 kB
import type { SSESession } from '../routes/fastifyRouteTypes.ts'; import type { SSESessionSpy } from '../sse/SSESessionSpy.ts'; import { type ParsedSSEEvent } from '../sse/sseParser.ts'; /** * Interface for objects that have a sessionSpy (e.g., SSE controllers in test mode). */ export type HasSessionSpy = { connectionSpy: SSESessionSpy; }; /** * Options for connecting to an SSE endpoint via HTTP. */ export type SSEHttpConnectOptions = { /** Query parameters to add to the URL */ query?: Record<string, string | undefined>; /** Additional headers to send with the request */ headers?: Record<string, string>; }; /** * Options for connecting with automatic server-side connection waiting. */ export type SSEHttpConnectWithSpyOptions = SSEHttpConnectOptions & { /** * Wait for server-side connection registration after HTTP headers are received. * This eliminates the race condition between `connect()` returning and the * server-side handler completing connection registration. */ awaitServerConnection: { /** The SSE controller (must have connectionSpy enabled via isTestMode) */ controller: HasSessionSpy; /** Timeout in milliseconds (default: 5000) */ timeout?: number; }; }; /** * Result when connecting with awaitServerConnection option. */ export type SSEHttpConnectResult = { client: SSEHttpClient; serverConnection: SSESession; }; /** * SSE client for testing long-lived connections using real HTTP. * * This client uses the native `fetch()` API to establish a real HTTP connection * to an SSE endpoint. Events are streamed incrementally as the server sends them, * making it suitable for testing: * * - **Long-lived connections** that stay open indefinitely * - **Real-time notifications** where events arrive over time * - **Push-based streaming** where the client waits for server-initiated events * * **When to use SSEHttpClient vs SSEInjectClient:** * * | SSEHttpClient (this class) | SSEInjectClient | * |-------------------------------------|--------------------------------------| * | Real HTTP connection via fetch() | Fastify's inject() (no network) | * | Events arrive incrementally | All events returned at once | * | Connection can stay open | Response must complete | * | Requires running server (listen()) | Works without starting server | * | Use for: notifications, chat, feeds | Use for: OpenAI-style streaming | * * @example * ```typescript * // 1. Start a real HTTP server * await app.listen({ port: 0 }) * const address = app.server.address() as { port: number } * const baseUrl = `http://localhost:${address.port}` * * // 2. Connect to SSE endpoint (returns when headers are received) * const client = await SSEHttpClient.connect(baseUrl, '/api/notifications', { * headers: { authorization: 'Bearer token' }, * }) * * // 3. Server can now send events at any time * controller.sendEvent(connectionId, { event: 'notification', data: { msg: 'Hello' } }) * * // 4. Collect events as they arrive * const events = await client.collectEvents(3) // wait for 3 events * // or: collect until a specific event * const events = await client.collectEvents(e => e.event === 'done') * * // 5. Alternative: use async iterator for manual control * for await (const event of client.events()) { * console.log('Received:', event.event, event.data) * if (event.event === 'done') break * } * * // 6. Cleanup * client.close() * await app.close() * ``` */ export declare class SSEHttpClient { /** The fetch Response object. Available immediately after connect() returns. */ readonly response: Response; private readonly abortController; private readonly reader; private readonly decoder; private buffer; private closed; private constructor(); /** * Connect to an SSE endpoint. * * The returned promise resolves as soon as HTTP headers are received, * indicating the connection is established. Events can then be consumed * via `events()` or `collectEvents()`. * * @param baseUrl - Base URL of the server (e.g., 'http://localhost:3000') * @param path - SSE endpoint path (e.g., '/api/notifications') * @param options - Connection options (query params, headers) * @returns Connected SSE client ready to receive events * * @example * ```typescript * // Basic connection (returns when HTTP headers received) * const client = await SSEHttpClient.connect( * 'http://localhost:3000', * '/api/stream', * { query: { userId: '123' }, headers: { authorization: 'Bearer token' } } * ) * * // With awaitServerConnection (waits for server-side registration) * const { client, serverConnection } = await SSEHttpClient.connect( * 'http://localhost:3000', * '/api/stream', * { awaitServerConnection: { controller } } * ) * // serverConnection is ready to use immediately * await controller.sendEvent(serverConnection.id, { event: 'test', data: {} }) * ``` */ static connect(baseUrl: string, path: string, options: SSEHttpConnectWithSpyOptions): Promise<SSEHttpConnectResult>; static connect(baseUrl: string, path: string, options?: SSEHttpConnectOptions): Promise<SSEHttpClient>; /** * Async generator that yields parsed SSE events as they arrive. * * Use this for full control over event processing. The generator * completes when the server closes the connection or the abort signal fires. * * @param signal - Optional AbortSignal to stop the generator early * * @example * ```typescript * for await (const event of client.events()) { * const data = JSON.parse(event.data) * console.log(`[${event.event}]`, data) * * if (event.event === 'done') { * break // Stop consuming, connection stays open until close() * } * } * ``` * * @example * ```typescript * // With abort signal for timeout control * const controller = new AbortController() * setTimeout(() => controller.abort(), 5000) * * for await (const event of client.events(controller.signal)) { * console.log(event) * } * ``` */ events(signal?: AbortSignal): AsyncGenerator<ParsedSSEEvent, void, unknown>; /** * Read from the stream with abort signal support. * Returns 'aborted' if the signal fires before read completes. */ private readWithAbort; /** * Collect events until a count is reached or predicate returns true. * * @param countOrPredicate - Either a number of events to collect, * or a predicate function that returns true when collection should stop. * The event that matches the predicate IS included in the result. * @param timeout - Maximum time to wait in milliseconds (default: 5000) * @returns Array of collected events * @throws Error if timeout is reached before condition is met * * @example * ```typescript * // Collect exactly 5 events * const events = await client.collectEvents(5) * * // Collect until 'done' event is received * const events = await client.collectEvents(e => e.event === 'done') * * // Collect with custom timeout * const events = await client.collectEvents(10, 30000) // 30s timeout * ``` */ collectEvents(countOrPredicate: number | ((event: ParsedSSEEvent) => boolean), timeout?: number): Promise<ParsedSSEEvent[]>; /** * Close the connection from the client side. * * This aborts the underlying fetch request. Call this when done * consuming events to clean up resources. */ close(): void; }