Real-time
Live updates over Server-Sent Events.
A pub/sub createBroadcaster hub fans messages to per-channel subscribers; an h3
sseHandler streams a caller's channels to the browser with the correct SSE headers and
keep-alive; and apexRealtimeClient wraps the native EventSource so the
client is a few lines with automatic reconnect.
The broadcaster #
createBroadcaster() is an in-process pub/sub hub. subscribe returns an
unsubscribe function; publish fans an (event, data) pair to every
subscriber of that channel — and only that channel. A throwing listener is isolated from the rest
of the fan-out.
import { createBroadcaster } from '@apex-stack/core/server'
const hub = createBroadcaster()
const off = hub.subscribe('room:1', (event, data) => console.log(event, data))
hub.publish('room:1', 'msg', { text: 'hi' }) // listener sees ('msg', { text: 'hi' })
off() // stop delivery (idempotent)
The SSE endpoint #
sseHandler(broadcaster, opts) is an h3 handler. It sets
Content-Type: text/event-stream (plus no-cache and keep-alive), opens
with a comment frame, then streams every publish on the caller's channels as an
event/data frame. Cancelling the stream unsubscribes and closes — no leaks.
import { createApp } from 'h3'
import { createBroadcaster, sseHandler } from '@apex-stack/core/server'
const hub = createBroadcaster()
const app = createApp()
app.use('/events', sseHandler(hub, {
channels: (event) => ['room:1'], // which channels this caller receives
keepAliveMs: 15000, // periodic comment to hold the connection open
}))
// Anywhere on the server, push to open streams:
hub.publish('room:1', 'greet', { hi: true })
The browser client #
apexRealtimeClient(url, { onEvent }) wraps the native EventSource. It
best-effort JSON-decodes each payload (falling back to the raw string), the browser reconnects
automatically on a transient drop, and close() tears the connection down.
import { apexRealtimeClient } from '@apex-stack/core/server'
const rt = apexRealtimeClient('/events', {
onEvent: (event, data) => console.log(event, data),
events: ['greet'], // extra named events beyond the default 'message' stream
onOpen: () => console.log('connected'),
})
// later:
rt.close()
The client is browser-only — it uses the DOM EventSource and is never imported by a
server module, so it can't pull DOM globals into the server bundle.
The wire format #
Under the hood, frames are plain SSE text. encodeSseFrame emits
id/event/retry before data, splits multi-line
data into one data: line each, and strips CR/LF from single-line fields so a value can
never inject a new frame.
import { encodeSseFrame } from '@apex-stack/core/server'
encodeSseFrame({ event: 'x', data: JSON.stringify({ a: 1 }) })
// → 'event: x\ndata: {"a":1}\n\n'