@flamefrontend/sse-runtime-core
v0.10.0
Published
Framework-agnostic runtime for the Server-Sent Events lifecycle: typed events, reconnect with backoff, Last-Event-ID resumption, auth refresh, and connection status.
Maintainers
Readme
@flamefrontend/sse-runtime-core
Framework-agnostic TypeScript runtime for Server-Sent Events clients.
It provides typed event dispatch, reconnects with jittered backoff,
Last-Event-ID resumption, auth refresh on 401, connection status, cleanup,
and optional single-tab coordination across browser tabs.
Install
pnpm add @flamefrontend/sse-runtime-coreQuick Start
import { createSSEClient } from "@flamefrontend/sse-runtime-core";
type Events = {
message: {
id: string;
text: string;
};
};
const client = createSSEClient<Events>({
key: ["chat", "123"],
url: "/api/chats/123/stream",
events: {
message: (message) => {
console.log(message.text);
}
}
});
await client.connect();
client.disconnect();Event Format
id: 42
event: message
data: {"id":"42","text":"hello"}
JSON data: payloads are parsed before dispatch. Invalid JSON is delivered as
the raw string payload.
Client API
connect(): Promise<void>disconnect(): voidreconnect(): Promise<void>— force a fresh connection even when already open, resuming from the last event id; recovers a stream that looks open but has gone silentensureOpen(options?: { timeout?: number }): Promise<boolean>— wait until the stream is open; starts connecting if neededgetStatus(): SSEConnectionStatusgetError(): SSEError | nullgetLastEventAt(): number | undefined— timestamp of the most recent event; a staleness signal for custom watchdogssubscribeStatus(listener): () => voidsubscribeError(listener): () => voidsubscribeEvent(eventName, handler): () => voidsubscribeAnyEvent(handler): () => void— observe every event as{ type, data, raw }(rawis the unparseddatastring)getRole(): CoordinationRole | null/subscribeRole(listener): () => void— coordinated clients only; observe whether this tab is the"leader"or a"follower"
Core Options
key: stable stream identity, for example["chat", chatId].url: SSE endpoint URL.enabled: set tofalseto start idle. The client still opens when you callconnect();enabledonly controls auto-connect in the React adapter.headers: static or async headers, resolved before each connection attempt.credentials: passed tofetch, for example"include".events: typed event handlers registered at creation time.reconnect: backoff and retry limits.auth:401refresh callback.coordination: optional single-tab coordination.heartbeat: reconnect when the stream is silent for too long.openTimeout: abort the connection attempt if the server does not respond within this many ms.retry: custom per-errorshouldRetrypredicate andgetDelayfunction.diagnostics: structured observability callbacks (onOpen,onDisconnect,onRawEvent, etc.).
Reconnect And Resumption
Reconnect is enabled by default. The runtime uses jittered exponential backoff,
honors server retry fields, and stops reconnecting after manual
disconnect().
When events include id, the latest id is sent as Last-Event-ID on reconnect.
attachLifecycleResume(client, options?) reconnects when the page regains focus,
comes back online, or becomes visible again. It returns a cleanup function and is
a no-op outside the browser:
import { attachLifecycleResume } from "@flamefrontend/sse-runtime-core";
const detach = attachLifecycleResume(client, { strategy: "reconnect" });Set staleTimeoutMs and/or wakeDriftMs to also run a background watchdog that
recovers a stream the browser never reported as broken — one that went silent or
whose socket died while the device slept. minHiddenMs suppresses the visible
trigger after a brief tab switch. See the API reference for all options.
Auth Refresh
const client = createSSEClient<Events>({
key: ["chat", chatId],
url: `/api/chats/${chatId}/stream`,
headers: async () => ({
Authorization: `Bearer ${await getAccessToken()}`
}),
auth: {
onUnauthorized: refreshAccessToken,
retryAfterRefresh: true
}
});createBearerAuth(getToken, options?) builds the headers + auth pair for the
common case — the token is resolved before every attempt and again on 401:
createSSEClient<Events>({
key: ["chat", chatId],
url: `/api/chats/${chatId}/stream`,
...createBearerAuth(getAccessToken)
});Reconnect Notifications
attachReconnectNotifications(client, handlers) maps status transitions to
reconnect-lifecycle callbacks (drops and recoveries only, not the initial connect
or a manual disconnect). It returns a cleanup function.
const detach = attachReconnectNotifications(client, {
onReconnecting: () => showToast("Reconnecting…"),
onReconnected: () => dismissToast(),
onFailed: () => showToast("Connection lost")
});Single-Tab Coordination
const client = createSSEClient<Events>({
key: ["chat", chatId],
url: `/api/chats/${chatId}/stream`,
coordination: {
enabled: true,
mode: "single-tab"
}
});Tabs with the same key elect one leader through Web Locks. The leader owns the
SSE connection and forwards events/status/errors through BroadcastChannel.
When the leader goes away, a follower is promoted and resumes the stream from the
last seen Last-Event-ID. Unsupported environments fall back to independent
per-tab connections.
Known Limitations
- A leader that reaches a terminal
errorstatus (for example after exhausting reconnect retries) keeps leadership and does not hand it off, so other tabs wait rather than racing to open a fresh connection that would likely fail the same way. - During the leadership handoff there is a sub-millisecond window in which an event from the new leader may be delivered ahead of a still-draining event from the previous leader. The window is bounded by microtask timing and does not affect steady-state ordering.
More Documentation
Full guide: https://github.com/FlameFront-end/sse-runtime#readme
License
MIT
