Askr
Guides

Realtime Streams

Use realtime streams with the current published Askr packages.

  • @askrjs/askr0.0.59

How to use realtime streams

Tie streams and sockets to the request signal so disconnects stop producers and release subscriptions.

  1. Import the published @askrjs/askr entrypoint.
  2. Keep realtime streams configuration next to the component, route, or server composition root that owns it.
  3. Handle pending, unavailable, cancellation, and failure states, then verify the production path.
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.