Files
2026-10-12 14:25:22 +02:00

66 lines
1.7 KiB
TypeScript

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,
}
}