opinionated-machine
Version:
Very opinionated DI framework for fastify, built on top of awilix
191 lines (190 loc) • 8.49 kB
TypeScript
import type { SSEReplyInterface } from '@fastify/sse';
import type { FastifyReply, FastifyRequest } from 'fastify';
import type { z } from 'zod';
import type { DualModeType } from '../dualmode/dualModeTypes.ts';
import type { SSERoomManager } from '../sse/rooms/SSERoomManager.ts';
import type { SSEEventSchemas, SSELogger, SSEMessage } from '../sse/sseTypes.ts';
import type { SSEContext, SSESession, SSESessionMode } from './fastifyRouteTypes.ts';
/**
* FastifyReply extended with SSE capabilities from @fastify/sse.
*/
export type SSEReply = FastifyReply & {
sse: SSEReplyInterface;
};
/**
* Minimal interface for SSE controller methods used by route utilities.
* This allows shared utilities to work with both SSE and dual-mode controllers.
*/
export type SSEControllerLike = {
_sendEventRaw(connectionId: string, message: SSEMessage): Promise<boolean>;
registerConnection(connection: SSESession): void;
unregisterConnection(connectionId: string): void;
/** Room manager, if rooms are enabled */
_internalRoomManager?: SSERoomManager;
};
/**
* Reason why the SSE connection was closed.
* - 'server': Server explicitly called closeConnection() or returned success('disconnect')
* - 'client': Client closed the connection (EventSource.close(), navigated away, etc.)
*/
export type SSECloseReason = 'server' | 'client';
/**
* Options for SSE connection lifecycle hooks.
*/
export type SSELifecycleOptions<TConnection = SSESession> = {
onConnect?: (connection: TConnection) => void | Promise<void>;
/**
* Called when the SSE connection closes for any reason (client disconnect,
* network failure, or server explicitly closing via closeConnection()).
*
* @param connection - The connection that was closed
* @param reason - Why the connection was closed:
* - 'server': Server explicitly closed (closeConnection() or success('disconnect'))
* - 'client': Client closed (EventSource.close(), navigation, network failure)
*
* Use this for cleanup like unsubscribing from events or removing from tracking.
*/
onClose?: (connection: TConnection, reason: SSECloseReason) => void | Promise<void>;
onReconnect?: (connection: TConnection, lastEventId: string) => Iterable<SSEMessage> | AsyncIterable<SSEMessage> | void | Promise<void>;
logger?: SSELogger;
};
/**
* Extract Fastify path template from pathResolver.
*
* This function creates placeholder params with ':paramName' values and calls
* the pathResolver to generate a Fastify-compatible path template.
*
* @example
* ```typescript
* // pathResolver: (p) => `/users/${p.userId}/posts/${p.postId}`
* // paramsSchema: z.object({ userId: z.string(), postId: z.string() })
* // Result: '/users/:userId/posts/:postId'
* ```
*/
export declare function extractPathTemplate<Params>(pathResolver: (params: Params) => string, paramsSchema: z.ZodObject<z.ZodRawShape>): string;
/**
* Check if an error has a valid httpStatusCode property (like PublicNonRecoverableError).
* Uses duck typing instead of instanceof for reliability across realms.
* Validates the status code is a finite integer within valid HTTP range (100-599).
*/
export declare function hasHttpStatusCode(err: unknown): err is {
httpStatusCode: number;
};
/**
* Send replay events from either sync or async iterables.
*/
export declare function sendReplayEvents(sseReply: SSEReply, replayEvents: Iterable<SSEMessage> | AsyncIterable<SSEMessage>): Promise<void>;
/**
* Handle Last-Event-ID reconnection by replaying missed events.
*/
export declare function handleReconnection(sseReply: SSEReply, connection: SSESession, lastEventId: string, options: SSELifecycleOptions | undefined, logPrefix?: string): Promise<void>;
/**
* Send error event to client and close connection gracefully.
*/
export declare function handleSSEError(sseReply: SSEReply, controller: SSEControllerLike, connectionId: string, err: unknown, logger?: SSELogger): Promise<void>;
/**
* Result of setting up an SSE connection.
*/
export type SSESessionSetupResult<Events extends SSEEventSchemas = SSEEventSchemas> = {
connectionId: string;
connection: SSESession<Events>;
connectionClosed: Promise<void>;
sseReply: SSEReply;
};
/**
* Result of creating an SSE context.
*/
export type SSEContextResult<Events extends SSEEventSchemas = SSEEventSchemas> = {
sseContext: SSEContext<Events>;
/** Promise that resolves when client disconnects */
connectionClosed: Promise<void>;
/** The SSE reply object for advanced operations */
sseReply: SSEReply;
/** Get the connection if streaming was started */
getConnection: () => SSESession<Events> | undefined;
/** Get the connection ID if streaming was started */
getConnectionId: () => string | undefined;
/** Check if streaming was started */
isStarted: () => boolean;
/** Check if a response was sent via sse.respond() */
hasResponse: () => boolean;
/** Get the response data if sse.respond() was called */
getResponseData: () => {
code: number;
body: unknown;
} | undefined;
/** Get the session mode if streaming was started */
getMode: () => SSESessionMode | undefined;
};
/**
* Create an SSEContext for deferred header sending.
*
* This factory creates the `sse` parameter passed to SSE handlers, allowing:
* - Validation before headers are sent
* - Proper HTTP error responses (404, 422, etc.)
* - Explicit streaming start via `sse.start()`
*
* @param controller - The SSE controller for connection management
* @param request - The Fastify request
* @param reply - The Fastify reply
* @param eventSchemas - Event schemas for type-safe event sending
* @param options - Lifecycle hooks and options
* @param logPrefix - Prefix for log messages
*
* @returns SSEContext result with context object and state accessors
*/
export declare function createSSEContext<Events extends SSEEventSchemas>(controller: SSEControllerLike, request: FastifyRequest, reply: FastifyReply, eventSchemas: Events, options: SSELifecycleOptions | undefined, logPrefix?: string): SSEContextResult<Events>;
/**
* Setup an SSE connection with all the boilerplate:
* - Create connection object with typed event sender
* - Register with controller
* - Setup disconnect handler
* - Initialize SSE reply (keepAlive, sendHeaders, flushHeaders)
* - Handle reconnection
* - Call onConnect hook
*
* @deprecated Use createSSEContext for new code. This function is kept for backwards compatibility.
*
* @returns Connection setup result with connection object and closed promise
*/
export declare function setupSSESession<Events extends SSEEventSchemas>(controller: SSEControllerLike, request: FastifyRequest, reply: FastifyReply, eventSchemas: Events, options: SSELifecycleOptions | undefined, logPrefix?: string): Promise<SSESessionSetupResult<Events>>;
/**
* Determine response mode from Accept header.
*
* Parses the Accept header and determines whether to use JSON or SSE mode.
* Supports quality values (q=) for content negotiation.
*
* @param accept - The Accept header value
* @param defaultMode - Mode to use when no preference is specified
* @returns The determined response mode
*/
export declare function determineMode(accept: string | undefined, defaultMode?: DualModeType): DualModeType;
/**
* Result of sync format determination.
*/
export type SyncFormatResult = {
mode: 'sse';
} | {
mode: 'sync';
contentType: string;
};
/**
* Determine sync format from Accept header for content negotiation.
*
* Parses the Accept header and determines which format to use.
* Supports quality values (q=) for content negotiation and subtype wildcards
* (e.g., "application/*", "text/*").
*
* Matching priority:
* 1. text/event-stream (SSE mode)
* 2. Exact matches against supportedFormats
* 3. Subtype wildcards (e.g., "text/*" matches first "text/..." in supportedFormats)
* 4. Full wildcard (*\/*) uses fallback format
* 5. Fallback to defaultFormat or first supported format
*
* @param accept - The Accept header value
* @param supportedFormats - Array of Content-Types that the route supports
* @param defaultFormat - Format to use when no preference is specified (default: first supported format)
* @returns The determined format or 'sse' mode indicator
*/
export declare function determineSyncFormat(accept: string | undefined, supportedFormats: string[], defaultFormat?: string): SyncFormatResult;