@stackline/sse 1.0.0 → 1.0.1
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 +19 -0
- package/README.md +10 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.min.js +1 -1
- package/docs/ARCHITECTURE.md +71 -0
- package/docs/BENCHMARKS.md +20 -0
- package/docs/COMPATIBILITY.md +57 -0
- package/docs/INTEGRATIONS.md +46 -0
- package/docs/MARKET_RESEARCH.md +84 -0
- package/docs/RELEASING.md +60 -0
- package/examples/ai-client.mjs +28 -0
- package/examples/anthropic-messages.mjs +24 -0
- package/examples/openai-responses.mjs +18 -0
- package/examples/parser.mjs +10 -0
- package/examples/server-edge.mjs +10 -0
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.0.1] - 2026-08-21
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Executable local stream, parser, OpenAI Responses, Anthropic Messages, and
|
|
10
|
+
edge-handler examples.
|
|
11
|
+
- Integration recipes for provider streams, browsers, Node.js, Workers, and
|
|
12
|
+
low-change package aliases.
|
|
13
|
+
- Reproducible parser, encoder, and adversarial-fragmentation methodology.
|
|
14
|
+
- First-party documentation analytics that never records stream contents.
|
|
15
|
+
- Trusted-publishing workflow for provenance-enabled future releases.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Package tarballs now include the public guides and examples.
|
|
20
|
+
|
|
21
|
+
No runtime API or declaration behavior changed in this release.
|
|
22
|
+
|
|
5
23
|
## [1.0.0] - 2026-08-16
|
|
6
24
|
|
|
7
25
|
### Added
|
|
@@ -20,3 +38,4 @@ All notable changes are documented in this file.
|
|
|
20
38
|
- Request header preservation when adding SSE negotiation and resume headers.
|
|
21
39
|
|
|
22
40
|
[1.0.0]: https://github.com/alexandroit/stackline-sse/releases/tag/v1.0.0
|
|
41
|
+
[1.0.1]: https://github.com/alexandroit/stackline-sse/compare/v1.0.0...v1.0.1
|
package/README.md
CHANGED
|
@@ -335,6 +335,16 @@ TypeScript 3.9, 4.7, 4.9, 5.x, 6.x, and 7.x.
|
|
|
335
335
|
See [SECURITY.md](SECURITY.md) for vulnerability reporting and
|
|
336
336
|
[CONTRIBUTING.md](CONTRIBUTING.md) for development instructions.
|
|
337
337
|
|
|
338
|
+
## Adoption resources
|
|
339
|
+
|
|
340
|
+
- [OpenAI, Anthropic, browser, Node.js, and edge recipes](docs/INTEGRATIONS.md)
|
|
341
|
+
- [Reproducible parser and fragmentation benchmarks](docs/BENCHMARKS.md)
|
|
342
|
+
- [Executable examples](examples)
|
|
343
|
+
- [Stackline open-source catalog](https://alexandro.net/docs/open-source/)
|
|
344
|
+
|
|
345
|
+
The examples ship in the npm tarball. Network examples expose functions and do
|
|
346
|
+
not send requests during installation or import.
|
|
347
|
+
|
|
338
348
|
## License
|
|
339
349
|
|
|
340
350
|
[MIT](LICENSE) Copyright 2026 Alexandro Paixao Marques.
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/index.min.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @stackline/sse v1.0.
|
|
1
|
+
/*! @stackline/sse v1.0.1 | MIT */
|
|
2
2
|
var StacklineSSE=(()=>{var Le=Object.defineProperty;var Ze=Object.getOwnPropertyDescriptor;var Ge=Object.getOwnPropertyNames;var et=Object.prototype.hasOwnProperty;var tt=(e,t)=>{for(var r in t)Le(e,r,{get:t[r],enumerable:!0})},rt=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of Ge(t))!et.call(e,a)&&a!==r&&Le(e,a,{get:()=>t[a],enumerable:!(n=Ze(t,a))||n.enumerable});return e};var nt=e=>rt(Le({},"__esModule",{value:!0}),e);var Ut={};tt(Ut,{EVENT_STREAM_CONTENT_TYPE:()=>U,EventStreamContentType:()=>ye,SSEEncodeError:()=>g,SSEError:()=>_,SSEHTTPError:()=>H,SSEParseError:()=>O,SSEReplayError:()=>q,SSERetryError:()=>J,SSETimeoutError:()=>M,consumeSSE:()=>le,createDecoderStream:()=>Re,createEncoderStream:()=>be,createParser:()=>G,createSSEChannel:()=>Ae,decodeJSON:()=>Te,decodeSSE:()=>ee,default:()=>Mt,encode:()=>Se,encodeComment:()=>we,encodeJSON:()=>W,encodeSSE:()=>D,eventStreamResponse:()=>Ne,fetchEventSource:()=>_e,fetchSSE:()=>de,toAsyncIterable:()=>fe});var _=class extends Error{constructor(t,r,n){super(t),this.name=new.target.name,this.code=r,n&&n.cause!==void 0&&(this.cause=n.cause)}},O=class extends _{constructor(t,r={}){super(t,r.code||"ERR_SSE_PARSE",r),this.fatal=r.fatal===!0,r.field!==void 0&&(this.field=r.field),r.line!==void 0&&(this.line=r.line),r.limit!==void 0&&(this.limit=r.limit)}},g=class extends _{constructor(t,r,n){super(t,"ERR_SSE_ENCODE",{cause:n}),this.field=r}},H=class extends _{constructor(t,r,n="ERR_SSE_HTTP"){super(t,n),this.response=r,this.status=r&&typeof r.status=="number"?r.status:0}},M=class extends _{constructor(t,r,n){super(`SSE ${t} timeout after ${r}ms`,"ERR_SSE_TIMEOUT",{cause:n}),this.phase=t,this.timeout=r}},J=class extends _{constructor(t,r){super(`SSE retry budget exhausted after ${t} attempt${t===1?"":"s"}`,"ERR_SSE_RETRY",{cause:r}),this.attempts=t}},q=class extends _{constructor(){super("The request body cannot be replayed. Provide bodyFactory or disable reconnection.","ERR_SSE_BODY_REPLAY")}};var ot=1024*1024,at=1024*1024,he=10,Fe=13,Z=32,it=65279,De="\0",ce=()=>{};function G(e={}){if(typeof e=="function"&&(e={onEvent:e}),!e||typeof e!="object")throw new TypeError("Parser configuration must be an object or event callback");let t=typeof e.onEvent=="function"?e.onEvent:ce,r=typeof e.onComment=="function"?e.onComment:ce,n=typeof e.onRetry=="function"?e.onRetry:ce,a=typeof e.onError=="function"?e.onError:ce,i=typeof e.onId=="function"?e.onId:ce,f=e.strict===!0,m=e.fatalUTF8===!0,u=$e(e.maxEventSize===void 0?e.maxBufferSize:e.maxEventSize,ot,"maxEventSize"),y=$e(e.maxLineLength,at,"maxLineLength"),p,l,T=[],h=0,S=!1,v=!0,I=[],A=0,w,E=ke(e.lastEventId),b=E,x=!1,L=!1,F=0,d=0,$=0,B=0,ae=0;function P(o){if(L)throw new O("Parser is terminated; call reset() before feeding more data",{code:"ERR_SSE_TERMINATED",fatal:!0});if(typeof o=="string"){if(l==="bytes")throw j("Cannot mix string and byte chunks in one parser stream","ERR_SSE_CHUNK_TYPE");return l="string",d+=o.length,ie(o),ne}if(!(o instanceof Uint8Array))throw new TypeError("SSE parser chunks must be strings or Uint8Array values");if(l==="string")throw j("Cannot mix string and byte chunks in one parser stream","ERR_SSE_CHUNK_TYPE");l="bytes",F+=o.byteLength;let c;try{c=Xe().decode(o,{stream:!0})}catch(s){throw s instanceof O?s:j("Invalid UTF-8 in the SSE stream","ERR_SSE_UTF8",{cause:s})}return d+=c.length,ie(c),ne}function K(){if(L)return ne;if(p)try{let o=p.decode();d+=o.length,ie(o)}catch(o){throw j("Invalid UTF-8 at the end of the SSE stream","ERR_SSE_UTF8",{cause:o})}return Qe(),re(),ne}function Oe(o={}){o&&o.consume&&h>0&&N(te());let s=!!(o&&o.preserveLastEventId)?b:ke(e.lastEventId);return p=void 0,l=void 0,T=[],h=0,S=!1,v=!0,I=[],A=0,w=void 0,E=s,b=s,x=!1,L=!1,F=0,d=0,$=0,B=0,ae=0,ne}function ie(o){let c=o;if(v&&c.length>0&&(v=!1,c.charCodeAt(0)===it&&(c=c.slice(1))),c.length===0)return;if(!S&&c.indexOf("\r")===-1){Ee(c);return}let s=0,C=0;for(S&&(S=!1,c.charCodeAt(0)===he&&(s=1,C=1));s<c.length;s++){let R=c.charCodeAt(s);R!==he&&R!==Fe||(Y(c.slice(C,s)),N(te()),re(),R===Fe&&(c.charCodeAt(s+1)===he?s++:s+1===c.length&&(S=!0)),C=s+1)}Y(c.slice(C))}function Ee(o){let c=0,s=o.indexOf(`
|
|
3
3
|
`);for(;s!==-1;){if(h===0&&I.length===0&&o.charCodeAt(s+1)===he&&ft(o,c)){let C=o.charCodeAt(c+5)===Z?c+6:c+5,R=o.slice(C,s);if(s-c>y)throw j(`SSE line exceeded ${y} characters`,"ERR_SSE_LINE_LIMIT",{limit:y});Q(R.length+1),Ke(R),c=s+2,s=o.indexOf(`
|
|
4
4
|
`,c);continue}if(h>0)Y(o.slice(c,s)),N(te()),re();else{if(s-c>y)throw j(`SSE line exceeded ${y} characters`,"ERR_SSE_LINE_LIMIT",{limit:y});N(o.slice(c,s))}c=s+1,s=o.indexOf(`
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Goals
|
|
4
|
+
|
|
5
|
+
`@stackline/sse` treats SSE as one protocol with five composable surfaces:
|
|
6
|
+
|
|
7
|
+
1. incremental wire parser;
|
|
8
|
+
2. async iterable and TransformStream adapters;
|
|
9
|
+
3. injection-resistant encoder;
|
|
10
|
+
4. reconnecting Fetch client;
|
|
11
|
+
5. backpressure-aware server output.
|
|
12
|
+
|
|
13
|
+
The runtime has no package dependencies and no Node-specific imports.
|
|
14
|
+
|
|
15
|
+
## Parser state machine
|
|
16
|
+
|
|
17
|
+
The parser stores line fragments rather than repeatedly concatenating an
|
|
18
|
+
unterminated line. This keeps one-character chunk fragmentation linear. LF-only
|
|
19
|
+
input uses an `indexOf` fast path; CR and CRLF use the complete state machine.
|
|
20
|
+
|
|
21
|
+
State is separated into:
|
|
22
|
+
|
|
23
|
+
- pending line fragments;
|
|
24
|
+
- current data lines and event type;
|
|
25
|
+
- pending ID field;
|
|
26
|
+
- committed `lastEventId`;
|
|
27
|
+
- counters and configured limits.
|
|
28
|
+
|
|
29
|
+
An ID is committed at a blank line even if the block has no data. Incomplete
|
|
30
|
+
EOF data is discarded as required by the WHATWG algorithm.
|
|
31
|
+
|
|
32
|
+
## Backpressure
|
|
33
|
+
|
|
34
|
+
`decodeSSE` reads one source chunk only when the consumer advances its async
|
|
35
|
+
iterator. Large source chunks are admitted in 16 KiB slices and callback bursts
|
|
36
|
+
are bounded. The server iterable helper asks its source for one item per stream
|
|
37
|
+
pull. Push channels expose `send(): boolean` and `ready`.
|
|
38
|
+
|
|
39
|
+
## Client lifecycle
|
|
40
|
+
|
|
41
|
+
Each connection attempt owns an AbortController. External cancellation,
|
|
42
|
+
connect timeout, idle timeout, and total timeout converge on that controller.
|
|
43
|
+
Rejected response bodies are cancelled before retry. A user callback failure is
|
|
44
|
+
wrapped as `ERR_SSE_CALLBACK` and is never classified as a network failure.
|
|
45
|
+
Custom Fetch implementations, body factories, and iterators are raced against
|
|
46
|
+
the attempt signal so a non-cooperative adapter cannot hold a timeout open. A
|
|
47
|
+
response that resolves after timeout is cancelled on arrival.
|
|
48
|
+
|
|
49
|
+
Resume state is advanced only by committed parser IDs. Retry delay precedence
|
|
50
|
+
is HTTP `Retry-After`, server `retry:`, then client exponential backoff.
|
|
51
|
+
|
|
52
|
+
Streaming request bodies are not replayed. Applications can provide a
|
|
53
|
+
`bodyFactory` to create one body per attempt; the factory receives the attempt
|
|
54
|
+
signal. Headers from a Request input are preserved unless explicitly replaced.
|
|
55
|
+
|
|
56
|
+
## Encoder boundary
|
|
57
|
+
|
|
58
|
+
Data and comments can span lines and are encoded one field per line. Event names
|
|
59
|
+
cannot contain CR or LF. IDs also cannot contain NUL, matching the value space
|
|
60
|
+
of `Last-Event-ID`. These checks prevent field and header injection.
|
|
61
|
+
|
|
62
|
+
## Build outputs
|
|
63
|
+
|
|
64
|
+
One source graph produces:
|
|
65
|
+
|
|
66
|
+
- `dist/index.js` for ESM;
|
|
67
|
+
- `dist/index.cjs` for CommonJS;
|
|
68
|
+
- `dist/index.min.js` for browser globals;
|
|
69
|
+
- declarations for old and conditional-export-aware TypeScript versions.
|
|
70
|
+
|
|
71
|
+
All outputs are generated from the same commit and packed once for release.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Benchmark Methodology
|
|
2
|
+
|
|
3
|
+
The checked-in benchmark compares the incremental parser with
|
|
4
|
+
`eventsource-parser@3.1.1` using the same 50,000-event document. It measures a
|
|
5
|
+
single document, 16 KiB chunks, encoding throughput, and an adversarial stream
|
|
6
|
+
split one character at a time.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm ci
|
|
10
|
+
npm run benchmark
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Both parsers are warmed before measurement and must produce the expected event
|
|
14
|
+
count. The adversarial case is reported separately because fragmented input is
|
|
15
|
+
a complexity and memory-safety concern, not only a throughput concern.
|
|
16
|
+
|
|
17
|
+
Results depend on Node.js, CPU, chunk shape, event size, callbacks, and enabled
|
|
18
|
+
limits. Publish the complete command, environment, and raw JSON output. Do not
|
|
19
|
+
compare unlike parser, client, encoder, and server contracts as if they were
|
|
20
|
+
the same operation.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Compatibility
|
|
2
|
+
|
|
3
|
+
## JavaScript runtimes
|
|
4
|
+
|
|
5
|
+
Parser and encoder APIs require JavaScript ES2018 and `TextDecoder` only when
|
|
6
|
+
byte chunks are used. The declared Node.js floor is 14.17.
|
|
7
|
+
|
|
8
|
+
Fetch client requirements:
|
|
9
|
+
|
|
10
|
+
- `fetch` and `AbortController`;
|
|
11
|
+
- a response body exposed as Web Stream or async iterable.
|
|
12
|
+
|
|
13
|
+
Server helper requirements:
|
|
14
|
+
|
|
15
|
+
- `ReadableStream` and `TextEncoder`;
|
|
16
|
+
- `Response` when creating HTTP responses.
|
|
17
|
+
|
|
18
|
+
Node.js 14 and 16 users can inject a Fetch implementation. Parser and encoder
|
|
19
|
+
usage does not require a polyfill.
|
|
20
|
+
|
|
21
|
+
## TypeScript
|
|
22
|
+
|
|
23
|
+
CI compiles clean install fixtures with TypeScript 3.9.10, 4.7.4, 4.9.5,
|
|
24
|
+
5.9.3, 6.0.2, and 7.0.2. Declarations avoid requiring DOM library types by
|
|
25
|
+
using structural Fetch and Web Stream interfaces.
|
|
26
|
+
|
|
27
|
+
## eventsource-parser surface
|
|
28
|
+
|
|
29
|
+
Supported familiar fields:
|
|
30
|
+
|
|
31
|
+
- `createParser({ onEvent, onRetry, onComment, onError })`;
|
|
32
|
+
- `feed(chunk)` and `reset({ consume })`;
|
|
33
|
+
- `maxBufferSize` as an alias for the event limit;
|
|
34
|
+
- event `data`, `event`, and `id` fields.
|
|
35
|
+
|
|
36
|
+
Differences:
|
|
37
|
+
|
|
38
|
+
- byte chunks are accepted directly;
|
|
39
|
+
- security limits are enabled by default;
|
|
40
|
+
- `lastEventId` is included and ID-only blocks advance resume state;
|
|
41
|
+
- fatal limits throw immediately and terminate the parser.
|
|
42
|
+
- unknown fields and invalid retries call `onError` only with `strict: true`;
|
|
43
|
+
- `maxBufferSize` aliases the accumulated event limit, while line length has a
|
|
44
|
+
separate `maxLineLength` limit.
|
|
45
|
+
|
|
46
|
+
## @microsoft/fetch-event-source surface
|
|
47
|
+
|
|
48
|
+
Supported callback names are `onopen`, `onmessage`, `onclose`, and `onerror`.
|
|
49
|
+
Fetch options, custom methods, headers, bodies, and custom Fetch functions pass
|
|
50
|
+
through. The package does not automatically disconnect when a document becomes
|
|
51
|
+
hidden, so `openWhenHidden` has no behavioral effect.
|
|
52
|
+
|
|
53
|
+
## Protocol limits
|
|
54
|
+
|
|
55
|
+
SSE is UTF-8 only. This package does not implement WebSocket framing, HTTP
|
|
56
|
+
server transports, compression, JSON schema validation, or provider-specific
|
|
57
|
+
AI event models.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Integration Recipes
|
|
2
|
+
|
|
3
|
+
## OpenAI Responses
|
|
4
|
+
|
|
5
|
+
Use `fetchSSE` when the application wants direct control over streamed
|
|
6
|
+
Responses without adding a provider SDK. Pass the bearer token, JSON request,
|
|
7
|
+
finite retry budget, and an idle timeout. See
|
|
8
|
+
[`examples/openai-responses.mjs`](../examples/openai-responses.mjs).
|
|
9
|
+
|
|
10
|
+
## Anthropic Messages
|
|
11
|
+
|
|
12
|
+
Anthropic emits named SSE events. Preserve `event.event` alongside the parsed
|
|
13
|
+
JSON payload so message-start, content-block, delta, and stop events remain
|
|
14
|
+
distinguishable. See
|
|
15
|
+
[`examples/anthropic-messages.mjs`](../examples/anthropic-messages.mjs).
|
|
16
|
+
|
|
17
|
+
## Browser or Node client
|
|
18
|
+
|
|
19
|
+
`fetchSSE` accepts standard Fetch options plus retry, connect, idle, and total
|
|
20
|
+
timeouts. Node.js 18 and newer provide Fetch. Node.js 14 and 16 can inject a
|
|
21
|
+
compatible implementation with `options.fetch`.
|
|
22
|
+
|
|
23
|
+
## Cloudflare Workers and edge handlers
|
|
24
|
+
|
|
25
|
+
Return `eventStreamResponse(events())` from a Fetch-style handler. The helper
|
|
26
|
+
sets the event-stream content type and cache headers while consuming an async
|
|
27
|
+
iterable with backpressure. See
|
|
28
|
+
[`examples/server-edge.mjs`](../examples/server-edge.mjs).
|
|
29
|
+
|
|
30
|
+
## Existing parser imports
|
|
31
|
+
|
|
32
|
+
For a staged migration that keeps `eventsource-parser` imports:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install eventsource-parser@npm:@stackline/sse
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For a staged migration that keeps `@microsoft/fetch-event-source` imports:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install @microsoft/fetch-event-source@npm:@stackline/sse
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Commit the changed lockfile and run the complete consumer suite. Compatibility
|
|
45
|
+
aliases reduce source changes but do not remove the need to review documented
|
|
46
|
+
hardening differences and finite retry policies.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Market Research
|
|
2
|
+
|
|
3
|
+
Research date: 2026-08-16.
|
|
4
|
+
|
|
5
|
+
## Decision
|
|
6
|
+
|
|
7
|
+
The selected need is a unified, safe Server-Sent Events toolkit. SSE is used by
|
|
8
|
+
AI response streaming, live dashboards, notifications, build logs, and edge
|
|
9
|
+
applications. Existing adoption is large, while the common solution remains
|
|
10
|
+
fragmented across parser, polyfill/client, encoder, and custom retry code.
|
|
11
|
+
|
|
12
|
+
## Download evidence
|
|
13
|
+
|
|
14
|
+
Counts below come from the public npm downloads API for two fixed 30-day
|
|
15
|
+
windows. They measure package download activity, including CI and transitive
|
|
16
|
+
installs. They are not unique users and must not be treated as a download
|
|
17
|
+
forecast for this package.
|
|
18
|
+
|
|
19
|
+
| Package | 2026-07-17 to 2026-08-15 | Prior 30 days | Change |
|
|
20
|
+
| --- | ---: | ---: | ---: |
|
|
21
|
+
| `eventsource-parser` | 239,814,210 | 213,287,465 | +12.44% |
|
|
22
|
+
| `eventsource` | 203,093,267 | 176,499,445 | +15.07% |
|
|
23
|
+
| `eventsource-client` | 809,871 | 832,191 | -2.68% |
|
|
24
|
+
| `eventsource-encoder` | 186,321 | 121,896 | +52.85% |
|
|
25
|
+
| `@microsoft/fetch-event-source` | 11,218,110 | 10,963,830 | +2.32% |
|
|
26
|
+
| `eventsource-polyfill` | 757,091 | 749,470 | +1.02% |
|
|
27
|
+
| Combined activity | 455,878,870 | 402,454,297 | +13.27% |
|
|
28
|
+
|
|
29
|
+
The combined row contains overlap because applications may install several of
|
|
30
|
+
these packages. The overlap is part of the product observation: users often
|
|
31
|
+
need more than one package to cover the protocol lifecycle.
|
|
32
|
+
|
|
33
|
+
API source examples:
|
|
34
|
+
|
|
35
|
+
- `https://api.npmjs.org/downloads/point/2026-07-17:2026-08-15/eventsource-parser`
|
|
36
|
+
- `https://api.npmjs.org/downloads/point/2026-07-17:2026-08-15/eventsource`
|
|
37
|
+
- scoped names use URL encoding in the same endpoint.
|
|
38
|
+
|
|
39
|
+
## Standards and demand signals
|
|
40
|
+
|
|
41
|
+
- The WHATWG HTML Living Standard defines UTF-8 decoding, CR/LF handling,
|
|
42
|
+
field processing, resume IDs, reconnect behavior, and HTTP 204 termination.
|
|
43
|
+
- OpenAI documents server-sent events as the transport for streamed Responses.
|
|
44
|
+
- Native browser EventSource cannot send POST bodies or arbitrary auth headers.
|
|
45
|
+
- `@microsoft/fetch-event-source` addresses Fetch flexibility but its published
|
|
46
|
+
2.0.1 release is five years old at the research date.
|
|
47
|
+
- `eventsource-parser` is current and fast, but intentionally only a parser.
|
|
48
|
+
|
|
49
|
+
Primary references:
|
|
50
|
+
|
|
51
|
+
- [WHATWG Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html)
|
|
52
|
+
- [OpenAI streaming events](https://platform.openai.com/docs/api-reference/responses-streaming)
|
|
53
|
+
- [npm eventsource-parser](https://www.npmjs.com/package/eventsource-parser)
|
|
54
|
+
- [npm eventsource](https://www.npmjs.com/package/eventsource)
|
|
55
|
+
- [npm @microsoft/fetch-event-source](https://www.npmjs.com/package/@microsoft/fetch-event-source)
|
|
56
|
+
|
|
57
|
+
## Competitor analysis
|
|
58
|
+
|
|
59
|
+
| Capability | Parser package | EventSource client | Microsoft Fetch client | `@stackline/sse` |
|
|
60
|
+
| --- | --- | --- | --- | --- |
|
|
61
|
+
| Incremental parser | Yes | Dependency | Internal | Yes |
|
|
62
|
+
| Encoder | Separate package | No | No | Yes |
|
|
63
|
+
| Async iterator | Stream adapter | Yes | No | Yes |
|
|
64
|
+
| POST and custom headers | Retrieval-agnostic | Yes | Yes | Yes |
|
|
65
|
+
| Resume after ID-only block | Not exposed | Limited by parser events | Limited | Yes |
|
|
66
|
+
| Default memory limits | Optional | Parser-dependent | No | Yes |
|
|
67
|
+
| Retry-After and jitter | No | Basic reconnect | Custom callback | Yes |
|
|
68
|
+
| Connect, idle, total timeout | No | No | Custom | Yes |
|
|
69
|
+
| Non-replayable body guard | No | No | No | Yes |
|
|
70
|
+
| Safe encoder field validation | Separate | No | No | Yes |
|
|
71
|
+
| Server Response helper | No | No | No | Yes |
|
|
72
|
+
| Runtime dependencies | 0 | 1 | 0 | 0 |
|
|
73
|
+
|
|
74
|
+
This table describes documented public behavior at the research date. It is not
|
|
75
|
+
a claim that competing projects are unsafe or unsuitable.
|
|
76
|
+
|
|
77
|
+
## Product thesis
|
|
78
|
+
|
|
79
|
+
The opportunity is not another EventSource polyfill. It is one standards-based
|
|
80
|
+
protocol toolkit with consistent safety, cancellation, observability hooks,
|
|
81
|
+
backpressure, and migration paths on both sides of the HTTP connection.
|
|
82
|
+
|
|
83
|
+
Success requires documentation and ecosystem trust. Download volume is a
|
|
84
|
+
market signal, not a guarantee of adoption.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
## Preconditions
|
|
4
|
+
|
|
5
|
+
- `main` is the only development branch.
|
|
6
|
+
- Version, changelog, documentation, and browser metadata agree.
|
|
7
|
+
- Runtime dependencies remain zero or an exception is documented.
|
|
8
|
+
- Local tests, GitHub CI, and CodeQL pass.
|
|
9
|
+
- npm and Verdaccio authentication are verified.
|
|
10
|
+
|
|
11
|
+
## Release gate
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm ci
|
|
15
|
+
npm test
|
|
16
|
+
npm run test:attw
|
|
17
|
+
npm run audit:dependencies
|
|
18
|
+
npm pack --dry-run
|
|
19
|
+
npm run benchmark
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Immutable artifact
|
|
23
|
+
|
|
24
|
+
Build and pack once. Publish the same bytes everywhere.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
version="$(node -p "require('./package.json').version")"
|
|
28
|
+
mkdir -p "release/$version"
|
|
29
|
+
npm pack --ignore-scripts --pack-destination "release/$version"
|
|
30
|
+
(cd "release/$version" && sha512sum *.tgz > SHA512SUMS)
|
|
31
|
+
npm sbom --omit=dev --sbom-format cyclonedx > "release/$version/sbom.cdx.json"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Run `scripts/smoke-install.mjs` against the retained tarball.
|
|
35
|
+
|
|
36
|
+
## Registry sequence
|
|
37
|
+
|
|
38
|
+
1. Publish the retained tarball to Verdaccio.
|
|
39
|
+
2. Download it anonymously and compare SHA-512.
|
|
40
|
+
3. Smoke install directly and through both documented aliases.
|
|
41
|
+
4. Run `publish.yml` with the expected SHA-512. The trusted workflow rebuilds
|
|
42
|
+
the reviewed commit and stops unless its tarball is byte-identical.
|
|
43
|
+
5. Let the workflow publish with npm provenance, then download anonymously and
|
|
44
|
+
compare SHA-512 again.
|
|
45
|
+
6. Verify `latest`, registry signatures, audit output, and package visibility.
|
|
46
|
+
|
|
47
|
+
Do not rebuild between publications.
|
|
48
|
+
|
|
49
|
+
## GitHub and documentation
|
|
50
|
+
|
|
51
|
+
Tag the exact tested commit. Attach tarball, checksum, and CycloneDX SBOM to the
|
|
52
|
+
GitHub release. Deploy only the project documentation directory under
|
|
53
|
+
`/docs/vanilla/sse/`; do not delete unrelated documentation trees. Verify the
|
|
54
|
+
interactive site on desktop and mobile before announcing the release.
|
|
55
|
+
|
|
56
|
+
## Rollback
|
|
57
|
+
|
|
58
|
+
npm releases are immutable. Deprecate a defective version with a precise
|
|
59
|
+
message and publish a patch from the last known-good source. Unpublish only for
|
|
60
|
+
a compelling legal or security emergency.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { fetchSSE } from '@stackline/sse';
|
|
2
|
+
|
|
3
|
+
const received = [];
|
|
4
|
+
for await (const event of fetchSSE('https://api.example.com/stream', {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
headers: {
|
|
7
|
+
Authorization: `Bearer ${process.env.API_TOKEN}`,
|
|
8
|
+
'Content-Type': 'application/json'
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify({ stream: true }),
|
|
11
|
+
fetch: async () => ({
|
|
12
|
+
body: chunks('data: {"delta":"Hello"}\n\ndata: [DONE]\n\n'),
|
|
13
|
+
headers: { get: (name) => name.toLowerCase() === 'content-type' ? 'text/event-stream' : null },
|
|
14
|
+
status: 200
|
|
15
|
+
}),
|
|
16
|
+
retry: { retries: 0 }
|
|
17
|
+
})) {
|
|
18
|
+
if (event.data === '[DONE]') break;
|
|
19
|
+
received.push(JSON.parse(event.data));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(received);
|
|
23
|
+
|
|
24
|
+
async function* chunks(document) {
|
|
25
|
+
const bytes = new TextEncoder().encode(document);
|
|
26
|
+
yield bytes.subarray(0, 13);
|
|
27
|
+
yield bytes.subarray(13);
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { fetchSSE } from '@stackline/sse';
|
|
2
|
+
|
|
3
|
+
export async function* streamAnthropic(prompt, apiKey = process.env.ANTHROPIC_API_KEY) {
|
|
4
|
+
if (!apiKey) throw new Error('Set ANTHROPIC_API_KEY before calling streamAnthropic');
|
|
5
|
+
|
|
6
|
+
for await (const event of fetchSSE('https://api.anthropic.com/v1/messages', {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: {
|
|
9
|
+
'anthropic-version': '2023-06-01',
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
'x-api-key': apiKey
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify({
|
|
14
|
+
max_tokens: 1024,
|
|
15
|
+
messages: [{ role: 'user', content: prompt }],
|
|
16
|
+
model: 'your-model',
|
|
17
|
+
stream: true
|
|
18
|
+
}),
|
|
19
|
+
idleTimeout: 45_000,
|
|
20
|
+
retry: { retries: 3 }
|
|
21
|
+
})) {
|
|
22
|
+
yield { event: event.event, payload: JSON.parse(event.data) };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { fetchSSE } from '@stackline/sse';
|
|
2
|
+
|
|
3
|
+
export async function* streamOpenAI(input, apiKey = process.env.OPENAI_API_KEY) {
|
|
4
|
+
if (!apiKey) throw new Error('Set OPENAI_API_KEY before calling streamOpenAI');
|
|
5
|
+
|
|
6
|
+
for await (const event of fetchSSE('https://api.openai.com/v1/responses', {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: {
|
|
9
|
+
Authorization: `Bearer ${apiKey}`,
|
|
10
|
+
'Content-Type': 'application/json'
|
|
11
|
+
},
|
|
12
|
+
body: JSON.stringify({ model: 'your-model', input, stream: true }),
|
|
13
|
+
idleTimeout: 45_000,
|
|
14
|
+
retry: { retries: 3 }
|
|
15
|
+
})) {
|
|
16
|
+
yield JSON.parse(event.data);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { eventStreamResponse } from '@stackline/sse';
|
|
2
|
+
|
|
3
|
+
async function* events() {
|
|
4
|
+
yield { event: 'ready', data: 'connected', id: '1' };
|
|
5
|
+
yield { event: 'message', data: 'hello', id: '2' };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function GET() {
|
|
9
|
+
return eventStreamResponse(events());
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackline/sse",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Universal, spec-correct and memory-safe Server-Sent Events toolkit for AI streaming, browsers, servers and edge runtimes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sse",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
},
|
|
62
62
|
"files": [
|
|
63
63
|
"dist",
|
|
64
|
+
"docs",
|
|
65
|
+
"examples",
|
|
64
66
|
"CHANGELOG.md",
|
|
65
67
|
"CONTRIBUTING.md",
|
|
66
68
|
"LICENSE",
|
|
@@ -75,10 +77,11 @@
|
|
|
75
77
|
"clean": "node scripts/clean.mjs",
|
|
76
78
|
"build": "node scripts/build.mjs",
|
|
77
79
|
"lint": "eslint . && node scripts/check-markdown.mjs",
|
|
78
|
-
"test": "npm run build && npm run lint && npm run test:coverage && npm run test:types && npm run test:package && npm run test:install && npm run test:docs",
|
|
80
|
+
"test": "npm run build && npm run lint && npm run test:coverage && npm run test:types && npm run test:examples && npm run test:package && npm run test:install && npm run test:docs",
|
|
79
81
|
"test:unit": "node --test --test-reporter=spec test/*.test.mjs",
|
|
80
82
|
"test:coverage": "c8 --all --src src --check-coverage --lines 100 --functions 100 --statements 100 --branches 95 node --test test/parser.test.mjs test/encoder.test.mjs test/iterable.test.mjs test/client.test.mjs test/server.test.mjs test/security.test.mjs",
|
|
81
83
|
"test:types": "node scripts/test-types.mjs",
|
|
84
|
+
"test:examples": "node examples/parser.mjs && node examples/ai-client.mjs && node examples/server-edge.mjs && node examples/openai-responses.mjs && node examples/anthropic-messages.mjs",
|
|
82
85
|
"test:package": "node --test test/package.test.mjs && node scripts/check-dist.mjs && publint",
|
|
83
86
|
"test:install": "node scripts/smoke-install.mjs",
|
|
84
87
|
"test:docs": "npm run docs:build && node scripts/check-docs.mjs",
|