@songfreedom/sse-client 0.1.0 → 0.1.2
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/README.md +213 -190
- package/batch.d.ts +13 -11
- package/batch.d.ts.map +1 -1
- package/batch.js.map +1 -1
- package/client.d.ts +70 -60
- package/client.d.ts.map +1 -1
- package/client.js +56 -45
- package/client.js.map +1 -1
- package/errors.d.ts +16 -13
- package/errors.d.ts.map +1 -1
- package/errors.js +20 -9
- package/errors.js.map +1 -1
- package/package.json +3 -4
- package/parser.d.ts +14 -13
- package/parser.d.ts.map +1 -1
- package/parser.js +14 -11
- package/parser.js.map +1 -1
- package/share.d.ts +10 -7
- package/share.d.ts.map +1 -1
- package/share.js +9 -6
- package/share.js.map +1 -1
- package/stream.d.ts +7 -5
- package/stream.d.ts.map +1 -1
- package/stream.js +12 -7
- package/stream.js.map +1 -1
- package/types.d.ts +10 -10
- package/types.d.ts.map +1 -1
- package/vue.d.ts +12 -12
- package/vue.d.ts.map +1 -1
- package/vue.js +4 -3
- package/vue.js.map +1 -1
package/client.d.ts
CHANGED
|
@@ -1,97 +1,107 @@
|
|
|
1
1
|
import { SSEError } from "./errors.js";
|
|
2
2
|
import type { SSECloseReason, SSEEventMap, SSEMessage, SSEStatus, Unsubscribe } from "./types.js";
|
|
3
|
-
/**
|
|
3
|
+
/** Same shape as `fetch`; inject a custom transport for SSR, tests or signed requests. */
|
|
4
4
|
export type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
|
|
5
|
-
/**
|
|
5
|
+
/** Context passed to a retry policy. */
|
|
6
6
|
export interface RetryContext {
|
|
7
|
-
/**
|
|
7
|
+
/** Retry number, starting at 1. */
|
|
8
8
|
attempt: number;
|
|
9
|
-
/**
|
|
9
|
+
/** Why the reconnect happens; `undefined` when the server closed the stream normally. */
|
|
10
10
|
error?: SSEError;
|
|
11
|
-
/**
|
|
11
|
+
/** How long the attempt was alive, in milliseconds. */
|
|
12
12
|
elapsed: number;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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.
|
|
18
19
|
*/
|
|
19
20
|
export type RetryPolicy = (context: RetryContext) => number | false | Promise<number | false>;
|
|
20
|
-
/**
|
|
21
|
+
/** Client options. */
|
|
21
22
|
export interface SSEClientOptions {
|
|
22
|
-
/**
|
|
23
|
+
/** Request URL (include your gateway prefix yourself). */
|
|
23
24
|
url: string;
|
|
24
|
-
/**
|
|
25
|
+
/** HTTP method; business push endpoints usually use POST. */
|
|
25
26
|
method?: "GET" | "POST";
|
|
26
|
-
/**
|
|
27
|
+
/** Request body. Serialized as JSON, or passed through when it is a string / Blob / FormData. */
|
|
27
28
|
body?: unknown;
|
|
28
29
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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()`.
|
|
31
33
|
*/
|
|
32
34
|
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
33
|
-
/**
|
|
35
|
+
/** Payload decoder, e.g. `(raw) => JSON.parse(raw)`. Defaults to the raw string. */
|
|
34
36
|
parse?: (raw: string) => unknown;
|
|
35
|
-
/**
|
|
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
|
+
*/
|
|
36
41
|
idleTimeout?: number;
|
|
37
|
-
/**
|
|
42
|
+
/** Proactive rotation interval in milliseconds: reconnect on schedule to avoid zombie connections. `0` disables it. */
|
|
38
43
|
rotateInterval?: number;
|
|
39
|
-
/**
|
|
44
|
+
/** Custom retry policy, overrides the built-in backoff. */
|
|
40
45
|
retry?: RetryPolicy;
|
|
41
|
-
/**
|
|
46
|
+
/** Maximum retry count, unlimited by default (only used when `retry` is not provided). */
|
|
42
47
|
maxRetry?: number;
|
|
43
|
-
/**
|
|
48
|
+
/** Delay before the first retry in milliseconds, defaults to 1000. */
|
|
44
49
|
retryBaseDelay?: number;
|
|
45
|
-
/**
|
|
50
|
+
/** Upper bound for reconnect delays in milliseconds, defaults to 30000. */
|
|
46
51
|
retryMaxDelay?: number;
|
|
47
|
-
/**
|
|
52
|
+
/** Reconnect when the server closes the stream normally, defaults to true. */
|
|
48
53
|
reconnectOnEnd?: boolean;
|
|
49
|
-
/**
|
|
54
|
+
/** An attempt that lived at least this long resets the retry counter, defaults to 10000. */
|
|
50
55
|
stableAfter?: number;
|
|
51
56
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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.
|
|
54
60
|
*/
|
|
55
61
|
strictContentType?: boolean;
|
|
56
|
-
/**
|
|
62
|
+
/** Cap for a single message buffer in characters, defaults to 1MB, guards against runaway streams. */
|
|
57
63
|
maxBufferSize?: number;
|
|
58
64
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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.
|
|
61
68
|
*/
|
|
62
69
|
endSignal?: string;
|
|
63
|
-
/**
|
|
70
|
+
/** Event allow-list: only these event names are dispatched. All events are dispatched when omitted. */
|
|
64
71
|
events?: string[];
|
|
65
72
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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.
|
|
68
76
|
*/
|
|
69
77
|
pauseWhenHidden?: boolean;
|
|
70
|
-
/**
|
|
78
|
+
/** Connect in the constructor, defaults to true. */
|
|
71
79
|
autoStart?: boolean;
|
|
72
|
-
/**
|
|
80
|
+
/** Inject a fetch implementation, defaults to the global `fetch`. */
|
|
73
81
|
fetch?: FetchLike;
|
|
74
|
-
/**
|
|
82
|
+
/** Fallback when a handler throws, defaults to `console.error`. */
|
|
75
83
|
onHandlerError?: (error: unknown) => void;
|
|
76
|
-
/**
|
|
84
|
+
/** Fired once the connection is established (response headers received). Same as `client.onOpen()`. */
|
|
77
85
|
onOpen?: (response: Response) => void;
|
|
78
|
-
/**
|
|
86
|
+
/** Fired for every message. Same as `client.onMessage()`. */
|
|
79
87
|
onMessage?: (message: SSEMessage) => void;
|
|
80
|
-
/**
|
|
88
|
+
/** Fired on every state change. Same as `client.onStatus()`. */
|
|
81
89
|
onStatus?: (status: SSEStatus) => void;
|
|
82
|
-
/**
|
|
90
|
+
/** Fired for every failed attempt. Same as `client.onError()`. */
|
|
83
91
|
onError?: (error: SSEError, attempt: number) => void;
|
|
84
|
-
/**
|
|
92
|
+
/** Fired once when the connection is finally closed. Same as `client.onClose()`. */
|
|
85
93
|
onClose?: (reason: SSECloseReason, error?: SSEError) => void;
|
|
86
94
|
}
|
|
87
95
|
/**
|
|
88
|
-
*
|
|
96
|
+
* Server-Sent Events client.
|
|
89
97
|
*
|
|
90
|
-
* -
|
|
91
|
-
* -
|
|
92
|
-
*
|
|
93
|
-
* -
|
|
94
|
-
* -
|
|
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`)
|
|
95
105
|
*/
|
|
96
106
|
export declare class SSEClient<E extends object = SSEEventMap> {
|
|
97
107
|
private readonly options;
|
|
@@ -123,29 +133,29 @@ export declare class SSEClient<E extends object = SSEEventMap> {
|
|
|
123
133
|
private readonly openHandlers;
|
|
124
134
|
private readonly closeHandlers;
|
|
125
135
|
constructor(options: SSEClientOptions);
|
|
126
|
-
/**
|
|
136
|
+
/** Current state. */
|
|
127
137
|
get status(): SSEStatus;
|
|
128
|
-
/**
|
|
138
|
+
/** Whether the client is finally closed and will not reconnect. */
|
|
129
139
|
get closed(): boolean;
|
|
130
|
-
/**
|
|
140
|
+
/** Consecutive retry count; resets once a connection proves stable. */
|
|
131
141
|
get attempts(): number;
|
|
132
|
-
/**
|
|
142
|
+
/** Latest event id, sent back as `Last-Event-ID` on reconnect. */
|
|
133
143
|
get lastEventId(): string;
|
|
134
|
-
/**
|
|
144
|
+
/** Resolves when the connection terminates (closed or retries exhausted). Never rejects. */
|
|
135
145
|
get done(): Promise<void>;
|
|
136
|
-
/**
|
|
146
|
+
/** Open the connection. No-op while already running or suspended. */
|
|
137
147
|
start(): this;
|
|
138
|
-
/**
|
|
148
|
+
/** Close the connection and stop reconnecting. */
|
|
139
149
|
close(): void;
|
|
140
|
-
/**
|
|
150
|
+
/** Reconnect after closing, keeping `lastEventId` so the server can resume. */
|
|
141
151
|
restart(): this;
|
|
142
|
-
/**
|
|
152
|
+
/** Subscribe to one event name; returns an unsubscribe function. */
|
|
143
153
|
on<K extends Extract<keyof E, string>>(event: K, handler: (data: E[K], message: SSEMessage) => void): Unsubscribe;
|
|
144
|
-
/**
|
|
154
|
+
/** Subscribe to every message by passing `"*"`. */
|
|
145
155
|
on(event: "*", handler: (data: unknown, message: SSEMessage) => void): Unsubscribe;
|
|
146
|
-
/**
|
|
156
|
+
/** Subscribe to one event name for a single delivery. */
|
|
147
157
|
once<K extends Extract<keyof E, string>>(event: K, handler: (data: E[K], message: SSEMessage) => void): Unsubscribe;
|
|
148
|
-
/**
|
|
158
|
+
/** Subscribe to every message regardless of event name. */
|
|
149
159
|
onMessage(handler: (message: SSEMessage) => void): Unsubscribe;
|
|
150
160
|
onStatus(handler: (status: SSEStatus) => void): Unsubscribe;
|
|
151
161
|
onError(handler: (error: SSEError, attempt: number) => void): Unsubscribe;
|
|
@@ -158,7 +168,7 @@ export declare class SSEClient<E extends object = SSEEventMap> {
|
|
|
158
168
|
private get reconnectOnEnd();
|
|
159
169
|
private reset;
|
|
160
170
|
private connect;
|
|
161
|
-
/**
|
|
171
|
+
/** Open one connection and read it until the stream ends (throws on failure). */
|
|
162
172
|
private openOnce;
|
|
163
173
|
private resolveHeaders;
|
|
164
174
|
private handleParsedEvent;
|
|
@@ -167,7 +177,7 @@ export declare class SSEClient<E extends object = SSEEventMap> {
|
|
|
167
177
|
private safeCall;
|
|
168
178
|
private reportHandlerError;
|
|
169
179
|
private decideDelay;
|
|
170
|
-
/**
|
|
180
|
+
/** Reconnect delay: the server's `retry:` wins, otherwise exponential backoff with ±20% jitter. */
|
|
171
181
|
private nextDelay;
|
|
172
182
|
private sleep;
|
|
173
183
|
private waitForResume;
|
|
@@ -177,7 +187,7 @@ export declare class SSEClient<E extends object = SSEEventMap> {
|
|
|
177
187
|
private clearRotate;
|
|
178
188
|
private clearTimers;
|
|
179
189
|
private setState;
|
|
180
|
-
/**
|
|
190
|
+
/** Network state is always observed: no reconnects while offline, an immediate one when back online. */
|
|
181
191
|
private bindLifecycle;
|
|
182
192
|
private unbindLifecycle;
|
|
183
193
|
private suspend;
|
package/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAiC,MAAM,aAAa,CAAC;AAOtE,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAElG,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/client.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { SSEError, isRetryableStatus, toSSEError } from "./errors.js";
|
|
2
2
|
import { DEFAULT_MAX_BUFFER_SIZE, createSSEParser, } from "./parser.js";
|
|
3
3
|
const EVENT_STREAM_CONTENT_TYPE = "text/event-stream";
|
|
4
|
-
/**
|
|
4
|
+
/** Prevents tight reconnect loops when the server drops the connection immediately. */
|
|
5
5
|
const MIN_RETRY_DELAY = 200;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Server-Sent Events client.
|
|
8
8
|
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
9
|
+
* - Built on `fetch` + `ReadableStream`, so custom headers and POST bodies work
|
|
10
|
+
* - Reconnects automatically (exponential backoff with jitter); terminal errors
|
|
11
|
+
* such as 401 stop immediately
|
|
12
|
+
* - Resumes with `Last-Event-ID`
|
|
13
|
+
* - Routes events by name to multiple subscribers, so one connection serves many components
|
|
14
|
+
* - Idle watchdog, proactive rotation, offline/background suspension, shared
|
|
15
|
+
* connections (see `acquireSSEClient`)
|
|
14
16
|
*/
|
|
15
17
|
export class SSEClient {
|
|
16
18
|
options;
|
|
@@ -43,7 +45,7 @@ export class SSEClient {
|
|
|
43
45
|
closeHandlers = new Set();
|
|
44
46
|
constructor(options) {
|
|
45
47
|
if (!options?.url) {
|
|
46
|
-
throw new Error("[sse-client]
|
|
48
|
+
throw new Error("[sse-client] missing required option: url");
|
|
47
49
|
}
|
|
48
50
|
this.options = options;
|
|
49
51
|
this.parser = createSSEParser({
|
|
@@ -68,27 +70,27 @@ export class SSEClient {
|
|
|
68
70
|
this.start();
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
|
-
/**
|
|
73
|
+
/** Current state. */
|
|
72
74
|
get status() {
|
|
73
75
|
return this.state;
|
|
74
76
|
}
|
|
75
|
-
/**
|
|
77
|
+
/** Whether the client is finally closed and will not reconnect. */
|
|
76
78
|
get closed() {
|
|
77
79
|
return this.finished;
|
|
78
80
|
}
|
|
79
|
-
/**
|
|
81
|
+
/** Consecutive retry count; resets once a connection proves stable. */
|
|
80
82
|
get attempts() {
|
|
81
83
|
return this.attempt;
|
|
82
84
|
}
|
|
83
|
-
/**
|
|
85
|
+
/** Latest event id, sent back as `Last-Event-ID` on reconnect. */
|
|
84
86
|
get lastEventId() {
|
|
85
87
|
return this.lastEventIdValue;
|
|
86
88
|
}
|
|
87
|
-
/**
|
|
89
|
+
/** Resolves when the connection terminates (closed or retries exhausted). Never rejects. */
|
|
88
90
|
get done() {
|
|
89
91
|
return this.runPromise ?? Promise.resolve();
|
|
90
92
|
}
|
|
91
|
-
/**
|
|
93
|
+
/** Open the connection. No-op while already running or suspended. */
|
|
92
94
|
start() {
|
|
93
95
|
if (this.running || this.suspended) {
|
|
94
96
|
return this;
|
|
@@ -96,7 +98,8 @@ export class SSEClient {
|
|
|
96
98
|
if (this.finished) {
|
|
97
99
|
this.reset();
|
|
98
100
|
}
|
|
99
|
-
//
|
|
101
|
+
// Do not request anything while the page is hidden; wait for visibilitychange.
|
|
102
|
+
// This is the usual case for WebView preloading.
|
|
100
103
|
if (this.options.pauseWhenHidden &&
|
|
101
104
|
typeof document !== "undefined" &&
|
|
102
105
|
document.hidden) {
|
|
@@ -108,7 +111,7 @@ export class SSEClient {
|
|
|
108
111
|
this.bindLifecycle();
|
|
109
112
|
this.runPromise = this.connect()
|
|
110
113
|
.catch((error) => {
|
|
111
|
-
//
|
|
114
|
+
// Safety net: a throwing callback must not become an unhandled rejection.
|
|
112
115
|
this.finish("error", toSSEError(error));
|
|
113
116
|
})
|
|
114
117
|
.finally(() => {
|
|
@@ -116,14 +119,14 @@ export class SSEClient {
|
|
|
116
119
|
});
|
|
117
120
|
return this;
|
|
118
121
|
}
|
|
119
|
-
/**
|
|
122
|
+
/** Close the connection and stop reconnecting. */
|
|
120
123
|
close() {
|
|
121
124
|
if (this.finished) {
|
|
122
125
|
return;
|
|
123
126
|
}
|
|
124
127
|
this.finish("user");
|
|
125
128
|
}
|
|
126
|
-
/**
|
|
129
|
+
/** Reconnect after closing, keeping `lastEventId` so the server can resume. */
|
|
127
130
|
restart() {
|
|
128
131
|
this.close();
|
|
129
132
|
return this.start();
|
|
@@ -146,7 +149,7 @@ export class SSEClient {
|
|
|
146
149
|
}
|
|
147
150
|
};
|
|
148
151
|
}
|
|
149
|
-
/**
|
|
152
|
+
/** Subscribe to one event name for a single delivery. */
|
|
150
153
|
once(event, handler) {
|
|
151
154
|
const off = this.on(event, (data, message) => {
|
|
152
155
|
off();
|
|
@@ -154,7 +157,7 @@ export class SSEClient {
|
|
|
154
157
|
});
|
|
155
158
|
return off;
|
|
156
159
|
}
|
|
157
|
-
/**
|
|
160
|
+
/** Subscribe to every message regardless of event name. */
|
|
158
161
|
onMessage(handler) {
|
|
159
162
|
return subscribe(this.messageHandlers, handler);
|
|
160
163
|
}
|
|
@@ -171,7 +174,7 @@ export class SSEClient {
|
|
|
171
174
|
return subscribe(this.closeHandlers, handler);
|
|
172
175
|
}
|
|
173
176
|
// ---------------------------------------------------------------------------
|
|
174
|
-
//
|
|
177
|
+
// Internals
|
|
175
178
|
// ---------------------------------------------------------------------------
|
|
176
179
|
get retryBaseDelay() {
|
|
177
180
|
return this.options.retryBaseDelay ?? 1000;
|
|
@@ -203,7 +206,7 @@ export class SSEClient {
|
|
|
203
206
|
async connect() {
|
|
204
207
|
while (!this.finished) {
|
|
205
208
|
if (this.suspended) {
|
|
206
|
-
//
|
|
209
|
+
// Do not reconnect while suspended; wait for resume() to wake us up.
|
|
207
210
|
await this.waitForResume();
|
|
208
211
|
continue;
|
|
209
212
|
}
|
|
@@ -227,19 +230,24 @@ export class SSEClient {
|
|
|
227
230
|
this.setState("idle");
|
|
228
231
|
continue;
|
|
229
232
|
}
|
|
230
|
-
//
|
|
233
|
+
// Proactive rotation: reconnect immediately, keeping lastEventId and skipping backoff.
|
|
231
234
|
if (this.rotating) {
|
|
232
235
|
this.rotating = false;
|
|
233
236
|
this.attempt = 0;
|
|
234
237
|
continue;
|
|
235
238
|
}
|
|
236
|
-
//
|
|
239
|
+
// Treat an idle timeout as a retryable network error so a missing
|
|
240
|
+
// heartbeat cannot leave a zombie connection behind.
|
|
237
241
|
if (this.idleTimedOut) {
|
|
238
242
|
this.idleTimedOut = false;
|
|
239
|
-
failure = new SSEError("SSE
|
|
243
|
+
failure = new SSEError("SSE idle timeout, reconnecting", {
|
|
244
|
+
kind: "heartbeat",
|
|
245
|
+
retryable: true,
|
|
246
|
+
});
|
|
240
247
|
}
|
|
241
248
|
const elapsed = Date.now() - startedAt;
|
|
242
|
-
//
|
|
249
|
+
// A connection that lived long enough means the failure was transient,
|
|
250
|
+
// so reset the retry budget.
|
|
243
251
|
if (elapsed >= this.stableAfter) {
|
|
244
252
|
this.attempt = 0;
|
|
245
253
|
}
|
|
@@ -264,7 +272,7 @@ export class SSEClient {
|
|
|
264
272
|
await this.sleep(delay);
|
|
265
273
|
}
|
|
266
274
|
}
|
|
267
|
-
/**
|
|
275
|
+
/** Open one connection and read it until the stream ends (throws on failure). */
|
|
268
276
|
async openOnce(signal) {
|
|
269
277
|
const { url, method = "POST", body, fetch: fetchImpl } = this.options;
|
|
270
278
|
const init = {
|
|
@@ -277,7 +285,7 @@ export class SSEClient {
|
|
|
277
285
|
};
|
|
278
286
|
const response = fetchImpl ? await fetchImpl(url, init) : await fetch(url, init);
|
|
279
287
|
if (!response.ok) {
|
|
280
|
-
throw new SSEError(`SSE
|
|
288
|
+
throw new SSEError(`SSE request failed: HTTP ${response.status}`, {
|
|
281
289
|
kind: "http",
|
|
282
290
|
status: response.status,
|
|
283
291
|
retryable: isRetryableStatus(response.status),
|
|
@@ -286,12 +294,13 @@ export class SSEClient {
|
|
|
286
294
|
if (this.options.strictContentType !== false) {
|
|
287
295
|
const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
288
296
|
if (!contentType.startsWith(EVENT_STREAM_CONTENT_TYPE)) {
|
|
289
|
-
//
|
|
290
|
-
|
|
297
|
+
// Two usual causes: a gateway/auth page returned HTML, or the backend
|
|
298
|
+
// did not declare produces.
|
|
299
|
+
throw new SSEError(`SSE response content-type is not ${EVENT_STREAM_CONTENT_TYPE} (got "${contentType || "(empty)"}")`, { kind: "protocol", status: response.status, retryable: false });
|
|
291
300
|
}
|
|
292
301
|
}
|
|
293
302
|
if (!response.body) {
|
|
294
|
-
throw new SSEError("
|
|
303
|
+
throw new SSEError("Streaming is not supported in this environment (response.body is null)", {
|
|
295
304
|
kind: "protocol",
|
|
296
305
|
status: response.status,
|
|
297
306
|
retryable: false,
|
|
@@ -302,7 +311,7 @@ export class SSEClient {
|
|
|
302
311
|
this.emit(this.openHandlers, response);
|
|
303
312
|
this.parser.reset();
|
|
304
313
|
const reader = response.body.getReader();
|
|
305
|
-
//
|
|
314
|
+
// Decode in streaming mode so multi-byte characters split across chunks survive.
|
|
306
315
|
const decoder = new TextDecoder("utf-8");
|
|
307
316
|
this.armIdle();
|
|
308
317
|
try {
|
|
@@ -314,8 +323,9 @@ export class SSEClient {
|
|
|
314
323
|
this.armIdle();
|
|
315
324
|
this.parser.feed(decoder.decode(value, { stream: true }));
|
|
316
325
|
if (this.parser.overflowed) {
|
|
317
|
-
//
|
|
318
|
-
|
|
326
|
+
// The server never terminates a line, or a single message is huge;
|
|
327
|
+
// reconnecting would not help.
|
|
328
|
+
throw new SSEError("SSE buffer size limit exceeded, connection reset", {
|
|
319
329
|
kind: "protocol",
|
|
320
330
|
retryable: false,
|
|
321
331
|
});
|
|
@@ -328,7 +338,7 @@ export class SSEClient {
|
|
|
328
338
|
reader.releaseLock();
|
|
329
339
|
}
|
|
330
340
|
catch {
|
|
331
|
-
// abort
|
|
341
|
+
// releaseLock can throw after an abort; that is fine.
|
|
332
342
|
}
|
|
333
343
|
this.clearIdle();
|
|
334
344
|
this.clearRotate();
|
|
@@ -350,7 +360,8 @@ export class SSEClient {
|
|
|
350
360
|
return resolved;
|
|
351
361
|
}
|
|
352
362
|
handleParsedEvent(event) {
|
|
353
|
-
//
|
|
363
|
+
// Check the end sentinel before parsing: `[DONE]` is not valid JSON and
|
|
364
|
+
// would make the parser throw.
|
|
354
365
|
if (this.options.endSignal !== undefined && event.data === this.options.endSignal) {
|
|
355
366
|
this.finish("end");
|
|
356
367
|
return;
|
|
@@ -370,7 +381,7 @@ export class SSEClient {
|
|
|
370
381
|
this.lastEventIdValue = id;
|
|
371
382
|
}
|
|
372
383
|
const name = event.event || "message";
|
|
373
|
-
//
|
|
384
|
+
// Filter after updating the id so the resume position never moves backwards.
|
|
374
385
|
if (this.eventFilter && !this.eventFilter.has(name)) {
|
|
375
386
|
return;
|
|
376
387
|
}
|
|
@@ -417,7 +428,7 @@ export class SSEClient {
|
|
|
417
428
|
this.options.onHandlerError(error);
|
|
418
429
|
return;
|
|
419
430
|
}
|
|
420
|
-
console.error("[sse-client]
|
|
431
|
+
console.error("[sse-client] a callback threw", error);
|
|
421
432
|
}
|
|
422
433
|
async decideDelay(context) {
|
|
423
434
|
if (this.options.retry) {
|
|
@@ -431,7 +442,7 @@ export class SSEClient {
|
|
|
431
442
|
}
|
|
432
443
|
return this.nextDelay(context.attempt);
|
|
433
444
|
}
|
|
434
|
-
/**
|
|
445
|
+
/** Reconnect delay: the server's `retry:` wins, otherwise exponential backoff with ±20% jitter. */
|
|
435
446
|
nextDelay(attempt) {
|
|
436
447
|
if (this.serverRetryDelay !== undefined) {
|
|
437
448
|
return Math.max(Math.min(this.serverRetryDelay, this.retryMaxDelay), MIN_RETRY_DELAY);
|
|
@@ -462,7 +473,7 @@ export class SSEClient {
|
|
|
462
473
|
return Promise.resolve();
|
|
463
474
|
}
|
|
464
475
|
return new Promise((resolve) => {
|
|
465
|
-
//
|
|
476
|
+
// Only resume() / finish() wake this up, so the loop never exits while suspended.
|
|
466
477
|
this.resumeWaiter = resolve;
|
|
467
478
|
});
|
|
468
479
|
}
|
|
@@ -512,9 +523,9 @@ export class SSEClient {
|
|
|
512
523
|
this.emit(this.statusHandlers, next);
|
|
513
524
|
}
|
|
514
525
|
// ---------------------------------------------------------------------------
|
|
515
|
-
//
|
|
526
|
+
// Offline / background handling
|
|
516
527
|
// ---------------------------------------------------------------------------
|
|
517
|
-
/**
|
|
528
|
+
/** Network state is always observed: no reconnects while offline, an immediate one when back online. */
|
|
518
529
|
bindLifecycle() {
|
|
519
530
|
if (this.lifecycleBound) {
|
|
520
531
|
return;
|
|
@@ -590,21 +601,21 @@ export class SSEClient {
|
|
|
590
601
|
this.unbindLifecycle();
|
|
591
602
|
this.cancelController.abort();
|
|
592
603
|
this.attemptController?.abort();
|
|
593
|
-
//
|
|
604
|
+
// Wake the connection loop in case it is waiting on a suspended state.
|
|
594
605
|
this.resumeWaiter?.();
|
|
595
606
|
this.resumeWaiter = null;
|
|
596
607
|
this.setState("closed");
|
|
597
608
|
this.emit(this.closeHandlers, reason, error);
|
|
598
609
|
}
|
|
599
610
|
}
|
|
600
|
-
/**
|
|
611
|
+
/** Serialize the body: strings / Blob / FormData pass through, everything else becomes JSON. */
|
|
601
612
|
function serializeBody(body) {
|
|
602
613
|
if (typeof body === "string" || body instanceof Blob || body instanceof FormData) {
|
|
603
614
|
return body;
|
|
604
615
|
}
|
|
605
616
|
return JSON.stringify(body);
|
|
606
617
|
}
|
|
607
|
-
/**
|
|
618
|
+
/** Shared subscribe/unsubscribe implementation. */
|
|
608
619
|
function subscribe(handlers, handler) {
|
|
609
620
|
handlers.add(handler);
|
|
610
621
|
return () => {
|