69 lines
5.4 KiB
JavaScript
69 lines
5.4 KiB
JavaScript
import { createRouteBody } from '../products/serving/routes.js';
|
|
import { streamSSE } from 'hono/streaming';
|
|
import { z } from '@hono/zod-openapi';
|
|
import { appEvents } from '../events.js';
|
|
import { logger } from '../commons/logger.js';
|
|
export const getStreamRoute = createRouteBody('get', '/api/stream', ['System'], 'Stream System Events', 'Subscribe to real-time updates for categories, posts, and pages.', undefined, {
|
|
200: {
|
|
description: 'Event Stream',
|
|
content: {
|
|
'text/event-stream': {
|
|
schema: z.string()
|
|
}
|
|
}
|
|
}
|
|
}, true // public
|
|
);
|
|
// Track active connections
|
|
const connectedClients = new Set();
|
|
// Single listener for the entire application
|
|
const broadcastAppUpdate = async (event) => {
|
|
const payload = JSON.stringify(event);
|
|
for (const client of connectedClients) {
|
|
try {
|
|
await client.stream.writeSSE({
|
|
event: event.kind,
|
|
data: payload
|
|
});
|
|
}
|
|
catch (err) {
|
|
logger.error({ err, clientId: client.id }, 'Error broadcasting to stream');
|
|
// Client will be removed by the onAbort handler in the stream handler
|
|
}
|
|
}
|
|
};
|
|
// Subscribe once
|
|
appEvents.on('app-update', broadcastAppUpdate);
|
|
export const streamHandler = async (c) => {
|
|
return streamSSE(c, async (stream) => {
|
|
const id = crypto.randomUUID();
|
|
const client = { id, stream };
|
|
connectedClients.add(client);
|
|
// Send initial connection message
|
|
await stream.writeSSE({
|
|
event: 'connected',
|
|
data: JSON.stringify({ message: 'Connected to event stream', clientId: id })
|
|
});
|
|
// Keep connection alive & handle cleanup
|
|
let interval;
|
|
const heartbeatInterval = parseInt(process.env.STREAM_HEARTBEAT_INTERVAL_MS || '30000', 10);
|
|
// Send heartbeat to prevent timeouts
|
|
interval = setInterval(async () => {
|
|
try {
|
|
await stream.writeSSE({ event: 'ping', data: '' });
|
|
}
|
|
catch (e) {
|
|
// connection likely closed
|
|
}
|
|
}, heartbeatInterval);
|
|
// Wait until the stream is aborted
|
|
await new Promise((resolve) => {
|
|
stream.onAbort(() => {
|
|
connectedClients.delete(client);
|
|
clearInterval(interval);
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
};
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RyZWFtLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2VuZHBvaW50cy9zdHJlYW0udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBRWhFLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUMzQyxPQUFPLEVBQUUsQ0FBQyxFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDdEMsT0FBTyxFQUFFLFNBQVMsRUFBWSxNQUFNLGNBQWMsQ0FBQztBQUNuRCxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFFOUMsTUFBTSxDQUFDLE1BQU0sY0FBYyxHQUFHLGVBQWUsQ0FDekMsS0FBSyxFQUNMLGFBQWEsRUFDYixDQUFDLFFBQVEsQ0FBQyxFQUNWLHNCQUFzQixFQUN0QixrRUFBa0UsRUFDbEUsU0FBUyxFQUNUO0lBQ0ksR0FBRyxFQUFFO1FBQ0QsV0FBVyxFQUFFLGNBQWM7UUFDM0IsT0FBTyxFQUFFO1lBQ0wsbUJBQW1CLEVBQUU7Z0JBQ2pCLE1BQU0sRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFO2FBQ3JCO1NBQ0o7S0FDSjtDQUNKLEVBQ0QsSUFBSSxDQUFDLFNBQVM7Q0FDakIsQ0FBQztBQUVGLDJCQUEyQjtBQUMzQixNQUFNLGdCQUFnQixHQUFHLElBQUksR0FBRyxFQUc1QixDQUFDO0FBRUwsNkNBQTZDO0FBQzdDLE1BQU0sa0JBQWtCLEdBQUcsS0FBSyxFQUFFLEtBQWUsRUFBRSxFQUFFO0lBQ2pELE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDdEMsS0FBSyxNQUFNLE1BQU0sSUFBSSxnQkFBZ0IsRUFBRSxDQUFDO1FBQ3BDLElBQUksQ0FBQztZQUNELE1BQU0sTUFBTSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUM7Z0JBQ3pCLEtBQUssRUFBRSxLQUFLLENBQUMsSUFBSTtnQkFDakIsSUFBSSxFQUFFLE9BQU87YUFDaEIsQ0FBQyxDQUFDO1FBQ1AsQ0FBQztRQUFDLE9BQU8sR0FBRyxFQUFFLENBQUM7WUFDWCxNQUFNLENBQUMsS0FBSyxDQUFDLEVBQUUsR0FBRyxFQUFFLFFBQVEsRUFBRSxNQUFNLENBQUMsRUFBRSxFQUFFLEVBQUUsOEJBQThCLENBQUMsQ0FBQztZQUMzRSxzRUFBc0U7UUFDMUUsQ0FBQztJQUNMLENBQUM7QUFDTCxDQUFDLENBQUM7QUFFRixpQkFBaUI7QUFDakIsU0FBUyxDQUFDLEVBQUUsQ0FBQyxZQUFZLEVBQUUsa0JBQWtCLENBQUMsQ0FBQztBQUUvQyxNQUFNLENBQUMsTUFBTSxhQUFhLEdBQUcsS0FBSyxFQUFFLENBQVUsRUFBRSxFQUFFO0lBQzlDLE9BQU8sU0FBUyxDQUFDLENBQUMsRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDakMsTUFBTSxFQUFFLEdBQUcsTUFBTSxDQUFDLFVBQVUsRUFBRSxDQUFDO1FBQy9CLE1BQU0sTUFBTSxHQUFHLEVBQUUsRUFBRSxFQUFFLE1BQU0sRUFBRSxDQUFDO1FBRTlCLGdCQUFnQixDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQztRQUM3QixrQ0FBa0M7UUFDbEMsTUFBTSxNQUFNLENBQUMsUUFBUSxDQUFDO1lBQ2xCLEtBQUssRUFBRSxXQUFXO1lBQ2xCLElBQUksRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLEVBQUUsT0FBTyxFQUFFLDJCQUEyQixFQUFFLFFBQVEsRUFBRSxFQUFFLEVBQUUsQ0FBQztTQUMvRSxDQUFDLENBQUM7UUFFSCx5Q0FBeUM7UUFDekMsSUFBSSxRQUF3QixDQUFDO1FBQzdCLE1BQU0saUJBQWlCLEdBQUcsUUFBUSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsNEJBQTRCLElBQUksT0FBTyxFQUFFLEVBQUUsQ0FBQyxDQUFDO1FBRTVGLHFDQUFxQztRQUNyQyxRQUFRLEdBQUcsV0FBVyxDQUFDLEtBQUssSUFBSSxFQUFFO1lBQzlCLElBQUksQ0FBQztnQkFDRCxNQUFNLE1BQU0sQ0FBQyxRQUFRLENBQUMsRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxFQUFFLEVBQUUsQ0FBQyxDQUFDO1lBQ3ZELENBQUM7WUFBQyxPQUFPLENBQUMsRUFBRSxDQUFDO2dCQUNULDJCQUEyQjtZQUMvQixDQUFDO1FBQ0wsQ0FBQyxFQUFFLGlCQUFpQixDQUFDLENBQUM7UUFFdEIsbUNBQW1DO1FBQ25DLE1BQU0sSUFBSSxPQUFPLENBQU8sQ0FBQyxPQUFPLEVBQUUsRUFBRTtZQUNoQyxNQUFNLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRTtnQkFDaEIsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDO2dCQUNoQyxhQUFhLENBQUMsUUFBUSxDQUFDLENBQUM7Z0JBQ3hCLE9BQU8sRUFBRSxDQUFDO1lBQ2QsQ0FBQyxDQUFDLENBQUM7UUFDUCxDQUFDLENBQUMsQ0FBQztJQUNQLENBQUMsQ0FBQyxDQUFDO0FBQ1AsQ0FBQyxDQUFDIn0=
|