SSE (Server-Sent Events) client for Expo apps, built on expo/fetch streaming.
- Expo SDK 54+
| Expo SSE version 🧑💻 | Expo SDK Version |
|---|---|
| 1.3.0 | 55+ |
| 1.0.0+ | 54+ |
npm install @dawidzawada/expo-sse
# or
yarn add @dawidzawada/expo-sse
# or
bun add @dawidzawada/expo-sseThree layers, each usable independently:
parseSSEBuffer → parseSSEStream → fetchSSE
(pure fn) (stream reader) (full client)
parseSSEBuffer— Pure function. Parses a string buffer into SSE events per the WHATWG spec.parseSSEStream— Reads aReadableStream<Uint8Array>, decodes chunks, and feeds them toparseSSEBuffer.fetchSSE— Top-level API. Connects viaexpo/fetch, parses the stream, and auto-reconnects with exponential backoff + jitter.
Connects to an SSE endpoint and auto-reconnects on failure.
import { fetchSSE } from '@dawidzawada/expo-sse';
const controller = new AbortController();
await fetchSSE('https://api.example.com/events', {
signal: controller.signal,
headers: { Authorization: 'Bearer token' },
onMessage: (msg) => console.log(msg.event, msg.data),
onError: (error) => console.error(error),
onClose: () => console.log('stream closed'),
});| Field | Type | Description |
|---|---|---|
headers |
HeadersInit | () => HeadersInit | () => Promise<HeadersInit> |
Request headers. Async functions are re-evaluated on each reconnect attempt (useful for token refresh). |
signal |
AbortSignal |
Abort signal to stop the connection. |
onOpen |
(response: Response) => void | Promise<void> |
Called when the connection opens. Errors here are fatal (no reconnect). |
onMessage |
(message: SSEMessage) => void |
Required. Called for each SSE event. |
onError |
(error: Error) => number | void | Promise<number | void> |
Called on errors. Return a delay in ms (0 for immediate reconnect) or throw to stop. |
onClose |
() => void |
Called when the stream closes normally. |
onAbort |
() => void |
Called when the connection is aborted via the signal. |
maxBufferSize |
number |
Max buffer size in bytes. Defaults to 512KB. |
Reads a ReadableStream<Uint8Array> and parses it into SSE events.
import { parseSSEStream } from '@dawidzawada/expo-sse';
await parseSSEStream(response.body, {
onMessage: (msg) => console.log(msg),
onRetry: (ms) => console.log('server requested retry:', ms),
maxBufferSize: 1024 * 1024, // 1MB
});| Field | Type | Description |
|---|---|---|
onMessage |
(message: SSEMessage) => void |
Required. Called for each parsed event. |
onRetry |
(retryMs: number) => void |
Called when a retry: field is received. |
maxBufferSize |
number |
Max buffer size in bytes. Defaults to 512KB. Throws SSEBufferOverflowError if exceeded. |
Pure function that parses a string buffer into SSE events. Handles LF (\n), CRLF (\r\n), and CR (\r) line endings simultaneously.
import { parseSSEBuffer } from '@dawidzawada/expo-sse';
const result = parseSSEBuffer('event: greeting\ndata: hello\n\n');
// result.events = [{ event: 'greeting', data: 'hello', lastEventId: '' }]| Field | Type | Description |
|---|---|---|
events |
SSEMessage[] |
Parsed events. |
remaining |
string |
Unconsumed text (incomplete lines to prepend to the next chunk). |
retry |
number | undefined |
Last retry: value seen, if any. |
lastEventId |
string | undefined |
Last id: value seen, if any. |
interface SSEMessage {
event: string; // Event type (defaults to "message")
data: string; // Event data
lastEventId: string; // Last event ID
}Thrown when the server responds with a non-2xx status code.
| Property | Type | Description |
|---|---|---|
status |
number |
HTTP status code. |
response |
Response |
The full response object. |
Thrown when the internal buffer exceeds maxBufferSize.
| Property | Type | Description |
|---|---|---|
bufferSize |
number |
Current buffer size in bytes. |
maxBufferSize |
number |
Configured limit in bytes. |
Thrown when the connection fails at the transport layer — before an HTTP response is received (DNS, TCP connect, TLS, or timeout). Occurs when for example device is offline. Passed to onError (and rejected from fetchSSE when no onError is set).
| Property | Type | Description |
|---|---|---|
type |
'fetch_failed' |
Originating expo/fetch error band. |
kind |
'dns' | 'connect' | 'tls' | 'timeout' | 'unknown' |
Classified failure category (see below). |
cause |
unknown |
The original underlying error. |
The kind lets you triage transport failures by category and priority — for example, silence the expected offline noise ('dns', 'connect') while still reporting the ones worth investigating ('tls', 'timeout', 'unknown'):
import { fetchSSE, SSETransportError } from '@dawidzawada/expo-sse';
fetchSSE(url, {
onMessage: handleMessage,
onError: (error) => {
if (error instanceof SSETransportError) {
if (error.kind === 'dns' || error.kind === 'connect') {
return; // offline — reconnect quietly, don't report
}
reportToSentry(error); // tls / timeout / unknown
return;
}
throw error; // e.g. SSEHttpError — handle separately
},
});Platform support. Classification of kind is currently Android only — the underlying native error carries a stable signature (the JVM exception class name) that we can categorize reliably. On iOS, expo/fetch exposes only a localized, device-language error description with no stable machine-readable signature, so kind is always 'unknown' there for now. iOS classification is on the roadmap: as soon as expo/fetch surfaces a stable error signal on iOS, it will populate the same kind values — no API change required.
Use an AbortController to disconnect. The onAbort callback is called when the connection is stopped via the signal.
import { fetchSSE } from '@dawidzawada/expo-sse';
const controller = new AbortController();
fetchSSE('https://api.example.com/events', {
signal: controller.signal,
onMessage: (msg) => console.log(msg.event, msg.data),
onClose: () => console.log('stream closed by server'),
onAbort: () => console.log('disconnected by client'),
});
// Later: disconnect
controller.abort();Async headers are re-evaluated on each reconnect, making token refresh straightforward:
import { fetchSSE, SSEHttpError } from '@dawidzawada/expo-sse';
const getToken = async () => {};
const refreshToken = async () => {};
await fetchSSE('https://api.example.com/events', {
signal: controller.signal,
headers: async () => ({
Authorization: `Bearer ${getToken()}`,
}),
onMessage: (msg) => {
console.log(msg.event, msg.data);
},
onError: async (error) => {
if (error instanceof SSEHttpError && error.status === 401) {
await refreshToken();
return 0; // reconnect immediately with new token
}
},
});See CONTRIBUTING.md.
MIT