@roots/bud-server
Version:
Development server for @roots/bud
126 lines (105 loc) • 2.39 kB
text/typescript
import type {Payload} from '@roots/bud-server/middleware/hot'
import type {
NextFunction,
NextHandleFunction,
} from '@roots/bud-support/express'
import type {IncomingMessage, ServerResponse} from 'node:http'
export interface HotEventStream {
close(): void
handler: NextHandleFunction
publish(payload: Payload): void
}
/**
* Response headers
*/
const headers = {
'Access-Control-Allow-Origin': `*`,
'Cache-Control': `no-cache, no-transform`,
'Content-Type': `text/event-stream;charset=utf-8`,
// While behind nginx, event stream should not be buffered:
// http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
'X-Accel-Buffering': `no`,
}
/**
* Update interval
*/
const updateInterval = 1000
/**
* Registered clients
*/
let clients = {}
/**
* Current client identifier
*/
let currentClientId = 0
/**
* Static fn to execute a callback on every registered client
*/
const tapClients = (fn: CallableFunction) =>
Object.values(clients)
.filter(Boolean)
.forEach(client => fn(client))
const pingClient = client => {
client.write(`{data: {"action": "ping"}\n\n`)
}
const closeClient = client => {
if (client.finished) return
client.end()
}
/**
* Hot Module Replacement event stream
*/
export class HotEventStream {
/**
* hmr interval Timer
*/
public interval: NodeJS.Timeout
/**
* Class constructor
*/
public constructor() {
this.interval = setInterval(
() => tapClients(pingClient),
updateInterval,
).unref()
}
/**
* Close stream
*/
public close() {
clearInterval(this.interval)
tapClients(closeClient)
clients = {}
}
/**
* Handle update message
*/
public handle(
req: IncomingMessage,
res: ServerResponse,
_next?: NextFunction,
) {
const id = currentClientId++
clients[id] = res
const isHttp1 = parseInt(req.httpVersion) === 1
if (isHttp1) {
req.socket.setKeepAlive(true)
Object.assign(headers, {Connection: `keep-alive`})
}
res.writeHead(200, headers)
res.write(`\n`)
req.on(`close`, () => {
if (!res.writableEnded) res.end()
delete clients[id]
})
}
/**
* Broadcast to clients
*/
public publish(payload: Partial<Payload>) {
if (!payload) return
tapClients(client => {
client.write(`data: ${JSON.stringify(payload)} \n\n`)
})
}
}