UNPKG

opinionated-machine

Version:

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

191 lines (190 loc) 8.49 kB
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;