import { PassThrough } from 'node:stream' import type { HttpContext } from '@adonisjs/core/http' export interface SseChannel { /** * Sends one event to the browser. Ignored once the connection is closed. */ send(event: string, data: unknown): void /** * Registers a callback invoked once, when the browser disconnects or the * channel is closed by the server. */ onClose(callback: () => void): void close(): void } /** * Turns the response into a Server-Sent Events stream (used with * EventSource on the Angular side for the live metrics, logs and jobs). */ export function openSse({ response }: HttpContext): SseChannel { const stream = new PassThrough() const closeCallbacks: (() => void)[] = [] let closed = false response.header('Content-Type', 'text/event-stream; charset=utf-8') response.header('Cache-Control', 'no-cache, no-transform') response.header('Connection', 'keep-alive') response.header('X-Accel-Buffering', 'no') response.stream(stream) /** * Comments keep the connection open through Caddy and let the browser * know quickly that the stream is established. */ stream.write('retry: 5000\n\n') const heartbeat = setInterval(() => stream.write(': ping\n\n'), 25_000) const close = () => { if (closed) return closed = true clearInterval(heartbeat) for (const callback of closeCallbacks) callback() stream.end() } /** * "close" on the response (not the request) is emitted when the browser * goes away. */ response.response.on('close', close) return { send(event, data) { if (closed) return stream.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) }, onClose(callback) { closeCallbacks.push(callback) }, close, } }