@songfreedom/sse-client 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +301 -2
- package/batch.d.ts +39 -0
- package/batch.d.ts.map +1 -0
- package/batch.js +44 -0
- package/batch.js.map +1 -0
- package/client.d.ts +197 -0
- package/client.d.ts.map +1 -0
- package/client.js +625 -0
- package/client.js.map +1 -0
- package/errors.d.ts +33 -0
- package/errors.d.ts.map +1 -0
- package/errors.js +47 -0
- package/errors.js.map +1 -0
- package/index.d.ts +13 -0
- package/index.d.ts.map +1 -0
- package/index.js +7 -0
- package/index.js.map +1 -0
- package/package.json +47 -3
- package/parser.d.ts +35 -0
- package/parser.d.ts.map +1 -0
- package/parser.js +118 -0
- package/parser.js.map +1 -0
- package/share.d.ts +29 -0
- package/share.d.ts.map +1 -0
- package/share.js +67 -0
- package/share.js.map +1 -0
- package/stream.d.ts +24 -0
- package/stream.d.ts.map +1 -0
- package/stream.js +65 -0
- package/stream.js.map +1 -0
- package/types.d.ts +22 -0
- package/types.d.ts.map +1 -0
- package/types.js +2 -0
- package/types.js.map +1 -0
- package/vue.d.ts +45 -0
- package/vue.d.ts.map +1 -0
- package/vue.js +72 -0
- package/vue.js.map +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 songfreedom
|
|
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/README.md
CHANGED
|
@@ -1,3 +1,302 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @songfreedom/sse-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@songfreedom/sse-client)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://bundlephobia.com/package/@songfreedom/sse-client)
|
|
6
|
+
|
|
7
|
+
A small, dependency-free Server-Sent Events client for the browser.
|
|
8
|
+
`fetch` transport, typed event routing, automatic reconnection with `Last-Event-ID` resume,
|
|
9
|
+
an idle watchdog and shared connections.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { SSEClient } from "@songfreedom/sse-client";
|
|
13
|
+
|
|
14
|
+
const client = new SSEClient({
|
|
15
|
+
url: "/api/push/subscribe",
|
|
16
|
+
body: { topics: ["alarm"] },
|
|
17
|
+
headers: () => ({ authorization: `Bearer ${getToken()}` }),
|
|
18
|
+
parse: (raw) => JSON.parse(raw),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
client.on("ALARM", (alarm) => console.log(alarm.level));
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Why not `EventSource`?
|
|
25
|
+
|
|
26
|
+
| Option | Problem |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| Native `EventSource` | Cannot set request headers (no way to send an auth token), GET only, no request body, no control over reconnection |
|
|
29
|
+
| [`@microsoft/fetch-event-source`](https://github.com/Azure/fetch-event-source) | Archived. Non-2xx responses must be rejected by hand in `onopen`, otherwise it silently reconnects forever; fixed retry interval; no `Last-Event-ID` persistence |
|
|
30
|
+
| `axios` | Cannot read a streaming response in the browser (`responseType: "stream"` is Node-only). Concatenating `responseText` in `onDownloadProgress` corrupts multi-byte characters |
|
|
31
|
+
|
|
32
|
+
This library is roughly 3 kB gzipped, has no runtime dependencies, and only needs `fetch`
|
|
33
|
+
and `ReadableStream`.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **POST + JSON body**, arbitrary headers, and header functions re-evaluated on every reconnect
|
|
38
|
+
- **Reconnects** with exponential backoff and jitter; terminal errors (401/403/404) stop immediately
|
|
39
|
+
- **Resume** with `Last-Event-ID`, so a reconnect can pick up where it left off
|
|
40
|
+
- **Typed event routing**: one connection serves many subscribers, `on()` infers payload types from an event map
|
|
41
|
+
- **Idle watchdog** for streams that go quiet, and **proactive rotation** for connections that stay open but stop receiving data
|
|
42
|
+
- **Offline-aware**: pauses while the network is down, reconnects as soon as it is back
|
|
43
|
+
- **Pluggable retry policy**, including async policies such as "refresh the token, then retry"
|
|
44
|
+
- **Shared connections** by key with reference counting
|
|
45
|
+
- Optional **Vue 3** composable in a separate entry point
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm add @songfreedom/sse-client
|
|
51
|
+
# or
|
|
52
|
+
npm install @songfreedom/sse-client
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The Vue entry needs `vue` as a peer dependency (it is optional, so non-Vue users do not install it):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pnpm add @songfreedom/sse-client vue
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Requires a browser with `fetch` and `ReadableStream` (all evergreen browsers), or Node 18+ for SSR and tests.
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
### Framework-agnostic
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { SSEClient } from "@songfreedom/sse-client";
|
|
69
|
+
|
|
70
|
+
interface Events {
|
|
71
|
+
ALARM: { level: "high" | "low"; deviceId: string };
|
|
72
|
+
IOT: { objectId: string; value: string };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const client = new SSEClient<Events>({
|
|
76
|
+
url: `${gateway}/server-event/sse/subscribe`,
|
|
77
|
+
method: "POST",
|
|
78
|
+
body: [{ subscribeType: "SE", instanceType: "PROJECT" }],
|
|
79
|
+
headers: () => authHeaders(), // re-evaluated before every attempt
|
|
80
|
+
parse: (raw) => JSON.parse(raw),
|
|
81
|
+
idleTimeout: 45_000, // if the server sends a heartbeat every 15s
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const off = client.on("ALARM", (alarm) => {
|
|
85
|
+
// `alarm` is typed as Events["ALARM"]
|
|
86
|
+
console.log(alarm.deviceId);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Later
|
|
90
|
+
off();
|
|
91
|
+
client.close();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Vue 3
|
|
95
|
+
|
|
96
|
+
```vue
|
|
97
|
+
<script setup lang="ts">
|
|
98
|
+
import { computed } from "vue";
|
|
99
|
+
import { useSSE } from "@songfreedom/sse-client/vue";
|
|
100
|
+
|
|
101
|
+
interface Events {
|
|
102
|
+
ALARM: { level: "high" | "low"; deviceId: string };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { status, lastEvent, on } = useSSE<Events>({
|
|
106
|
+
url: "/api/push/alarm",
|
|
107
|
+
body: { topics: ["alarm"] },
|
|
108
|
+
parse: (raw) => JSON.parse(raw),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
on("ALARM", (alarm) => {
|
|
112
|
+
// unsubscribed automatically on unmount
|
|
113
|
+
console.log(alarm.level);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const connected = computed(() => status.value === "open");
|
|
117
|
+
</script>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`useSSE` adds two things on top of the client: a ring buffer of recent messages
|
|
121
|
+
(`lastEvent`, `events`, `maxEvents`) and scope-bound cleanup — unmounting the component,
|
|
122
|
+
or a qiankun sub-app, closes the connection.
|
|
123
|
+
|
|
124
|
+
## API
|
|
125
|
+
|
|
126
|
+
### `new SSEClient(options)`
|
|
127
|
+
|
|
128
|
+
| Option | Type | Default | Description |
|
|
129
|
+
| --- | --- | --- | --- |
|
|
130
|
+
| `url` | `string` | — | Request URL, including your gateway prefix |
|
|
131
|
+
| `method` | `"GET" \| "POST"` | `"POST"` | HTTP method |
|
|
132
|
+
| `body` | `unknown` | — | Serialized as JSON; strings, `Blob` and `FormData` are sent as-is |
|
|
133
|
+
| `headers` | `HeadersInit \| () => HeadersInit \| Promise<HeadersInit>` | — | Pass a function to recompute headers on every (re)connect |
|
|
134
|
+
| `parse` | `(raw: string) => unknown` | identity | Payload decoder |
|
|
135
|
+
| `idleTimeout` | `number` | `0` | Reconnect when no bytes arrive for this long. Set it to ~3× the heartbeat interval |
|
|
136
|
+
| `rotateInterval` | `number` | `0` | Reconnect on a schedule, even if the connection looks healthy |
|
|
137
|
+
| `retry` | `(ctx) => number \| false \| Promise<number \| false>` | — | Custom retry policy, overrides the built-in backoff |
|
|
138
|
+
| `maxRetry` | `number` | `Infinity` | Retry limit (only when `retry` is omitted) |
|
|
139
|
+
| `retryBaseDelay` | `number` | `1000` | Delay before the first retry (ms) |
|
|
140
|
+
| `retryMaxDelay` | `number` | `30000` | Upper bound for backoff (ms) |
|
|
141
|
+
| `reconnectOnEnd` | `boolean` | `true` | Reconnect when the server ends the stream normally |
|
|
142
|
+
| `stableAfter` | `number` | `10000` | An attempt that lived this long resets the retry counter |
|
|
143
|
+
| `strictContentType` | `boolean` | `true` | Require `content-type: text/event-stream` |
|
|
144
|
+
| `maxBufferSize` | `number` | `1MB` | Cap for a single message buffer (characters) |
|
|
145
|
+
| `endSignal` | `string` | — | Treat this `data` value as end-of-stream, e.g. `"[DONE]"` |
|
|
146
|
+
| `events` | `string[]` | — | Allow-list of event names |
|
|
147
|
+
| `pauseWhenHidden` | `boolean` | `false` | Suspend while the page is hidden, reconnect when visible |
|
|
148
|
+
| `autoStart` | `boolean` | `true` | Connect in the constructor |
|
|
149
|
+
| `fetch` | `FetchLike` | global `fetch` | Custom transport |
|
|
150
|
+
| `onHandlerError` | `(error) => void` | `console.error` | Fallback when a callback throws |
|
|
151
|
+
|
|
152
|
+
Lifecycle hooks can be passed as options (`onOpen`, `onMessage`, `onStatus`, `onError`,
|
|
153
|
+
`onClose`) or subscribed later; both forms are equivalent.
|
|
154
|
+
|
|
155
|
+
#### Properties
|
|
156
|
+
|
|
157
|
+
| Property | Type | Description |
|
|
158
|
+
| --- | --- | --- |
|
|
159
|
+
| `status` | `SSEStatus` | `idle` · `connecting` · `open` · `reconnecting` · `closed` |
|
|
160
|
+
| `closed` | `boolean` | Terminal state; the client will not reconnect |
|
|
161
|
+
| `attempts` | `number` | Consecutive retries; resets after a stable connection |
|
|
162
|
+
| `lastEventId` | `string` | Latest event id, sent back on reconnect |
|
|
163
|
+
| `done` | `Promise<void>` | Resolves when the connection terminates; never rejects |
|
|
164
|
+
|
|
165
|
+
#### Methods
|
|
166
|
+
|
|
167
|
+
| Method | Description |
|
|
168
|
+
| --- | --- |
|
|
169
|
+
| `start()` | Open the connection (no-op while running or suspended) |
|
|
170
|
+
| `close()` | Close and stop reconnecting |
|
|
171
|
+
| `restart()` | Close, then reconnect while keeping `lastEventId` |
|
|
172
|
+
| `on(event, handler)` | Subscribe to one event name; returns an unsubscribe function |
|
|
173
|
+
| `on("*", handler)` | Subscribe to every message |
|
|
174
|
+
| `once(event, handler)` | Subscribe for a single delivery |
|
|
175
|
+
| `onMessage` / `onStatus` / `onError` / `onOpen` / `onClose` | Lifecycle subscriptions |
|
|
176
|
+
|
|
177
|
+
### `SSEMessage`
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
interface SSEMessage<T = unknown> {
|
|
181
|
+
id: string; // event id, inherited from the previous event when omitted
|
|
182
|
+
event: string; // "message" when the server sends no `event:` field
|
|
183
|
+
data: T; // `parse` output
|
|
184
|
+
raw: string; // raw payload text
|
|
185
|
+
receivedAt: number; // client receive timestamp
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Error kinds
|
|
190
|
+
|
|
191
|
+
`SSEError.kind` is meant for logging and for branching inside a retry policy:
|
|
192
|
+
|
|
193
|
+
| `kind` | Meaning | Retried by default |
|
|
194
|
+
| --- | --- | --- |
|
|
195
|
+
| `http` | Non-2xx response (`status` is set) | Yes for 408/425/429/5xx, otherwise no |
|
|
196
|
+
| `network` | DNS failure, connection reset, … | Yes |
|
|
197
|
+
| `heartbeat` | Idle timeout, the connection looks zombie | Yes |
|
|
198
|
+
| `protocol` | Wrong `content-type`, buffer overflow, unreadable body | No |
|
|
199
|
+
| `aborted` | Interrupted (hidden page, offline, manual close) | No |
|
|
200
|
+
| `retry-exhausted` | Retry budget spent | No |
|
|
201
|
+
|
|
202
|
+
## Recipes
|
|
203
|
+
|
|
204
|
+
### Refresh the token on 401
|
|
205
|
+
|
|
206
|
+
A retry policy may be async, so the refresh can complete before the next attempt:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
let refreshed = false;
|
|
210
|
+
|
|
211
|
+
const client = new SSEClient({
|
|
212
|
+
url,
|
|
213
|
+
retry: async ({ error, attempt }) => {
|
|
214
|
+
if (error?.status === 401 && !refreshed) {
|
|
215
|
+
await refreshToken();
|
|
216
|
+
refreshed = true;
|
|
217
|
+
return 0; // retry immediately
|
|
218
|
+
}
|
|
219
|
+
return attempt > 5 ? false : Math.min(30_000, 1000 * 2 ** attempt);
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Share one connection
|
|
225
|
+
|
|
226
|
+
HTTP/1.1 allows six connections per origin, and a push channel holds one open indefinitely.
|
|
227
|
+
Share it instead of opening one per component:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { acquireSSEClient } from "@songfreedom/sse-client";
|
|
231
|
+
|
|
232
|
+
const { client, release } = acquireSSEClient("alarm", () => new SSEClient({ url, body }));
|
|
233
|
+
onScopeDispose(release); // closes when the last consumer releases
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Batch high-frequency updates
|
|
237
|
+
|
|
238
|
+
When the same entity is pushed every few hundred milliseconds, coalesce before writing to state:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import { createBatcher } from "@songfreedom/sse-client";
|
|
242
|
+
|
|
243
|
+
const batcher = createBatcher<string, Point>({
|
|
244
|
+
key: (point) => `${point.objectId}::${point.code}`,
|
|
245
|
+
interval: 300,
|
|
246
|
+
onFlush: (points) => store.applyPoints(points),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
client.on("IOT", (payload) => payload.data.forEach((p) => batcher.push(p)));
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Rotation vs reconnect
|
|
253
|
+
|
|
254
|
+
A connection can stay alive while the server-side subscription is already gone. Rotation
|
|
255
|
+
reconnects on a schedule (`rotateInterval`) to avoid such zombie connections, keeping
|
|
256
|
+
`lastEventId` so nothing is missed:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
new SSEClient({ url, rotateInterval: 30 * 60_000 });
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
This relies on the server honouring `Last-Event-ID`. An alternative is overlapping
|
|
263
|
+
connections (open a replacement, then close the old one), which avoids the gap but can
|
|
264
|
+
deliver duplicates when the server pushes per connection.
|
|
265
|
+
|
|
266
|
+
## Low-level building blocks
|
|
267
|
+
|
|
268
|
+
The client is assembled from small pieces that are exported for advanced use:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { createSSEParser, createSSEMessageStream, createBatcher } from "@songfreedom/sse-client";
|
|
272
|
+
|
|
273
|
+
// bytes → messages, as a standard TransformStream
|
|
274
|
+
const messages = response.body!.pipeThrough(createSSEMessageStream({ parse: JSON.parse }));
|
|
275
|
+
|
|
276
|
+
// raw frames, if you want to drive the loop yourself
|
|
277
|
+
const parser = createSSEParser({ onEvent: (event) => console.log(event) });
|
|
278
|
+
parser.feed("event: ping\ndata: 1\n\n");
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Server requirements
|
|
282
|
+
|
|
283
|
+
- Respond with `Content-Type: text/event-stream` (a `;charset=UTF-8` suffix is fine).
|
|
284
|
+
- Do not buffer the response: `proxy_buffering off; proxy_cache off;` at the gateway,
|
|
285
|
+
and do not gzip the stream.
|
|
286
|
+
- Send a heartbeat comment (`: ping\n\n`) every 15–30s, otherwise most load balancers
|
|
287
|
+
close the connection after 60s of silence. Pair it with `idleTimeout`.
|
|
288
|
+
- Support `Last-Event-ID` if you want gap-free reconnects; the client sends the header but
|
|
289
|
+
does not de-duplicate events.
|
|
290
|
+
|
|
291
|
+
## Design notes
|
|
292
|
+
|
|
293
|
+
- No business logic: the client does not know about tokens, stores, toasts or routing.
|
|
294
|
+
Authentication is injected through `headers`.
|
|
295
|
+
- Errors never escape as exceptions from callbacks; a throwing handler is isolated and
|
|
296
|
+
reported through `onHandlerError` while the stream keeps flowing.
|
|
297
|
+
- Timers are always cleared on close, and the connection loop only exits when the client is
|
|
298
|
+
finally closed, so suspending and resuming cannot leak a loop.
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
[MIT](./LICENSE)
|
package/batch.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coalescing window for high-frequency streams.
|
|
3
|
+
*
|
|
4
|
+
* A single entity is often pushed several times within a few hundred
|
|
5
|
+
* milliseconds (IoT points, device state). Writing every message to state
|
|
6
|
+
* causes render storms, so messages are merged by a business key while keeping
|
|
7
|
+
* the latest value, and flushed once per window.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const batcher = createBatcher<number, Point>({
|
|
11
|
+
* key: (p) => p.pointId,
|
|
12
|
+
* interval: 300,
|
|
13
|
+
* onFlush: (points) => store.applyPoints(points),
|
|
14
|
+
* });
|
|
15
|
+
* client.on("IOT", (p) => batcher.push(p));
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export interface BatcherOptions<K, V> {
|
|
19
|
+
/** Business key used to merge values; only the last value per key survives. */
|
|
20
|
+
key: (value: V) => K;
|
|
21
|
+
/** Coalescing window in milliseconds (default 300). */
|
|
22
|
+
interval?: number;
|
|
23
|
+
/** Called once per window with the latest value per key, in first-seen order. */
|
|
24
|
+
onFlush: (values: V[]) => void;
|
|
25
|
+
}
|
|
26
|
+
export interface Batcher<V> {
|
|
27
|
+
/** Accept a value; replaces any pending value with the same key. */
|
|
28
|
+
push: (value: V) => void;
|
|
29
|
+
/** Flush immediately and cancel the pending window. */
|
|
30
|
+
flush: () => void;
|
|
31
|
+
/** Drop pending values without flushing. */
|
|
32
|
+
clear: () => void;
|
|
33
|
+
/** Flush and release the timer (call on unmount). */
|
|
34
|
+
dispose: () => void;
|
|
35
|
+
/** Number of values waiting to be flushed. */
|
|
36
|
+
readonly pending: number;
|
|
37
|
+
}
|
|
38
|
+
export declare function createBatcher<K, V>(options: BatcherOptions<K, V>): Batcher<V>;
|
|
39
|
+
//# sourceMappingURL=batch.d.ts.map
|
package/batch.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,EAAE,CAAC;IAClC,+EAA+E;IAC/E,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;IACrB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,OAAO,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,OAAO,CAAC,CAAC;IACxB,oEAAoE;IACpE,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IACzB,uDAAuD;IACvD,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,qDAAqD;IACrD,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA4C7E"}
|
package/batch.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function createBatcher(options) {
|
|
2
|
+
const interval = options.interval ?? 300;
|
|
3
|
+
const buffer = new Map();
|
|
4
|
+
let timer = null;
|
|
5
|
+
const flush = () => {
|
|
6
|
+
if (timer) {
|
|
7
|
+
clearTimeout(timer);
|
|
8
|
+
timer = null;
|
|
9
|
+
}
|
|
10
|
+
if (!buffer.size) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const values = [...buffer.values()];
|
|
14
|
+
buffer.clear();
|
|
15
|
+
options.onFlush(values);
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
push(value) {
|
|
19
|
+
buffer.set(options.key(value), value);
|
|
20
|
+
if (timer) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
timer = setTimeout(() => {
|
|
24
|
+
timer = null;
|
|
25
|
+
flush();
|
|
26
|
+
}, interval);
|
|
27
|
+
},
|
|
28
|
+
flush,
|
|
29
|
+
clear() {
|
|
30
|
+
if (timer) {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
timer = null;
|
|
33
|
+
}
|
|
34
|
+
buffer.clear();
|
|
35
|
+
},
|
|
36
|
+
dispose() {
|
|
37
|
+
flush();
|
|
38
|
+
},
|
|
39
|
+
get pending() {
|
|
40
|
+
return buffer.size;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=batch.js.map
|
package/batch.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.js","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAuCA,MAAM,UAAU,aAAa,CAAO,OAA6B;IAC/D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,GAAG,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAQ,CAAC;IAC/B,IAAI,KAAK,GAAyC,IAAI,CAAC;IAEvD,MAAM,KAAK,GAAG,GAAG,EAAE;QACjB,IAAI,KAAK,EAAE,CAAC;YACV,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,KAAK,GAAG,IAAI,CAAC;QACf,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,CAAC,KAAQ;YACX,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC;YACtC,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO;YACT,CAAC;YACD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBACtB,KAAK,GAAG,IAAI,CAAC;gBACb,KAAK,EAAE,CAAC;YACV,CAAC,EAAE,QAAQ,CAAC,CAAC;QACf,CAAC;QACD,KAAK;QACL,KAAK;YACH,IAAI,KAAK,EAAE,CAAC;gBACV,YAAY,CAAC,KAAK,CAAC,CAAC;gBACpB,KAAK,GAAG,IAAI,CAAC;YACf,CAAC;YACD,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,CAAC;QACD,OAAO;YACL,KAAK,EAAE,CAAC;QACV,CAAC;QACD,IAAI,OAAO;YACT,OAAO,MAAM,CAAC,IAAI,CAAC;QACrB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/client.d.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { SSEError } from "./errors.js";
|
|
2
|
+
import type { SSECloseReason, SSEEventMap, SSEMessage, SSEStatus, Unsubscribe } from "./types.js";
|
|
3
|
+
/** Same shape as `fetch`; inject a custom transport for SSR, tests or signed requests. */
|
|
4
|
+
export type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
|
|
5
|
+
/** Context passed to a retry policy. */
|
|
6
|
+
export interface RetryContext {
|
|
7
|
+
/** Retry number, starting at 1. */
|
|
8
|
+
attempt: number;
|
|
9
|
+
/** Why the reconnect happens; `undefined` when the server closed the stream normally. */
|
|
10
|
+
error?: SSEError;
|
|
11
|
+
/** How long the attempt was alive, in milliseconds. */
|
|
12
|
+
elapsed: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Custom retry policy: return the delay in milliseconds before the next attempt,
|
|
16
|
+
* or `false` to give up. May be async, which makes flows such as
|
|
17
|
+
* "401 → refresh the token → retry" straightforward. When omitted, the built-in
|
|
18
|
+
* exponential backoff with jitter and a max attempt count is used.
|
|
19
|
+
*/
|
|
20
|
+
export type RetryPolicy = (context: RetryContext) => number | false | Promise<number | false>;
|
|
21
|
+
/** Client options. */
|
|
22
|
+
export interface SSEClientOptions {
|
|
23
|
+
/** Request URL (include your gateway prefix yourself). */
|
|
24
|
+
url: string;
|
|
25
|
+
/** HTTP method; business push endpoints usually use POST. */
|
|
26
|
+
method?: "GET" | "POST";
|
|
27
|
+
/** Request body. Serialized as JSON, or passed through when it is a string / Blob / FormData. */
|
|
28
|
+
body?: unknown;
|
|
29
|
+
/**
|
|
30
|
+
* Extra headers. Pass a function to recompute them before every (re)connect,
|
|
31
|
+
* which is the recommended way to pick up a refreshed token:
|
|
32
|
+
* `() => getAuthHeaders()`.
|
|
33
|
+
*/
|
|
34
|
+
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
35
|
+
/** Payload decoder, e.g. `(raw) => JSON.parse(raw)`. Defaults to the raw string. */
|
|
36
|
+
parse?: (raw: string) => unknown;
|
|
37
|
+
/**
|
|
38
|
+
* Idle timeout in milliseconds: reconnect when no bytes arrived for this long.
|
|
39
|
+
* `0` disables it. Two to three times the server heartbeat interval is a good value.
|
|
40
|
+
*/
|
|
41
|
+
idleTimeout?: number;
|
|
42
|
+
/** Proactive rotation interval in milliseconds: reconnect on schedule to avoid zombie connections. `0` disables it. */
|
|
43
|
+
rotateInterval?: number;
|
|
44
|
+
/** Custom retry policy, overrides the built-in backoff. */
|
|
45
|
+
retry?: RetryPolicy;
|
|
46
|
+
/** Maximum retry count, unlimited by default (only used when `retry` is not provided). */
|
|
47
|
+
maxRetry?: number;
|
|
48
|
+
/** Delay before the first retry in milliseconds, defaults to 1000. */
|
|
49
|
+
retryBaseDelay?: number;
|
|
50
|
+
/** Upper bound for reconnect delays in milliseconds, defaults to 30000. */
|
|
51
|
+
retryMaxDelay?: number;
|
|
52
|
+
/** Reconnect when the server closes the stream normally, defaults to true. */
|
|
53
|
+
reconnectOnEnd?: boolean;
|
|
54
|
+
/** An attempt that lived at least this long resets the retry counter, defaults to 10000. */
|
|
55
|
+
stableAfter?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Require the response `content-type` to be `text/event-stream`, defaults to true.
|
|
58
|
+
* This catches silent failures such as a gateway returning a 200 HTML page;
|
|
59
|
+
* disable it for non-compliant backends.
|
|
60
|
+
*/
|
|
61
|
+
strictContentType?: boolean;
|
|
62
|
+
/** Cap for a single message buffer in characters, defaults to 1MB, guards against runaway streams. */
|
|
63
|
+
maxBufferSize?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Stream-end sentinel: when `data` equals this value the stream is treated as
|
|
66
|
+
* finished (OpenAI / AI SDK style `[DONE]`) and no reconnect happens.
|
|
67
|
+
* Disabled by default.
|
|
68
|
+
*/
|
|
69
|
+
endSignal?: string;
|
|
70
|
+
/** Event allow-list: only these event names are dispatched. All events are dispatched when omitted. */
|
|
71
|
+
events?: string[];
|
|
72
|
+
/**
|
|
73
|
+
* Suspend the connection while the page is hidden and reconnect when it becomes
|
|
74
|
+
* visible again. Defaults to false; recommended for WebViews and mobile, where
|
|
75
|
+
* the OS freezes background pages and leaves zombie connections behind.
|
|
76
|
+
*/
|
|
77
|
+
pauseWhenHidden?: boolean;
|
|
78
|
+
/** Connect in the constructor, defaults to true. */
|
|
79
|
+
autoStart?: boolean;
|
|
80
|
+
/** Inject a fetch implementation, defaults to the global `fetch`. */
|
|
81
|
+
fetch?: FetchLike;
|
|
82
|
+
/** Fallback when a handler throws, defaults to `console.error`. */
|
|
83
|
+
onHandlerError?: (error: unknown) => void;
|
|
84
|
+
/** Fired once the connection is established (response headers received). Same as `client.onOpen()`. */
|
|
85
|
+
onOpen?: (response: Response) => void;
|
|
86
|
+
/** Fired for every message. Same as `client.onMessage()`. */
|
|
87
|
+
onMessage?: (message: SSEMessage) => void;
|
|
88
|
+
/** Fired on every state change. Same as `client.onStatus()`. */
|
|
89
|
+
onStatus?: (status: SSEStatus) => void;
|
|
90
|
+
/** Fired for every failed attempt. Same as `client.onError()`. */
|
|
91
|
+
onError?: (error: SSEError, attempt: number) => void;
|
|
92
|
+
/** Fired once when the connection is finally closed. Same as `client.onClose()`. */
|
|
93
|
+
onClose?: (reason: SSECloseReason, error?: SSEError) => void;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Server-Sent Events client.
|
|
97
|
+
*
|
|
98
|
+
* - Built on `fetch` + `ReadableStream`, so custom headers and POST bodies work
|
|
99
|
+
* - Reconnects automatically (exponential backoff with jitter); terminal errors
|
|
100
|
+
* such as 401 stop immediately
|
|
101
|
+
* - Resumes with `Last-Event-ID`
|
|
102
|
+
* - Routes events by name to multiple subscribers, so one connection serves many components
|
|
103
|
+
* - Idle watchdog, proactive rotation, offline/background suspension, shared
|
|
104
|
+
* connections (see `acquireSSEClient`)
|
|
105
|
+
*/
|
|
106
|
+
export declare class SSEClient<E extends object = SSEEventMap> {
|
|
107
|
+
private readonly options;
|
|
108
|
+
private readonly parser;
|
|
109
|
+
private readonly eventFilter;
|
|
110
|
+
private state;
|
|
111
|
+
private finished;
|
|
112
|
+
private running;
|
|
113
|
+
private suspended;
|
|
114
|
+
private attempt;
|
|
115
|
+
private lastEventIdValue;
|
|
116
|
+
private serverRetryDelay;
|
|
117
|
+
private idleTimedOut;
|
|
118
|
+
private rotating;
|
|
119
|
+
private idleTimer;
|
|
120
|
+
private rotateTimer;
|
|
121
|
+
private cancelController;
|
|
122
|
+
private attemptController;
|
|
123
|
+
private runPromise;
|
|
124
|
+
private resumeWaiter;
|
|
125
|
+
private lifecycleBound;
|
|
126
|
+
private onVisibilityChange?;
|
|
127
|
+
private onOffline?;
|
|
128
|
+
private onOnline?;
|
|
129
|
+
private readonly messageHandlers;
|
|
130
|
+
private readonly eventHandlers;
|
|
131
|
+
private readonly statusHandlers;
|
|
132
|
+
private readonly errorHandlers;
|
|
133
|
+
private readonly openHandlers;
|
|
134
|
+
private readonly closeHandlers;
|
|
135
|
+
constructor(options: SSEClientOptions);
|
|
136
|
+
/** Current state. */
|
|
137
|
+
get status(): SSEStatus;
|
|
138
|
+
/** Whether the client is finally closed and will not reconnect. */
|
|
139
|
+
get closed(): boolean;
|
|
140
|
+
/** Consecutive retry count; resets once a connection proves stable. */
|
|
141
|
+
get attempts(): number;
|
|
142
|
+
/** Latest event id, sent back as `Last-Event-ID` on reconnect. */
|
|
143
|
+
get lastEventId(): string;
|
|
144
|
+
/** Resolves when the connection terminates (closed or retries exhausted). Never rejects. */
|
|
145
|
+
get done(): Promise<void>;
|
|
146
|
+
/** Open the connection. No-op while already running or suspended. */
|
|
147
|
+
start(): this;
|
|
148
|
+
/** Close the connection and stop reconnecting. */
|
|
149
|
+
close(): void;
|
|
150
|
+
/** Reconnect after closing, keeping `lastEventId` so the server can resume. */
|
|
151
|
+
restart(): this;
|
|
152
|
+
/** Subscribe to one event name; returns an unsubscribe function. */
|
|
153
|
+
on<K extends Extract<keyof E, string>>(event: K, handler: (data: E[K], message: SSEMessage) => void): Unsubscribe;
|
|
154
|
+
/** Subscribe to every message by passing `"*"`. */
|
|
155
|
+
on(event: "*", handler: (data: unknown, message: SSEMessage) => void): Unsubscribe;
|
|
156
|
+
/** Subscribe to one event name for a single delivery. */
|
|
157
|
+
once<K extends Extract<keyof E, string>>(event: K, handler: (data: E[K], message: SSEMessage) => void): Unsubscribe;
|
|
158
|
+
/** Subscribe to every message regardless of event name. */
|
|
159
|
+
onMessage(handler: (message: SSEMessage) => void): Unsubscribe;
|
|
160
|
+
onStatus(handler: (status: SSEStatus) => void): Unsubscribe;
|
|
161
|
+
onError(handler: (error: SSEError, attempt: number) => void): Unsubscribe;
|
|
162
|
+
onOpen(handler: (response: Response) => void): Unsubscribe;
|
|
163
|
+
onClose(handler: (reason: SSECloseReason, error?: SSEError) => void): Unsubscribe;
|
|
164
|
+
private get retryBaseDelay();
|
|
165
|
+
private get retryMaxDelay();
|
|
166
|
+
private get maxRetry();
|
|
167
|
+
private get stableAfter();
|
|
168
|
+
private get reconnectOnEnd();
|
|
169
|
+
private reset;
|
|
170
|
+
private connect;
|
|
171
|
+
/** Open one connection and read it until the stream ends (throws on failure). */
|
|
172
|
+
private openOnce;
|
|
173
|
+
private resolveHeaders;
|
|
174
|
+
private handleParsedEvent;
|
|
175
|
+
private dispatch;
|
|
176
|
+
private emit;
|
|
177
|
+
private safeCall;
|
|
178
|
+
private reportHandlerError;
|
|
179
|
+
private decideDelay;
|
|
180
|
+
/** Reconnect delay: the server's `retry:` wins, otherwise exponential backoff with ±20% jitter. */
|
|
181
|
+
private nextDelay;
|
|
182
|
+
private sleep;
|
|
183
|
+
private waitForResume;
|
|
184
|
+
private armIdle;
|
|
185
|
+
private armRotate;
|
|
186
|
+
private clearIdle;
|
|
187
|
+
private clearRotate;
|
|
188
|
+
private clearTimers;
|
|
189
|
+
private setState;
|
|
190
|
+
/** Network state is always observed: no reconnects while offline, an immediate one when back online. */
|
|
191
|
+
private bindLifecycle;
|
|
192
|
+
private unbindLifecycle;
|
|
193
|
+
private suspend;
|
|
194
|
+
private resume;
|
|
195
|
+
private finish;
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=client.d.ts.map
|
package/client.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|