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'