@stackline/sse 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes are documented in this file.
4
+
5
+ ## [1.0.0] - 2026-08-16
6
+
7
+ ### Added
8
+
9
+ - WHATWG-compatible incremental SSE parser for strings and UTF-8 bytes.
10
+ - Pull-based decoding for Response, Web Stream, async iterable, and iterable sources.
11
+ - JSON event decoding with optional completion sentinel support.
12
+ - Injection-resistant text and JSON encoders.
13
+ - Fetch client with resume IDs, retry policy, jitter, Retry-After, and timeouts.
14
+ - Callback compatibility for `eventsource-parser` and `@microsoft/fetch-event-source` migrations.
15
+ - Backpressure-aware server channel and async-iterable Response helper.
16
+ - Default line, event, and callback queue limits.
17
+ - ESM, CommonJS, browser, and TypeScript declarations.
18
+ - Cross-runtime CI, security tests, deterministic fuzzing, and release artifacts.
19
+ - Abort-safe custom Fetch, body factories, and async iterators, including late-response cleanup.
20
+ - Request header preservation when adding SSE negotiation and resume headers.
21
+
22
+ [1.0.0]: https://github.com/alexandroit/stackline-sse/releases/tag/v1.0.0
@@ -0,0 +1,45 @@
1
+ # Contributing
2
+
3
+ Contributions are welcome through focused issues and pull requests.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 20.20 or newer for development;
8
+ - npm with lockfile support;
9
+ - no new runtime dependency without a documented architectural reason.
10
+
11
+ ## Setup
12
+
13
+ ```bash
14
+ npm ci
15
+ npm test
16
+ ```
17
+
18
+ Useful commands:
19
+
20
+ ```bash
21
+ npm run test:unit
22
+ npm run test:coverage
23
+ npm run test:types
24
+ npm run test:attw
25
+ npm run benchmark
26
+ npm run docs:serve
27
+ ```
28
+
29
+ ## Change expectations
30
+
31
+ - Preserve WHATWG parsing behavior unless a deviation is explicitly documented.
32
+ - Add a regression test for every bug fix.
33
+ - Exercise arbitrary chunk boundaries for parser changes.
34
+ - Preserve Node.js 14 parser and encoder compatibility.
35
+ - Keep callback failures from causing automatic event replay.
36
+ - Keep all externally controlled buffers bounded.
37
+ - Update README, changelog, types, and examples with public API changes.
38
+
39
+ Performance changes should include correctness assertions and benchmark output.
40
+ Benchmarks are evidence, not a substitute for tests.
41
+
42
+ ## Commit and review scope
43
+
44
+ Keep commits narrow and do not reformat unrelated files. Pull requests should
45
+ describe behavior, compatibility impact, security impact, and verification.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexandro Paixao Marques
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,6 @@
1
+ @stackline/sse
2
+ Copyright 2026 Alexandro Paixao Marques
3
+
4
+ This is an independently designed implementation of the Server-Sent Events
5
+ wire format defined by the WHATWG HTML Living Standard. Runtime code does not
6
+ include source copied from the packages used for development comparisons.
package/README.md ADDED
@@ -0,0 +1,340 @@
1
+ # @stackline/sse
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@stackline/sse.svg)](https://www.npmjs.com/package/@stackline/sse)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@stackline/sse.svg)](https://www.npmjs.com/package/@stackline/sse)
5
+ [![CI](https://github.com/alexandroit/stackline-sse/actions/workflows/ci.yml/badge.svg)](https://github.com/alexandroit/stackline-sse/actions/workflows/ci.yml)
6
+ [![CodeQL](https://github.com/alexandroit/stackline-sse/actions/workflows/codeql.yml/badge.svg)](https://github.com/alexandroit/stackline-sse/actions/workflows/codeql.yml)
7
+ [![license](https://img.shields.io/npm/l/@stackline/sse.svg)](LICENSE)
8
+
9
+ One zero-dependency toolkit for consuming, parsing, encoding, serving, and
10
+ reconnecting Server-Sent Events. It is designed for AI token streams, live
11
+ interfaces, serverless runtimes, browsers, and Node.js services.
12
+
13
+ ```bash
14
+ npm install @stackline/sse
15
+ ```
16
+
17
+ ## Why this package
18
+
19
+ SSE projects commonly combine one parser package, another encoder, a stale
20
+ fetch wrapper, and custom server code. `@stackline/sse` gives those layers one
21
+ consistent contract:
22
+
23
+ - WHATWG-compatible incremental parsing of strings and UTF-8 bytes;
24
+ - pull-based async iteration with real stream backpressure;
25
+ - `fetch` streaming with POST, auth headers, retries, timeouts, and resume IDs;
26
+ - safe event encoding that rejects CRLF and `Last-Event-ID` injection;
27
+ - Web Stream and `Response` helpers for edge and server runtimes;
28
+ - bounded line, event, and callback queues by default;
29
+ - ESM, CommonJS, browser global, TypeScript 3.9 through 7, Deno, and Bun;
30
+ - zero runtime dependencies.
31
+
32
+ ## AI streaming
33
+
34
+ ```js
35
+ import { fetchSSE } from '@stackline/sse';
36
+
37
+ const controller = new AbortController();
38
+
39
+ for await (const event of fetchSSE('https://api.example.com/responses', {
40
+ method: 'POST',
41
+ headers: {
42
+ Authorization: `Bearer ${process.env.API_TOKEN}`,
43
+ 'Content-Type': 'application/json'
44
+ },
45
+ body: JSON.stringify({ model: 'example-model', stream: true }),
46
+ signal: controller.signal,
47
+ connectTimeout: 10_000,
48
+ idleTimeout: 45_000,
49
+ totalTimeout: 5 * 60_000,
50
+ retry: {
51
+ retries: 3,
52
+ minDelay: 500,
53
+ maxDelay: 10_000
54
+ }
55
+ })) {
56
+ if (event.data === '[DONE]') break;
57
+ console.log(event.event, JSON.parse(event.data));
58
+ }
59
+ ```
60
+
61
+ `fetchSSE` accepts all ordinary `fetch` request options. Node.js 18 and newer
62
+ provide `fetch`; Node.js 14 and 16 can pass an implementation with `fetch`.
63
+
64
+ ## Parse any stream
65
+
66
+ ### Async iterator
67
+
68
+ ```js
69
+ import { decodeSSE } from '@stackline/sse';
70
+
71
+ const response = await fetch('/events');
72
+
73
+ for await (const event of decodeSSE(response)) {
74
+ console.log(event.event, event.data, event.lastEventId);
75
+ }
76
+ ```
77
+
78
+ The source can be a `Response`, `ReadableStream`, `AsyncIterable`, or ordinary
79
+ `Iterable` of `string` and `Uint8Array` chunks.
80
+
81
+ ### Incremental callback parser
82
+
83
+ ```js
84
+ import { createParser } from '@stackline/sse';
85
+
86
+ const parser = createParser({
87
+ onEvent(event) {
88
+ console.log(event.data);
89
+ },
90
+ onRetry(milliseconds) {
91
+ console.log('Server retry interval:', milliseconds);
92
+ }
93
+ });
94
+
95
+ parser.feed('id: 7\ndata: first chunk\n');
96
+ parser.feed('data: second chunk\n\n');
97
+ ```
98
+
99
+ Each event contains:
100
+
101
+ ```ts
102
+ interface SSEEvent<T = string> {
103
+ data: T;
104
+ event?: string;
105
+ id?: string; // ID field in this event block
106
+ lastEventId: string; // committed resume ID, including inherited IDs
107
+ }
108
+ ```
109
+
110
+ An `id`-only block commits `lastEventId` even when no message is dispatched.
111
+ That detail matters when a connection closes immediately after a checkpoint.
112
+
113
+ ## JSON streams
114
+
115
+ ```js
116
+ import { decodeJSON } from '@stackline/sse';
117
+
118
+ for await (const event of decodeJSON(response, {
119
+ doneSentinel: '[DONE]'
120
+ })) {
121
+ console.log(event.data); // parsed JSON value
122
+ }
123
+ ```
124
+
125
+ Invalid JSON throws `SSEParseError`. Set `ignoreInvalidJSON: true` only when a
126
+ mixed text and JSON protocol intentionally requires it.
127
+
128
+ ## Encode events
129
+
130
+ ```js
131
+ import { encodeJSON, encodeSSE } from '@stackline/sse';
132
+
133
+ encodeSSE({
134
+ id: '42',
135
+ event: 'delta',
136
+ retry: 3000,
137
+ data: 'line one\nline two'
138
+ });
139
+
140
+ encodeJSON({ token: 'hello' }, { event: 'delta', id: '43' });
141
+ ```
142
+
143
+ `id` and `event` values cannot contain line breaks. IDs also reject NUL. This
144
+ prevents a value from injecting additional SSE fields or HTTP resume headers.
145
+
146
+ ## Serve events
147
+
148
+ ### Response from an async generator
149
+
150
+ ```js
151
+ import { eventStreamResponse } from '@stackline/sse';
152
+
153
+ async function* updates() {
154
+ yield { event: 'ready', data: 'connected', id: '1' };
155
+ yield { event: 'delta', data: 'hello', id: '2' };
156
+ }
157
+
158
+ export function GET() {
159
+ return eventStreamResponse(updates());
160
+ }
161
+ ```
162
+
163
+ The response includes `text/event-stream`, `no-cache, no-transform`, and
164
+ `X-Accel-Buffering: no` headers unless the caller overrides them.
165
+
166
+ ### Push channel
167
+
168
+ ```js
169
+ import { createSSEChannel } from '@stackline/sse';
170
+
171
+ const channel = createSSEChannel({
172
+ heartbeatInterval: 15_000
173
+ });
174
+
175
+ const response = channel.toResponse();
176
+
177
+ if (!channel.sendJSON({ progress: 25 }, { event: 'progress' })) {
178
+ await channel.ready;
179
+ }
180
+
181
+ channel.close();
182
+ ```
183
+
184
+ `send` and `sendJSON` return `false` when the stream applies backpressure.
185
+ Wait for `channel.ready` before producing more data.
186
+
187
+ ## Reconnection behavior
188
+
189
+ `fetchSSE` follows SSE resume semantics and adds explicit production controls:
190
+
191
+ - sends `Accept: text/event-stream` and `Cache-Control: no-store` behavior;
192
+ - commits and forwards `Last-Event-ID` on reconnect;
193
+ - honors valid `retry:` fields and `Retry-After` headers;
194
+ - retries network failures and HTTP 408, 425, 429, 500, 502, 503, and 504;
195
+ - rejects other HTTP statuses and incorrect content types;
196
+ - uses exponential backoff with full jitter by default;
197
+ - stops permanently on HTTP 204;
198
+ - never replays a streaming request body without `bodyFactory`.
199
+
200
+ Native EventSource reconnects indefinitely, so the default retry budget is
201
+ also unlimited. Production applications should pass an `AbortSignal`, a finite
202
+ `retry.retries`, or `totalTimeout`.
203
+
204
+ ```js
205
+ const options = {
206
+ retry: {
207
+ retries: 5,
208
+ minDelay: 500,
209
+ maxDelay: 30_000,
210
+ factor: 2,
211
+ jitter: 'full'
212
+ },
213
+ onRetry({ delay, reconnects, error }) {
214
+ console.warn({ delay, reconnects, error });
215
+ }
216
+ };
217
+ ```
218
+
219
+ For a body that must be recreated on every attempt, `bodyFactory` receives the
220
+ attempt number, committed resume ID, and that attempt's abort signal:
221
+
222
+ ```js
223
+ const options = {
224
+ bodyFactory({ attempt, lastEventId, signal }) {
225
+ return createUploadStream({ attempt, lastEventId, signal });
226
+ }
227
+ };
228
+ ```
229
+
230
+ When the input is a `Request`, its headers are preserved unless `options.headers`
231
+ explicitly replaces them. The SSE `Accept` and resume headers are then merged
232
+ case-insensitively.
233
+
234
+ ## Memory safety
235
+
236
+ The parser is bounded by default:
237
+
238
+ | Limit | Default | Purpose |
239
+ | --- | ---: | --- |
240
+ | `maxLineLength` | 1 MiB | unterminated or oversized field line |
241
+ | `maxEventSize` | 1 MiB | accumulated multiline event |
242
+ | `maxQueuedEvents` | 4096 | callback burst inside one feed slice |
243
+ | `feedSize` | 16 KiB | limits work admitted before yielding |
244
+
245
+ Raise a limit explicitly for a trusted protocol that carries larger events.
246
+ Limit failures terminate the parser with a stable `ERR_SSE_*` code.
247
+
248
+ ## Migration
249
+
250
+ ### From eventsource-parser
251
+
252
+ Direct dependency:
253
+
254
+ ```bash
255
+ npm install @stackline/sse
256
+ ```
257
+
258
+ The familiar API is available:
259
+
260
+ ```js
261
+ import { createParser } from '@stackline/sse';
262
+ ```
263
+
264
+ For a low-change trial, npm aliases preserve the old import name:
265
+
266
+ ```bash
267
+ npm install eventsource-parser@npm:@stackline/sse
268
+ ```
269
+
270
+ `createParser({ onEvent, onRetry, onComment, onError, maxBufferSize })` is
271
+ supported. The additional `lastEventId` property follows WHATWG resume
272
+ semantics. Security limits are enabled by default, unlike unbounded parsers.
273
+
274
+ ### From @microsoft/fetch-event-source
275
+
276
+ ```bash
277
+ npm install @stackline/sse
278
+ ```
279
+
280
+ ```js
281
+ import { fetchEventSource } from '@stackline/sse';
282
+
283
+ await fetchEventSource('/events', {
284
+ onopen(response) {},
285
+ onmessage(event) {},
286
+ onclose(context) {},
287
+ onerror(error) {}
288
+ });
289
+ ```
290
+
291
+ An alias can support staged migration:
292
+
293
+ ```bash
294
+ npm install @microsoft/fetch-event-source@npm:@stackline/sse
295
+ ```
296
+
297
+ The callback names are supported. `openWhenHidden` is accepted but this package
298
+ does not silently disconnect a healthy stream when a page becomes hidden.
299
+
300
+ ## Runtime matrix
301
+
302
+ | Runtime | Parser / encoder | Fetch client | Server helpers |
303
+ | --- | --- | --- | --- |
304
+ | Modern browsers | Yes | Yes | Yes |
305
+ | Node.js 18+ | Yes | Yes | Yes |
306
+ | Node.js 14 / 16 | Yes | Inject `fetch` | Inject Web Streams if needed |
307
+ | Deno 2 | Yes | Yes | Yes |
308
+ | Bun | Yes | Yes | Yes |
309
+ | Cloudflare Workers | Yes | Yes | Yes |
310
+
311
+ The package ships ESM, CommonJS, a browser IIFE, and declarations tested with
312
+ TypeScript 3.9, 4.7, 4.9, 5.x, 6.x, and 7.x.
313
+
314
+ ## Errors
315
+
316
+ | Class | Code | Meaning |
317
+ | --- | --- | --- |
318
+ | `SSEParseError` | `ERR_SSE_PARSE` and specific variants | malformed or limited stream |
319
+ | `SSEEncodeError` | `ERR_SSE_ENCODE` | unsafe or unsupported output field |
320
+ | `SSEHTTPError` | `ERR_SSE_HTTP` | rejected HTTP response |
321
+ | `SSETimeoutError` | `ERR_SSE_TIMEOUT` | connect, idle, or total deadline |
322
+ | `SSERetryError` | `ERR_SSE_RETRY` | finite reconnect budget exhausted |
323
+ | `SSEReplayError` | `ERR_SSE_BODY_REPLAY` | non-replayable request body |
324
+
325
+ ## Package integrity
326
+
327
+ - zero runtime dependencies;
328
+ - no install scripts;
329
+ - deterministic ESM, CommonJS, and browser builds;
330
+ - CI tests Node.js 14 through 24, Windows, macOS, Linux, Deno, and Bun;
331
+ - CodeQL, npm audit, registry signature verification, `publint`, and
332
+ Are the Types Wrong checks;
333
+ - release tarballs include SHA-512 checksums and a CycloneDX SBOM.
334
+
335
+ See [SECURITY.md](SECURITY.md) for vulnerability reporting and
336
+ [CONTRIBUTING.md](CONTRIBUTING.md) for development instructions.
337
+
338
+ ## License
339
+
340
+ [MIT](LICENSE) Copyright 2026 Alexandro Paixao Marques.
package/SECURITY.md ADDED
@@ -0,0 +1,35 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ | Version | Supported |
6
+ | --- | --- |
7
+ | 1.x | Yes |
8
+ | Pre-release or unpublished builds | No |
9
+
10
+ Security fixes are released as new immutable npm versions. A vulnerable
11
+ published version is deprecated when appropriate; it is not silently replaced.
12
+
13
+ ## Reporting a vulnerability
14
+
15
+ Do not open a public issue for an undisclosed vulnerability. Use GitHub's
16
+ private vulnerability reporting for
17
+ [alexandroit/stackline-sse](https://github.com/alexandroit/stackline-sse/security/advisories/new).
18
+
19
+ Include:
20
+
21
+ - affected version and runtime;
22
+ - minimal reproduction or malformed byte sequence;
23
+ - security impact and expected behavior;
24
+ - whether the issue affects parser, encoder, client, or server APIs.
25
+
26
+ Reports are acknowledged as soon as practical. Confirmed issues receive a
27
+ coordinated fix, regression tests, release notes, and credit when requested.
28
+
29
+ ## Security boundaries
30
+
31
+ The package limits parser memory and validates encoded control fields. It does
32
+ not authenticate event producers, authorize endpoints, validate event JSON
33
+ schemas, encrypt transport, or make arbitrary event data safe for HTML output.
34
+ Applications must still use HTTPS, validate origin and payloads, and escape
35
+ data at the rendering boundary.