62 lines
1.8 KiB
TypeScript
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();
|
|
});
|
|
}
|