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