@stackline/sse
Universal, spec-correct and memory-safe Server-Sent Events toolkit for AI streaming, browsers, servers and edge runtimes.
Documentation | npm | Issues | Repository
Current package version: 1.0.4
Why this package?
One zero-dependency toolkit for consuming, parsing, encoding, serving, and reconnecting Server-Sent Events. It is designed for AI token streams, live interfaces, serverless runtimes, browsers, and Node.js services.
Why this package
SSE projects commonly combine one parser package, another encoder, a stale
fetch wrapper, and custom server code. @stackline/sse gives those layers one
consistent contract:
- WHATWG-compatible incremental parsing of strings and UTF-8 bytes;
- pull-based async iteration with real stream backpressure;
fetchstreaming with POST, auth headers, retries, timeouts, and resume IDs;- safe event encoding that rejects CRLF and
Last-Event-IDinjection; - Web Stream and
Responsehelpers for edge and server runtimes; - bounded line, event, and callback queues by default;
- ESM, CommonJS, browser global, TypeScript 3.9 through 7, Deno, and Bun;
- zero runtime dependencies.
Compatibility
| Item | Value |
|---|---|
| Package | @stackline/sse@1.0.4 |
| Node.js runtime | >=14.17.0 |
| CommonJS / primary entry | ./dist/index.cjs |
| ES module entry | ./dist/index.js |
| Type declarations | ./dist/index.d.ts |
Runtime matrix
| Runtime | Parser / encoder | Fetch client | Server helpers |
|---|---|---|---|
| Modern browsers | Yes | Yes | Yes |
| Node.js 18+ | Yes | Yes | Yes |
| Node.js 14 / 16 | Yes | Inject fetch |
Inject Web Streams if needed |
| Deno 2 | Yes | Yes | Yes |
| Bun | Yes | Yes | Yes |
| Cloudflare Workers | Yes | Yes | Yes |
The package ships ESM, CommonJS, a browser IIFE, and declarations tested with TypeScript 3.9, 4.7, 4.9, 5.x, 6.x, and 7.x.
Installation
npm install @stackline/sse
Usage
npm install @stackline/sse
Parse any stream
Async iterator
import { decodeSSE } from '@stackline/sse';
const response = await fetch('/events');
for await (const event of decodeSSE(response)) {
console.log(event.event, event.data, event.lastEventId);
}
The source can be a Response, ReadableStream, AsyncIterable, or ordinary
Iterable of string and Uint8Array chunks.
Incremental callback parser
import { createParser } from '@stackline/sse';
const parser = createParser({
onEvent(event) {
console.log(event.data);
},
onRetry(milliseconds) {
console.log('Server retry interval:', milliseconds);
}
});
parser.feed('id: 7\ndata: first chunk\n');
parser.feed('data: second chunk\n\n');
Each event contains:
interface SSEEvent<T = string> {
data: T;
event?: string;
id?: string; // ID field in this event block
lastEventId: string; // committed resume ID, including inherited IDs
}
An id-only block commits lastEventId even when no message is dispatched.
That detail matters when a connection closes immediately after a checkpoint.
JSON streams
import { decodeJSON } from '@stackline/sse';
for await (const event of decodeJSON(response, {
doneSentinel: '[DONE]'
})) {
console.log(event.data); // parsed JSON value
}
Invalid JSON throws SSEParseError. Set ignoreInvalidJSON: true only when a
mixed text and JSON protocol intentionally requires it.
Encode events
import { encodeJSON, encodeSSE } from '@stackline/sse';
encodeSSE({
id: '42',
event: 'delta',
retry: 3000,
data: 'line one\nline two'
});
encodeJSON({ token: 'hello' }, { event: 'delta', id: '43' });
id and event values cannot contain line breaks. IDs also reject NUL. This
prevents a value from injecting additional SSE fields or HTTP resume headers.
Serve events
Response from an async generator
import { eventStreamResponse } from '@stackline/sse';
async function* updates() {
yield { event: 'ready', data: 'connected', id: '1' };
yield { event: 'delta', data: 'hello', id: '2' };
}
export function GET() {
return eventStreamResponse(updates());
}
The response includes text/event-stream, no-cache, no-transform, and
X-Accel-Buffering: no headers unless the caller overrides them.
Push channel
import { createSSEChannel } from '@stackline/sse';
const channel = createSSEChannel({
heartbeatInterval: 15_000
});
const response = channel.toResponse();
if (!channel.sendJSON({ progress: 25 }, { event: 'progress' })) {
await channel.ready;
}
channel.close();
send and sendJSON return false when the stream applies backpressure.
Wait for channel.ready before producing more data.
Features and Integrations
AI streaming
import { fetchSSE } from '@stackline/sse';
const controller = new AbortController();
for await (const event of fetchSSE('https://api.example.com/responses', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.API_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ model: 'example-model', stream: true }),
signal: controller.signal,
connectTimeout: 10_000,
idleTimeout: 45_000,
totalTimeout: 5 * 60_000,
retry: {
retries: 3,
minDelay: 500,
maxDelay: 10_000
}
})) {
if (event.data === '[DONE]') break;
console.log(event.event, JSON.parse(event.data));
}
fetchSSE accepts all ordinary fetch request options. Node.js 18 and newer
provide fetch; Node.js 14 and 16 can pass an implementation with fetch.
Reconnection behavior
fetchSSE follows SSE resume semantics and adds explicit production controls:
- sends
Accept: text/event-streamandCache-Control: no-storebehavior; - commits and forwards
Last-Event-IDon reconnect; - honors valid
retry:fields andRetry-Afterheaders; - retries network failures and HTTP 408, 425, 429, 500, 502, 503, and 504;
- rejects other HTTP statuses and incorrect content types;
- uses exponential backoff with full jitter by default;
- stops permanently on HTTP 204;
- never replays a streaming request body without
bodyFactory.
Native EventSource reconnects indefinitely, so the default retry budget is
also unlimited. Production applications should pass an AbortSignal, a finite
retry.retries, or totalTimeout.
const options = {
retry: {
retries: 5,
minDelay: 500,
maxDelay: 30_000,
factor: 2,
jitter: 'full'
},
onRetry({ delay, reconnects, error }) {
console.warn({ delay, reconnects, error });
}
};
For a body that must be recreated on every attempt, bodyFactory receives the
attempt number, committed resume ID, and that attempt's abort signal:
const options = {
bodyFactory({ attempt, lastEventId, signal }) {
return createUploadStream({ attempt, lastEventId, signal });
}
};
When the input is a Request, its headers are preserved unless options.headers
explicitly replaces them. The SSE Accept and resume headers are then merged
case-insensitively.
Migration
From eventsource-parser
Direct dependency:
npm install @stackline/sse
The familiar API is available:
import { createParser } from '@stackline/sse';
For a low-change trial, npm aliases preserve the old import name:
npm install eventsource-parser@npm:@stackline/sse
createParser({ onEvent, onRetry, onComment, onError, maxBufferSize }) is
supported. The additional lastEventId property follows WHATWG resume
semantics. Security limits are enabled by default, unlike unbounded parsers.
From @microsoft/fetch-event-source
npm install @stackline/sse
import { fetchEventSource } from '@stackline/sse';
await fetchEventSource('/events', {
onopen(response) {},
onmessage(event) {},
onclose(context) {},
onerror(error) {}
});
An alias can support staged migration:
npm install @microsoft/fetch-event-source@npm:@stackline/sse
The callback names are supported. openWhenHidden is accepted but this package
does not silently disconnect a healthy stream when a page becomes hidden.
Adoption resources
- OpenAI, Anthropic, browser, Node.js, and edge recipes
- Reproducible parser and fragmentation benchmarks
- Executable examples
- Stackline open-source catalog
The examples ship in the npm tarball. Network examples expose functions and do not send requests during installation or import.
Security
Memory safety
The parser is bounded by default:
| Limit | Default | Purpose |
|---|---|---|
maxLineLength |
1 MiB | unterminated or oversized field line |
maxEventSize |
1 MiB | accumulated multiline event |
maxQueuedEvents |
4096 | callback burst inside one feed slice |
feedSize |
16 KiB | limits work admitted before yielding |
Raise a limit explicitly for a trusted protocol that carries larger events.
Limit failures terminate the parser with a stable ERR_SSE_* code.
API Surface
Errors
| Class | Code | Meaning |
|---|---|---|
SSEParseError |
ERR_SSE_PARSE and specific variants |
malformed or limited stream |
SSEEncodeError |
ERR_SSE_ENCODE |
unsafe or unsupported output field |
SSEHTTPError |
ERR_SSE_HTTP |
rejected HTTP response |
SSETimeoutError |
ERR_SSE_TIMEOUT |
connect, idle, or total deadline |
SSERetryError |
ERR_SSE_RETRY |
finite reconnect budget exhausted |
SSEReplayError |
ERR_SSE_BODY_REPLAY |
non-replayable request body |
Local Development
git clone https://github.com/alexandroit/stackline-sse.git
cd stackline-sse
npm ci
npm run test
Release tooling uses Node.js 24.20.0 and npm 11.19.0. The consumer runtime contract remains the one documented above.
Consumer Smoke Test
Run the repository's existing consumer/package check after installing development dependencies:
npm run test:install
Release Checklist
Package integrity
- zero runtime dependencies;
- no install scripts;
- deterministic ESM, CommonJS, and browser builds;
- CI tests Node.js 14 through 24, Windows, macOS, Linux, Deno, and Bun;
- CodeQL, npm audit, registry signature verification,
publint, and Are the Types Wrong checks; - release tarballs include SHA-512 checksums and a CycloneDX SBOM.
See SECURITY.md for vulnerability reporting and CONTRIBUTING.md for development instructions.
Run npm run test and inspect the package contents before release. Publish a new version through the GitHub Actions publishing workflow, using the SHA-512 digest of the reviewed tarball. Verify the exact published version, tarball integrity, and npm provenance after the run.
License
MIT Copyright 2026 Alexandro Paixao Marques.
Credits and original authors
- Alexandro Paixao Marques.
- Copyright (c) 2026 Alexandro Paixao Marques.
- Copyright 2026 Alexandro Paixao Marques.
- Stackline maintenance: Alexandro Paixao Marques and Stackline contributors.
Original copyright, license notices and contributor acknowledgements remain part of this distribution. Stackline maintenance does not replace authorship of the original work.
Community and Links
Use this repository's issue tracker for reproducible bugs and feature requests. Join r/Stackline for examples, usage questions and release discussions.