UNPKG

opinionated-machine

Version:

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

174 lines (173 loc) 6.52 kB
import { type ParsedSSEEvent } from '../sse/sseParser.ts'; import type { AnyFastifyInstance } from './AnyFastifyInstance.ts'; import type { SSEConnectOptions, SSETestConnection } from './sseTestTypes.ts'; /** * Response from a Fastify inject() call for SSE. */ export type SSEInjectResponse = { statusCode: number; headers: Record<string, string | string[] | undefined>; body: string; }; /** * SSE connection object returned by SSEInjectClient. * * Represents a completed SSE response from Fastify's inject(). * Since inject() waits for the complete response, all events * are available immediately after construction. */ export declare class SSEInjectConnection implements SSETestConnection { private readonly receivedEvents; private readonly response; constructor(response: SSEInjectResponse); /** * Wait for a specific event by name. * Since inject() returns the complete response, this searches * the already-received events. */ waitForEvent(eventName: string, timeout?: number): Promise<ParsedSSEEvent>; /** * Wait for a specific number of events. * Since inject() returns the complete response, this checks * the already-received events. */ waitForEvents(count: number, timeout?: number): Promise<ParsedSSEEvent[]>; /** * Get all events received in the response. */ getReceivedEvents(): ParsedSSEEvent[]; /** * Close the connection. No-op for inject connections since * the response is already complete. */ close(): void; /** * Check if the connection has been closed. * Always returns true for inject connections since response is complete. */ isClosed(): boolean; /** * Get the HTTP status code from the response. */ getStatusCode(): number; /** * Get the response headers. */ getHeaders(): Record<string, string | string[] | undefined>; } /** * SSE client using Fastify's inject() for testing SSE endpoints. * * This client uses Fastify's `inject()` method which simulates HTTP requests * without network overhead. The key characteristic is that `inject()` waits * for the **complete response** before returning, meaning: * * - All events are available immediately after connect() returns * - The SSE handler must close the connection for connect() to complete * - Best suited for SSE streams that have a defined end * * **Ideal for testing:** * - OpenAI-style streaming (POST with body, streams tokens, then closes) * - Short-lived streams that complete after sending all events * - Endpoints where you want to test the full response at once * * **When to use SSEInjectClient vs SSEHttpClient:** * * | SSEInjectClient (this class) | SSEHttpClient | * |-------------------------------------|--------------------------------------| * | Fastify's inject() (no network) | Real HTTP connection via fetch() | * | All events returned at once | Events arrive incrementally | * | Handler must close the connection | Connection can stay open | * | Works without starting server | Requires running server (listen()) | * | Use for: OpenAI-style, completions | Use for: notifications, chat, feeds | * * @example * ```typescript * // Testing OpenAI-style chat completion streaming * const client = new SSEInjectClient(app) * * // POST request that streams response and closes * const conn = await client.connectWithBody( * '/api/chat/completions', * { model: 'gpt-4', messages: [{ role: 'user', content: 'Hello' }], stream: true } * ) * * // connect() returns after handler closes - all events are available * expect(conn.getStatusCode()).toBe(200) * * // Get all events that were streamed * const events = conn.getReceivedEvents() * expect(events[events.length - 1].event).toBe('done') * * // Parse the streamed content * const chunks = events * .filter(e => e.event === 'chunk') * .map(e => JSON.parse(e.data).content) * const fullResponse = chunks.join('') * ``` * * @example * ```typescript * // Testing GET SSE endpoint * const client = new SSEInjectClient(app) * const conn = await client.connect('/api/export/progress', { * headers: { authorization: 'Bearer token' } * }) * * // Wait for specific event type * const completeEvent = await conn.waitForEvent('complete') * expect(JSON.parse(completeEvent.data)).toMatchObject({ status: 'success' }) * ``` */ export declare class SSEInjectClient { private readonly app; /** * Create a new SSE inject client. * @param app - Fastify instance (does not need to be listening) */ constructor(app: AnyFastifyInstance); /** * Send a GET request to an SSE endpoint. * * Returns when the SSE handler closes the connection. * All events are then available via getReceivedEvents(). * * @param url - The endpoint URL (e.g., '/api/stream') * @param options - Optional headers * @returns Connection object with all received events * * @example * ```typescript * const conn = await client.connect('/api/notifications/stream', { * headers: { authorization: 'Bearer token' } * }) * const events = conn.getReceivedEvents() * ``` */ connect(url: string, options?: Omit<SSEConnectOptions, 'method' | 'body'>): Promise<SSEInjectConnection>; /** * Send a POST/PUT/PATCH request to an SSE endpoint with a body. * * This is the typical pattern for OpenAI-style streaming APIs where * you send a request body and receive a streamed response. * * Returns when the SSE handler closes the connection. * All events are then available via getReceivedEvents(). * * @param url - The endpoint URL (e.g., '/api/chat/completions') * @param body - Request body (will be JSON stringified) * @param options - Optional method (defaults to POST) and headers * @returns Connection object with all received events * * @example * ```typescript * const conn = await client.connectWithBody( * '/api/chat/completions', * { model: 'gpt-4', messages: [...], stream: true }, * { headers: { authorization: 'Bearer sk-...' } } * ) * const chunks = conn.getReceivedEvents().filter(e => e.event === 'chunk') * ``` */ connectWithBody(url: string, body: unknown, options?: Omit<SSEConnectOptions, 'body'>): Promise<SSEInjectConnection>; }