Realtime Streams
Realtime Streams: a worked guide from route registry through to a production build.
Example
Tie streams and sockets to the request signal so disconnects stop producers and release subscriptions.
router.get('/events', (context) => {
const stream = createEventStream({ signal: context.signal });
void (async () => {
for await (const event of projects.events(context.signal)) {
await stream.send({ event: 'project', data: event });
}
})();
return stream.response;
});Goal and architecture
Askr's server context gives you two real primitives for pushing data to the client: context.sse(options), which returns an EventStream for Server-Sent Events, and context.upgrade(handler) (backed by a WebSocketAdapter), which hands your WebSocketHandler a WebSocketLike socket with send() and close(). There's no framework-level pub/sub or realtime store on top of these — you pick SSE for one-way server-to-client feeds (progress updates, notifications) and WebSockets when the client needs to talk back, and you own the message format either way.
Implementation
For SSE, call context.sse() inside a route handler and write events to the returned EventStream; formatServerSentEvent and the ServerSentEvent type describe the wire format if you need to hand-craft frames. For WebSockets, declare upgrade on an ApiRoute or call context.upgrade(handler), where handler(socket, context) gets the live connection and the same ServerContext (so context.auth and context.params are already resolved). On the client, a plain stream() codec from @askrjs/fetch decodes a response as a ReadableStream<Uint8Array>, useful for consuming chunked or SSE responses through the typed client instead of a raw fetch.
Failure states
context.signal is an AbortSignal tied to the request lifecycle — check it in long-lived SSE loops so a closed connection stops your write loop instead of leaking a handler that writes to a dead stream. WebSocket handlers should treat socket.close(code, reason) as the normal teardown path and be defensive about send() after a close, since the browser can drop a connection without your handler being told first. If a realtime feed is supplementary rather than essential, fall back to invalidateOnInterval(prefix, { intervalMs }) polling for the same data — it's simpler to reason about and doesn't need connection-recovery logic.
Verification
Test SSE handlers by opening a real EventSource (or reading the raw response stream) against a test server and asserting on the events received in order, since the format is just newline-delimited text over an HTTP response. For WebSocket handlers, drive handler(socket, context) directly with a mock WebSocketLike in unit tests to assert on what gets sent for a given inbound message, then layer a smaller number of real-socket integration tests on top. Always assert that closing the client connection triggers cleanup on the server side — a common realtime bug is a handler that keeps a timer or subscription alive after the socket is gone.