opinionated-machine
Version:
Very opinionated DI framework for fastify, built on top of awilix
94 lines (93 loc) • 3.8 kB
TypeScript
import type { AllContractEventNames, AnySSEContractDefinition, ExtractEventSchema, SSEEventSchemas } from '@lokalise/api-contracts';
import type { z } from 'zod';
import type { SSERoomBroadcaster } from './rooms/SSERoomBroadcaster.ts';
export type { SSEEventSchemas, AllContractEventNames, ExtractEventSchema };
/**
* Flatten all events from all contracts into a single record.
* Used for type-safe event sending across all controller routes.
*/
export type AllContractEvents<Contracts extends Record<string, AnySSEContractDefinition>> = {
[EventName in AllContractEventNames<Contracts>]: ExtractEventSchema<Contracts, EventName>;
};
/**
* Minimal logger interface for SSE route error handling.
* Compatible with CommonLogger from @lokalise/node-core and pino loggers.
*/
export type SSELogger = {
error: (obj: Record<string, unknown>, msg: string) => void;
warn?: (obj: Record<string, unknown>, msg: string) => void;
};
/**
* SSE message format compatible with @fastify/sse.
*
* By default, @fastify/sse JSON-serializes the data field, supporting both objects
* and primitive values (strings, numbers, booleans). This enables patterns like
* OpenAI's streaming API where JSON object chunks are followed by a string terminator.
*
* The @fastify/sse plugin allows customizing the serialization step via its
* configuration. For example, you can configure it to send strings raw (without
* JSON encoding) if your use case requires exact wire format control.
*
* @template T - Type of the event data (objects or primitives)
*
* @example
* ```typescript
* // Object data (common case)
* await sendEvent(id, { event: 'chunk', data: { content: 'Hello' } })
*
* // String data (e.g., OpenAI-style terminator)
* await sendEvent(id, { event: 'done', data: '[DONE]' })
* ```
*/
export type SSEMessage<T = unknown> = {
/** Event name (maps to EventSource 'event' field) */
event?: string;
/** Event data - objects or primitives, serialized per @fastify/sse config */
data: T;
/** Event ID for client reconnection via Last-Event-ID */
id?: string;
/** Reconnection delay hint in milliseconds */
retry?: number;
};
/**
* Type-safe event sender for SSE connections.
*
* This type provides compile-time type checking for event names and their
* corresponding data payloads based on the contract's event schemas.
*
* @template Events - Map of event name to Zod schema (from contract.serverSentEventSchemas)
*
* @example
* ```typescript
* // Given a contract with serverSentEventSchemas:
* // serverSentEventSchemas: { chunk: z.object({ content: z.string() }), done: z.object({ tokens: z.number() }) }
*
* // The sender will be typed as:
* send('chunk', { content: 'hello' }) // OK
* send('done', { tokens: 5 }) // OK
* send('chunk', { tokens: 5 }) // TS Error: wrong payload for 'chunk'
* send('invalid', { }) // TS Error: 'invalid' is not a valid event name
* ```
*/
export type SSEEventSender<Events extends SSEEventSchemas> = <EventName extends keyof Events & string>(eventName: EventName, data: z.input<Events[EventName]>, options?: {
id?: string;
retry?: number;
}) => Promise<boolean>;
/**
* Configuration options for SSE controllers.
*/
export type SSEControllerConfig = {
/**
* Enable connection spying for testing.
* When enabled, the controller tracks connections and allows waiting for them.
* Only enable this in test environments.
* @default false
*/
enableConnectionSpy?: boolean;
/**
* Shared room broadcaster from DI.
* When provided, the controller registers its sendEvent with the broadcaster
* and uses the broadcaster's room manager for room operations.
*/
roomBroadcaster?: SSERoomBroadcaster;
};