@meowpanel/api 0.13.0 → 0.14.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/esm/mod.d.ts +1 -1
- package/esm/mod.d.ts.map +1 -1
- package/esm/src/resources/servers/connection.d.ts +32 -5
- package/esm/src/resources/servers/connection.d.ts.map +1 -1
- package/esm/src/resources/servers/connection.js +107 -34
- package/esm/src/resources/servers/server.js +1 -1
- package/esm/src/sse.d.ts +63 -11
- package/esm/src/sse.d.ts.map +1 -1
- package/esm/src/sse.js +112 -31
- package/package.json +1 -1
- package/script/mod.d.ts +1 -1
- package/script/mod.d.ts.map +1 -1
- package/script/src/resources/servers/connection.d.ts +32 -5
- package/script/src/resources/servers/connection.d.ts.map +1 -1
- package/script/src/resources/servers/connection.js +107 -34
- package/script/src/resources/servers/server.js +1 -1
- package/script/src/sse.d.ts +63 -11
- package/script/src/sse.d.ts.map +1 -1
- package/script/src/sse.js +112 -31
package/script/src/sse.d.ts
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { TypedEmitter } from './emitter.js';
|
|
2
|
+
/**
|
|
3
|
+
* State of an {@link SseConnection}.
|
|
4
|
+
*
|
|
5
|
+
* `reconnecting` covers both the backoff wait and the retry request. `error` is terminal: the
|
|
6
|
+
* stream was refused with a status a retry cannot fix, or reconnecting is turned off.
|
|
7
|
+
*/
|
|
8
|
+
export type SseState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
|
|
9
|
+
/** Events emitted by {@link SseConnection}. */
|
|
10
|
+
export interface SseConnectionEvents {
|
|
11
|
+
/** The connection state changed. */
|
|
12
|
+
state: SseState;
|
|
13
|
+
}
|
|
14
|
+
/** Options for {@link SseConnection}. */
|
|
15
|
+
export interface SseOptions {
|
|
16
|
+
/** Reconnect after the stream drops or a connection attempt fails. Defaults to `true`. */
|
|
17
|
+
reconnect?: boolean;
|
|
18
|
+
/** Delay before the first reconnect attempt. Doubles per failed attempt. Defaults to 1 second. */
|
|
19
|
+
minReconnectDelayMs?: number;
|
|
20
|
+
/** Upper bound for the reconnect delay. Defaults to 30 seconds. */
|
|
21
|
+
maxReconnectDelayMs?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Treat the stream as dead after this long without receiving any bytes, and reconnect.
|
|
24
|
+
*
|
|
25
|
+
* A connection that dies without closing (lost network, dropped NAT entry) otherwise stays
|
|
26
|
+
* `connected` forever. Only set this against a server that sends periodic keepalives,
|
|
27
|
+
* comfortably above their interval. `0` disables it, which is the default.
|
|
28
|
+
*/
|
|
29
|
+
idleTimeoutMs?: number;
|
|
30
|
+
}
|
|
3
31
|
/**
|
|
4
32
|
* A generic, `fetch`-based Server-Sent Events client.
|
|
5
33
|
*
|
|
@@ -7,9 +35,13 @@ export type SseState = 'disconnected' | 'connecting' | 'connected' | 'error';
|
|
|
7
35
|
* header, making it compatible with bearer token auth in all environments
|
|
8
36
|
* (browser, Deno, Node.js).
|
|
9
37
|
*
|
|
38
|
+
* Like `EventSource`, it reconnects with exponential backoff whenever the stream ends without
|
|
39
|
+
* {@link disconnect} being called, including a clean close by the server.
|
|
40
|
+
*
|
|
10
41
|
* @example
|
|
11
42
|
* ```ts ignore
|
|
12
43
|
* const sse = new SseConnection('https://panel.example.com/v1/sse', token);
|
|
44
|
+
* sse.on('state', (state) => console.log('SSE is', state));
|
|
13
45
|
* sse.connect(
|
|
14
46
|
* (event, data) => console.log(event, data),
|
|
15
47
|
* (err) => console.error('SSE error:', err),
|
|
@@ -18,16 +50,22 @@ export type SseState = 'disconnected' | 'connecting' | 'connected' | 'error';
|
|
|
18
50
|
* sse.disconnect();
|
|
19
51
|
* ```
|
|
20
52
|
*/
|
|
21
|
-
export declare class SseConnection {
|
|
53
|
+
export declare class SseConnection extends TypedEmitter<SseConnectionEvents> {
|
|
22
54
|
private readonly url;
|
|
23
55
|
private token;
|
|
24
56
|
private abortController?;
|
|
57
|
+
private reconnectTimer?;
|
|
25
58
|
private _state;
|
|
59
|
+
private attempt;
|
|
60
|
+
private readonly options;
|
|
26
61
|
/**
|
|
27
|
-
* @param url
|
|
28
|
-
* @param token
|
|
62
|
+
* @param url The full URL to connect to.
|
|
63
|
+
* @param token Bearer token used in the `Authorization` header, or a function returning the
|
|
64
|
+
* current one. A function is read on every attempt, so reconnects pick up a
|
|
65
|
+
* refreshed token.
|
|
66
|
+
* @param options Reconnect and idle-timeout behavior.
|
|
29
67
|
*/
|
|
30
|
-
constructor(url: string, token: string);
|
|
68
|
+
constructor(url: string, token: string | (() => string), options?: SseOptions);
|
|
31
69
|
/** Update the bearer token for subsequent reconnections. */
|
|
32
70
|
updateToken(token: string): void;
|
|
33
71
|
/** Current connection state. */
|
|
@@ -37,16 +75,30 @@ export declare class SseConnection {
|
|
|
37
75
|
/**
|
|
38
76
|
* Open the SSE connection and begin streaming events.
|
|
39
77
|
*
|
|
40
|
-
* This method is **non-blocking**: it starts the connection and returns immediately
|
|
41
|
-
*
|
|
42
|
-
*
|
|
78
|
+
* This method is **non-blocking**: it starts the connection and returns immediately.
|
|
79
|
+
* Events are delivered asynchronously via the `onEvent` callback, and state changes via the
|
|
80
|
+
* `state` event.
|
|
43
81
|
*
|
|
44
82
|
* @param onEvent Callback invoked for each received SSE event, with the event name and raw data string.
|
|
45
|
-
* @param onError Optional callback invoked
|
|
83
|
+
* @param onError Optional callback invoked each time a connection attempt fails or an open stream
|
|
84
|
+
* is lost (not called on intentional disconnect). Reconnecting continues unless
|
|
85
|
+
* {@link state} is `error`.
|
|
46
86
|
*/
|
|
47
87
|
connect(onEvent: (event: string, data: string) => void, onError?: (error: Error) => void): void;
|
|
48
|
-
/** Close the SSE connection and stop receiving events. */
|
|
88
|
+
/** Close the SSE connection, cancel any pending reconnect, and stop receiving events. */
|
|
49
89
|
disconnect(): void;
|
|
90
|
+
private open;
|
|
91
|
+
/**
|
|
92
|
+
* Exponential backoff with jitter, so clients that lost the same server do not all retry at
|
|
93
|
+
* the same instant when it comes back.
|
|
94
|
+
*/
|
|
95
|
+
private nextReconnectDelay;
|
|
96
|
+
/**
|
|
97
|
+
* 401 is retryable because the token is re-read on every attempt and may have been refreshed
|
|
98
|
+
* in the meantime. Other client errors (403, 404, ...) will not change on their own.
|
|
99
|
+
*/
|
|
100
|
+
private static isRetryableStatus;
|
|
101
|
+
private setState;
|
|
50
102
|
/** Parse the raw `ReadableStream` body into SSE frames, calling `onEvent` for each complete frame. */
|
|
51
103
|
private readStream;
|
|
52
104
|
}
|
package/script/src/sse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/src/sse.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/src/sse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,OAAO,CAAC;AAE9F,+CAA+C;AAC/C,MAAM,WAAW,mBAAmB;IACnC,oCAAoC;IACpC,KAAK,EAAE,QAAQ,CAAC;CAChB;AAED,yCAAyC;AACzC,MAAM,WAAW,UAAU;IAC1B,0FAA0F;IAC1F,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kGAAkG;IAClG,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mEAAmE;IACnE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,aAAc,SAAQ,YAAY,CAAC,mBAAmB,CAAC;IAelE,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,KAAK;IAfd,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,cAAc,CAAC,CAAgC;IACvD,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAE/C;;;;;;OAMG;gBAEe,GAAG,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,EACtC,OAAO,GAAE,UAAe;IAYzB,4DAA4D;IACrD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIvC,gCAAgC;IAChC,IAAW,KAAK,IAAI,QAAQ,CAE3B;IAED,mDAAmD;IACnD,IAAW,WAAW,IAAI,OAAO,CAEhC;IAED;;;;;;;;;;;OAWG;IACI,OAAO,CACb,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,EAC9C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAC9B,IAAI;IAMP,yFAAyF;IAClF,UAAU,IAAI,IAAI;IAQzB,OAAO,CAAC,IAAI;IA2EZ;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAO1B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAIhC,OAAO,CAAC,QAAQ;IAMhB,sGAAsG;YACxF,UAAU;CAqCxB"}
|
package/script/src/sse.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SseConnection = void 0;
|
|
4
|
+
const emitter_js_1 = require("./emitter.js");
|
|
4
5
|
/**
|
|
5
6
|
* A generic, `fetch`-based Server-Sent Events client.
|
|
6
7
|
*
|
|
@@ -8,9 +9,13 @@ exports.SseConnection = void 0;
|
|
|
8
9
|
* header, making it compatible with bearer token auth in all environments
|
|
9
10
|
* (browser, Deno, Node.js).
|
|
10
11
|
*
|
|
12
|
+
* Like `EventSource`, it reconnects with exponential backoff whenever the stream ends without
|
|
13
|
+
* {@link disconnect} being called, including a clean close by the server.
|
|
14
|
+
*
|
|
11
15
|
* @example
|
|
12
16
|
* ```ts ignore
|
|
13
17
|
* const sse = new SseConnection('https://panel.example.com/v1/sse', token);
|
|
18
|
+
* sse.on('state', (state) => console.log('SSE is', state));
|
|
14
19
|
* sse.connect(
|
|
15
20
|
* (event, data) => console.log(event, data),
|
|
16
21
|
* (err) => console.error('SSE error:', err),
|
|
@@ -19,18 +24,32 @@ exports.SseConnection = void 0;
|
|
|
19
24
|
* sse.disconnect();
|
|
20
25
|
* ```
|
|
21
26
|
*/
|
|
22
|
-
class SseConnection {
|
|
27
|
+
class SseConnection extends emitter_js_1.TypedEmitter {
|
|
23
28
|
url;
|
|
24
29
|
token;
|
|
25
30
|
abortController;
|
|
31
|
+
reconnectTimer;
|
|
26
32
|
_state = 'disconnected';
|
|
33
|
+
attempt = 0;
|
|
34
|
+
options;
|
|
27
35
|
/**
|
|
28
|
-
* @param url
|
|
29
|
-
* @param token
|
|
36
|
+
* @param url The full URL to connect to.
|
|
37
|
+
* @param token Bearer token used in the `Authorization` header, or a function returning the
|
|
38
|
+
* current one. A function is read on every attempt, so reconnects pick up a
|
|
39
|
+
* refreshed token.
|
|
40
|
+
* @param options Reconnect and idle-timeout behavior.
|
|
30
41
|
*/
|
|
31
|
-
constructor(url, token) {
|
|
42
|
+
constructor(url, token, options = {}) {
|
|
43
|
+
super();
|
|
32
44
|
this.url = url;
|
|
33
45
|
this.token = token;
|
|
46
|
+
this.options = {
|
|
47
|
+
reconnect: true,
|
|
48
|
+
minReconnectDelayMs: 1_000,
|
|
49
|
+
maxReconnectDelayMs: 30_000,
|
|
50
|
+
idleTimeoutMs: 0,
|
|
51
|
+
...options,
|
|
52
|
+
};
|
|
34
53
|
}
|
|
35
54
|
/** Update the bearer token for subsequent reconnections. */
|
|
36
55
|
updateToken(token) {
|
|
@@ -47,60 +66,121 @@ class SseConnection {
|
|
|
47
66
|
/**
|
|
48
67
|
* Open the SSE connection and begin streaming events.
|
|
49
68
|
*
|
|
50
|
-
* This method is **non-blocking**: it starts the connection and returns immediately
|
|
51
|
-
*
|
|
52
|
-
*
|
|
69
|
+
* This method is **non-blocking**: it starts the connection and returns immediately.
|
|
70
|
+
* Events are delivered asynchronously via the `onEvent` callback, and state changes via the
|
|
71
|
+
* `state` event.
|
|
53
72
|
*
|
|
54
73
|
* @param onEvent Callback invoked for each received SSE event, with the event name and raw data string.
|
|
55
|
-
* @param onError Optional callback invoked
|
|
74
|
+
* @param onError Optional callback invoked each time a connection attempt fails or an open stream
|
|
75
|
+
* is lost (not called on intentional disconnect). Reconnecting continues unless
|
|
76
|
+
* {@link state} is `error`.
|
|
56
77
|
*/
|
|
57
78
|
connect(onEvent, onError) {
|
|
58
79
|
this.disconnect();
|
|
59
|
-
this.
|
|
80
|
+
this.attempt = 0;
|
|
81
|
+
this.open(onEvent, onError);
|
|
82
|
+
}
|
|
83
|
+
/** Close the SSE connection, cancel any pending reconnect, and stop receiving events. */
|
|
84
|
+
disconnect() {
|
|
85
|
+
clearTimeout(this.reconnectTimer);
|
|
86
|
+
this.reconnectTimer = undefined;
|
|
87
|
+
this.abortController?.abort();
|
|
88
|
+
this.abortController = undefined;
|
|
89
|
+
this.setState('disconnected');
|
|
90
|
+
}
|
|
91
|
+
open(onEvent, onError) {
|
|
92
|
+
this.setState(this.attempt === 0 ? 'connecting' : 'reconnecting');
|
|
60
93
|
const controller = new AbortController();
|
|
61
94
|
this.abortController = controller;
|
|
62
95
|
(async () => {
|
|
96
|
+
const { idleTimeoutMs } = this.options;
|
|
97
|
+
let idleTimer;
|
|
98
|
+
let idledOut = false;
|
|
99
|
+
const resetIdleTimer = () => {
|
|
100
|
+
if (!idleTimeoutMs)
|
|
101
|
+
return;
|
|
102
|
+
clearTimeout(idleTimer);
|
|
103
|
+
idleTimer = setTimeout(() => {
|
|
104
|
+
idledOut = true;
|
|
105
|
+
controller.abort();
|
|
106
|
+
}, idleTimeoutMs);
|
|
107
|
+
};
|
|
108
|
+
let retry = true;
|
|
109
|
+
let error;
|
|
63
110
|
try {
|
|
111
|
+
resetIdleTimer();
|
|
112
|
+
const token = typeof this.token === 'function' ? this.token() : this.token;
|
|
64
113
|
const response = await globalThis.fetch(this.url, {
|
|
65
114
|
headers: {
|
|
66
|
-
'Authorization': `Bearer ${
|
|
115
|
+
'Authorization': `Bearer ${token}`,
|
|
67
116
|
'Accept': 'text/event-stream',
|
|
68
117
|
'Cache-Control': 'no-cache',
|
|
69
118
|
},
|
|
70
119
|
signal: controller.signal,
|
|
71
120
|
});
|
|
72
|
-
// Guard against a concurrent connect() that replaced our controller.
|
|
73
|
-
if (this.abortController !== controller)
|
|
74
|
-
return;
|
|
75
121
|
if (!response.ok || !response.body) {
|
|
122
|
+
await response.body?.cancel();
|
|
123
|
+
retry = response.ok || SseConnection.isRetryableStatus(response.status);
|
|
76
124
|
throw new Error(`SSE connection failed: HTTP ${response.status}`);
|
|
77
125
|
}
|
|
78
|
-
this.
|
|
79
|
-
|
|
126
|
+
this.attempt = 0;
|
|
127
|
+
this.setState('connected');
|
|
128
|
+
await this.readStream(response.body, onEvent, resetIdleTimer);
|
|
129
|
+
error = new Error('SSE stream closed by the server');
|
|
80
130
|
}
|
|
81
131
|
catch (err) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
132
|
+
error = idledOut
|
|
133
|
+
? new Error(`SSE stream received nothing for ${idleTimeoutMs}ms`)
|
|
134
|
+
: err instanceof Error
|
|
135
|
+
? err
|
|
136
|
+
: new Error(String(err));
|
|
90
137
|
}
|
|
91
|
-
|
|
92
|
-
|
|
138
|
+
finally {
|
|
139
|
+
clearTimeout(idleTimer);
|
|
93
140
|
}
|
|
141
|
+
// disconnect() or a newer connect() replaced this attempt, so it ends silently.
|
|
142
|
+
if (this.abortController !== controller)
|
|
143
|
+
return;
|
|
144
|
+
onError?.(error);
|
|
145
|
+
if (this.abortController !== controller)
|
|
146
|
+
return;
|
|
147
|
+
if (!retry || !this.options.reconnect) {
|
|
148
|
+
this.abortController = undefined;
|
|
149
|
+
this.setState('error');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.setState('reconnecting');
|
|
153
|
+
this.reconnectTimer = setTimeout(() => {
|
|
154
|
+
this.reconnectTimer = undefined;
|
|
155
|
+
this.open(onEvent, onError);
|
|
156
|
+
}, this.nextReconnectDelay());
|
|
94
157
|
})();
|
|
95
158
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Exponential backoff with jitter, so clients that lost the same server do not all retry at
|
|
161
|
+
* the same instant when it comes back.
|
|
162
|
+
*/
|
|
163
|
+
nextReconnectDelay() {
|
|
164
|
+
const { minReconnectDelayMs, maxReconnectDelayMs } = this.options;
|
|
165
|
+
const base = Math.min(maxReconnectDelayMs, minReconnectDelayMs * 2 ** this.attempt);
|
|
166
|
+
this.attempt++;
|
|
167
|
+
return base / 2 + Math.random() * (base / 2);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 401 is retryable because the token is re-read on every attempt and may have been refreshed
|
|
171
|
+
* in the meantime. Other client errors (403, 404, ...) will not change on their own.
|
|
172
|
+
*/
|
|
173
|
+
static isRetryableStatus(status) {
|
|
174
|
+
return status >= 500 || status === 401 || status === 408 || status === 429;
|
|
175
|
+
}
|
|
176
|
+
setState(state) {
|
|
177
|
+
if (this._state === state)
|
|
178
|
+
return;
|
|
179
|
+
this._state = state;
|
|
180
|
+
this.emit('state', state);
|
|
101
181
|
}
|
|
102
182
|
/** Parse the raw `ReadableStream` body into SSE frames, calling `onEvent` for each complete frame. */
|
|
103
|
-
async readStream(body, onEvent) {
|
|
183
|
+
async readStream(body, onEvent, onChunk) {
|
|
104
184
|
const reader = body.getReader();
|
|
105
185
|
const decoder = new TextDecoder();
|
|
106
186
|
let buffer = '';
|
|
@@ -111,6 +191,7 @@ class SseConnection {
|
|
|
111
191
|
const { done, value } = await reader.read();
|
|
112
192
|
if (done)
|
|
113
193
|
break;
|
|
194
|
+
onChunk();
|
|
114
195
|
buffer += decoder.decode(value, { stream: true });
|
|
115
196
|
const lines = buffer.split('\n');
|
|
116
197
|
buffer = lines.pop() ?? '';
|