66 lines
1.7 KiB
TypeScript
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,
|
|
}
|
|
}
|