opinionated-machine
Version:
Very opinionated DI framework for fastify, built on top of awilix
204 lines (203 loc) • 7.88 kB
TypeScript
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;
}