learning_ai_common_plat/packages/fastify-sse/src/plugin.ts

62 lines
1.8 KiB
TypeScript

/**
* Fastify plugin that registers an SSE endpoint.
* Product backends configure the path and optional auth check.
*/
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { SSEHub } from './hub.js';
export interface SSEClient {
id: string;
userId?: string;
}
export interface SSEPluginOptions {
/** Route path for the SSE endpoint (default: /events/stream) */
path?: string;
/** Extract userId from request (optional, enables per-user targeting) */
getUserId?: (req: FastifyRequest) => string | undefined;
/** Heartbeat interval in ms (default: 30000, set 0 to disable) */
heartbeatIntervalMs?: number;
/** The SSE hub instance to use (creates one if not provided) */
hub?: SSEHub;
}
export async function ssePlugin(
app: FastifyInstance,
options: SSEPluginOptions = {}
): Promise<void> {
const path = options.path ?? '/events/stream';
const heartbeatMs = options.heartbeatIntervalMs ?? 30_000;
const hub = options.hub ?? new SSEHub();
// Decorate app with the hub so routes can push events
if (!app.hasDecorator('sseHub')) {
app.decorate('sseHub', hub);
}
// Register SSE endpoint
app.get(path, async (req: FastifyRequest, reply: FastifyReply) => {
const userId = options.getUserId?.(req);
// Hijack the raw response for SSE streaming
const raw = reply.raw;
hub.addClient(raw, userId);
// Prevent Fastify from sending its own response
reply.hijack();
});
// Heartbeat timer
let heartbeatTimer: ReturnType<typeof setInterval> | undefined;
if (heartbeatMs > 0) {
heartbeatTimer = setInterval(() => hub.heartbeat(), heartbeatMs);
}
// Cleanup on close
app.addHook('onClose', async () => {
if (heartbeatTimer) clearInterval(heartbeatTimer);
hub.disconnectAll();
});
}