websocket-ts 2.2.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +201 -63
- package/dist/cjs/{src/backoff → backoff}/backoff.d.ts +6 -3
- package/dist/{esm/src → cjs}/backoff/backoff.d.ts.map +1 -1
- package/dist/{esm/src → cjs}/backoff/backoff.js.map +1 -1
- package/dist/cjs/{src/backoff → backoff}/constantbackoff.d.ts +2 -2
- package/dist/cjs/backoff/constantbackoff.d.ts.map +1 -0
- package/dist/cjs/backoff/constantbackoff.js +34 -0
- package/dist/cjs/backoff/constantbackoff.js.map +1 -0
- package/dist/{esm/src → cjs}/backoff/exponentialbackoff.d.ts +2 -2
- package/dist/cjs/backoff/exponentialbackoff.d.ts.map +1 -0
- package/dist/cjs/{src/backoff → backoff}/exponentialbackoff.js +19 -27
- package/dist/cjs/backoff/exponentialbackoff.js.map +1 -0
- package/dist/{esm/src → cjs}/backoff/linearbackoff.d.ts +1 -1
- package/dist/cjs/backoff/linearbackoff.d.ts.map +1 -0
- package/dist/cjs/{src/backoff → backoff}/linearbackoff.js +23 -31
- package/dist/cjs/backoff/linearbackoff.js.map +1 -0
- package/dist/cjs/index.d.ts +14 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/{esm/src → cjs}/queue/array_queue.d.ts +1 -1
- package/dist/cjs/queue/array_queue.d.ts.map +1 -0
- package/dist/cjs/{src/queue → queue}/array_queue.js +17 -18
- package/dist/cjs/queue/array_queue.js.map +1 -0
- package/dist/cjs/queue/queue.d.ts.map +1 -0
- package/dist/cjs/{src/queue → queue}/queue.js.map +1 -1
- package/dist/cjs/{src/queue → queue}/ring_queue.d.ts +1 -1
- package/dist/cjs/queue/ring_queue.d.ts.map +1 -0
- package/dist/cjs/{src/queue → queue}/ring_queue.js +25 -22
- package/dist/cjs/queue/ring_queue.js.map +1 -0
- package/dist/cjs/{src/websocket.d.ts → websocket.d.ts} +84 -20
- package/dist/cjs/websocket.d.ts.map +1 -0
- package/dist/cjs/websocket.js +588 -0
- package/dist/cjs/websocket.js.map +1 -0
- package/dist/cjs/{src/websocket_buffer.d.ts → websocket_buffer.d.ts} +1 -1
- package/dist/cjs/websocket_buffer.d.ts.map +1 -0
- package/dist/cjs/websocket_buffer.js.map +1 -0
- package/dist/{esm/src → cjs}/websocket_builder.d.ts +22 -13
- package/dist/cjs/websocket_builder.d.ts.map +1 -0
- package/dist/cjs/websocket_builder.js +236 -0
- package/dist/cjs/websocket_builder.js.map +1 -0
- package/dist/cjs/websocket_event.d.ts +93 -0
- package/dist/cjs/websocket_event.d.ts.map +1 -0
- package/dist/cjs/websocket_event.js +27 -0
- package/dist/cjs/websocket_event.js.map +1 -0
- package/dist/{esm/src → cjs}/websocket_options.d.ts +6 -5
- package/dist/cjs/websocket_options.d.ts.map +1 -0
- package/dist/cjs/websocket_options.js.map +1 -0
- package/dist/{esm/src → cjs}/websocket_retry_options.d.ts +4 -3
- package/dist/cjs/websocket_retry_options.d.ts.map +1 -0
- package/dist/cjs/websocket_retry_options.js.map +1 -0
- package/dist/esm/{src/backoff → backoff}/backoff.d.ts +6 -3
- package/dist/{cjs/src → esm}/backoff/backoff.d.ts.map +1 -1
- package/dist/{cjs/src → esm}/backoff/backoff.js.map +1 -1
- package/dist/esm/{src/backoff → backoff}/constantbackoff.d.ts +2 -2
- package/dist/esm/backoff/constantbackoff.d.ts.map +1 -0
- package/dist/esm/{src/backoff → backoff}/constantbackoff.js +3 -3
- package/dist/esm/backoff/constantbackoff.js.map +1 -0
- package/dist/{cjs/src → esm}/backoff/exponentialbackoff.d.ts +2 -2
- package/dist/esm/backoff/exponentialbackoff.d.ts.map +1 -0
- package/dist/esm/{src/backoff → backoff}/exponentialbackoff.js +6 -5
- package/dist/esm/backoff/exponentialbackoff.js.map +1 -0
- package/dist/{cjs/src → esm}/backoff/linearbackoff.d.ts +1 -1
- package/dist/esm/backoff/linearbackoff.d.ts.map +1 -0
- package/dist/esm/{src/backoff → backoff}/linearbackoff.js +8 -7
- package/dist/esm/backoff/linearbackoff.js.map +1 -0
- package/dist/esm/index.d.ts +14 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/{cjs/src → esm}/queue/array_queue.d.ts +1 -1
- package/dist/esm/queue/array_queue.d.ts.map +1 -0
- package/dist/esm/queue/array_queue.js.map +1 -0
- package/dist/esm/queue/queue.d.ts.map +1 -0
- package/dist/esm/{src/queue → queue}/queue.js.map +1 -1
- package/dist/esm/{src/queue → queue}/ring_queue.d.ts +1 -1
- package/dist/esm/queue/ring_queue.d.ts.map +1 -0
- package/dist/esm/{src/queue → queue}/ring_queue.js +7 -3
- package/dist/esm/queue/ring_queue.js.map +1 -0
- package/dist/esm/{src/websocket.d.ts → websocket.d.ts} +84 -20
- package/dist/esm/websocket.d.ts.map +1 -0
- package/dist/esm/websocket.js +584 -0
- package/dist/esm/websocket.js.map +1 -0
- package/dist/esm/{src/websocket_buffer.d.ts → websocket_buffer.d.ts} +1 -1
- package/dist/esm/websocket_buffer.d.ts.map +1 -0
- package/dist/esm/websocket_buffer.js.map +1 -0
- package/dist/{cjs/src → esm}/websocket_builder.d.ts +22 -13
- package/dist/esm/websocket_builder.d.ts.map +1 -0
- package/dist/esm/{src/websocket_builder.js → websocket_builder.js} +42 -23
- package/dist/esm/websocket_builder.js.map +1 -0
- package/dist/esm/websocket_event.d.ts +93 -0
- package/dist/esm/websocket_event.d.ts.map +1 -0
- package/dist/esm/websocket_event.js +24 -0
- package/dist/esm/websocket_event.js.map +1 -0
- package/dist/{cjs/src → esm}/websocket_options.d.ts +6 -5
- package/dist/esm/websocket_options.d.ts.map +1 -0
- package/dist/esm/websocket_options.js.map +1 -0
- package/dist/{cjs/src → esm}/websocket_retry_options.d.ts +4 -3
- package/dist/esm/websocket_retry_options.d.ts.map +1 -0
- package/dist/esm/websocket_retry_options.js.map +1 -0
- package/package.json +44 -25
- package/src/backoff/backoff.ts +6 -3
- package/src/backoff/constantbackoff.ts +4 -4
- package/src/backoff/exponentialbackoff.ts +7 -6
- package/src/backoff/linearbackoff.ts +9 -8
- package/src/index.ts +14 -13
- package/src/queue/array_queue.ts +1 -1
- package/src/queue/ring_queue.ts +12 -8
- package/src/websocket.ts +343 -78
- package/src/websocket_buffer.ts +1 -3
- package/src/websocket_builder.ts +33 -22
- package/src/websocket_event.ts +47 -13
- package/src/websocket_options.ts +6 -5
- package/src/websocket_retry_options.ts +4 -3
- package/dist/cjs/src/backoff/constantbackoff.d.ts.map +0 -1
- package/dist/cjs/src/backoff/constantbackoff.js +0 -43
- package/dist/cjs/src/backoff/constantbackoff.js.map +0 -1
- package/dist/cjs/src/backoff/exponentialbackoff.d.ts.map +0 -1
- package/dist/cjs/src/backoff/exponentialbackoff.js.map +0 -1
- package/dist/cjs/src/backoff/linearbackoff.d.ts.map +0 -1
- package/dist/cjs/src/backoff/linearbackoff.js.map +0 -1
- package/dist/cjs/src/index.d.ts +0 -14
- package/dist/cjs/src/index.d.ts.map +0 -1
- package/dist/cjs/src/index.js +0 -20
- package/dist/cjs/src/index.js.map +0 -1
- package/dist/cjs/src/queue/array_queue.d.ts.map +0 -1
- package/dist/cjs/src/queue/array_queue.js.map +0 -1
- package/dist/cjs/src/queue/queue.d.ts.map +0 -1
- package/dist/cjs/src/queue/ring_queue.d.ts.map +0 -1
- package/dist/cjs/src/queue/ring_queue.js.map +0 -1
- package/dist/cjs/src/websocket.d.ts.map +0 -1
- package/dist/cjs/src/websocket.js +0 -435
- package/dist/cjs/src/websocket.js.map +0 -1
- package/dist/cjs/src/websocket_buffer.d.ts.map +0 -1
- package/dist/cjs/src/websocket_buffer.js.map +0 -1
- package/dist/cjs/src/websocket_builder.d.ts.map +0 -1
- package/dist/cjs/src/websocket_builder.js +0 -263
- package/dist/cjs/src/websocket_builder.js.map +0 -1
- package/dist/cjs/src/websocket_event.d.ts +0 -67
- package/dist/cjs/src/websocket_event.d.ts.map +0 -1
- package/dist/cjs/src/websocket_event.js +0 -22
- package/dist/cjs/src/websocket_event.js.map +0 -1
- package/dist/cjs/src/websocket_options.d.ts.map +0 -1
- package/dist/cjs/src/websocket_options.js.map +0 -1
- package/dist/cjs/src/websocket_retry_options.d.ts.map +0 -1
- package/dist/cjs/src/websocket_retry_options.js.map +0 -1
- package/dist/esm/src/backoff/constantbackoff.d.ts.map +0 -1
- package/dist/esm/src/backoff/constantbackoff.js.map +0 -1
- package/dist/esm/src/backoff/exponentialbackoff.d.ts.map +0 -1
- package/dist/esm/src/backoff/exponentialbackoff.js.map +0 -1
- package/dist/esm/src/backoff/linearbackoff.d.ts.map +0 -1
- package/dist/esm/src/backoff/linearbackoff.js.map +0 -1
- package/dist/esm/src/index.d.ts +0 -14
- package/dist/esm/src/index.d.ts.map +0 -1
- package/dist/esm/src/index.js +0 -9
- package/dist/esm/src/index.js.map +0 -1
- package/dist/esm/src/queue/array_queue.d.ts.map +0 -1
- package/dist/esm/src/queue/array_queue.js.map +0 -1
- package/dist/esm/src/queue/queue.d.ts.map +0 -1
- package/dist/esm/src/queue/ring_queue.d.ts.map +0 -1
- package/dist/esm/src/queue/ring_queue.js.map +0 -1
- package/dist/esm/src/websocket.d.ts.map +0 -1
- package/dist/esm/src/websocket.js +0 -357
- package/dist/esm/src/websocket.js.map +0 -1
- package/dist/esm/src/websocket_buffer.d.ts.map +0 -1
- package/dist/esm/src/websocket_buffer.js.map +0 -1
- package/dist/esm/src/websocket_builder.d.ts.map +0 -1
- package/dist/esm/src/websocket_builder.js.map +0 -1
- package/dist/esm/src/websocket_event.d.ts +0 -67
- package/dist/esm/src/websocket_event.d.ts.map +0 -1
- package/dist/esm/src/websocket_event.js +0 -19
- package/dist/esm/src/websocket_event.js.map +0 -1
- package/dist/esm/src/websocket_options.d.ts.map +0 -1
- package/dist/esm/src/websocket_options.js.map +0 -1
- package/dist/esm/src/websocket_retry_options.d.ts.map +0 -1
- package/dist/esm/src/websocket_retry_options.js.map +0 -1
- package/eslint.config.mjs +0 -43
- package/tsconfig.cjs.json +0 -74
- package/tsconfig.esm.json +0 -74
- package/websocket-ts-2.2.1.tgz +0 -0
- /package/dist/cjs/{src/backoff → backoff}/backoff.js +0 -0
- /package/dist/cjs/{src/queue → queue}/queue.d.ts +0 -0
- /package/dist/cjs/{src/queue → queue}/queue.js +0 -0
- /package/dist/cjs/{src/websocket_buffer.js → websocket_buffer.js} +0 -0
- /package/dist/cjs/{src/websocket_options.js → websocket_options.js} +0 -0
- /package/dist/cjs/{src/websocket_retry_options.js → websocket_retry_options.js} +0 -0
- /package/dist/esm/{src/backoff → backoff}/backoff.js +0 -0
- /package/dist/esm/{src/queue → queue}/array_queue.js +0 -0
- /package/dist/esm/{src/queue → queue}/queue.d.ts +0 -0
- /package/dist/esm/{src/queue → queue}/queue.js +0 -0
- /package/dist/esm/{src/websocket_buffer.js → websocket_buffer.js} +0 -0
- /package/dist/esm/{src/websocket_options.js → websocket_options.js} +0 -0
- /package/dist/esm/{src/websocket_retry_options.js → websocket_retry_options.js} +0 -0
package/src/websocket.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Backoff } from "./backoff/backoff";
|
|
2
|
-
import { WebsocketBuffer } from "./websocket_buffer";
|
|
1
|
+
import { Backoff } from "./backoff/backoff.js";
|
|
2
|
+
import { WebsocketBuffer } from "./websocket_buffer.js";
|
|
3
3
|
import {
|
|
4
|
+
ExhaustedEventDetail,
|
|
4
5
|
ReconnectEventDetail,
|
|
5
6
|
RetryEventDetail,
|
|
6
7
|
WebsocketEvent,
|
|
@@ -9,37 +10,97 @@ import {
|
|
|
9
10
|
WebsocketEventListeners,
|
|
10
11
|
WebsocketEventListenerWithOptions,
|
|
11
12
|
WebsocketEventMap,
|
|
12
|
-
} from "./websocket_event";
|
|
13
|
-
import { WebsocketOptions } from "./websocket_options";
|
|
13
|
+
} from "./websocket_event.js";
|
|
14
|
+
import { WebsocketOptions } from "./websocket_options.js";
|
|
15
|
+
import { WebsocketConnectionRetryOptions } from "./websocket_retry_options.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A URL or a function that returns a URL. When a function is provided, it is called on each connection attempt,
|
|
19
|
+
* enabling use cases like load balancing, auth token rotation, and failover.
|
|
20
|
+
*
|
|
21
|
+
* The function is called without the websocket as `this` and should only resolve a URL. If, during a
|
|
22
|
+
* connection attempt after construction, it synchronously calls close() or reconnect() on the websocket, the
|
|
23
|
+
* attempt that called it is abandoned without creating a socket. It must not call reconnect() on every
|
|
24
|
+
* invocation, as that recurses indefinitely.
|
|
25
|
+
*
|
|
26
|
+
* If it throws during an automatic retry, that attempt fails with an 'error' event (an ErrorEvent whose
|
|
27
|
+
* 'error' is the thrown value) and retrying continues. During construction and reconnect(), the exception
|
|
28
|
+
* propagates to the caller instead.
|
|
29
|
+
*/
|
|
30
|
+
export type UrlProvider = string | (() => string);
|
|
14
31
|
|
|
15
32
|
/**
|
|
16
33
|
* A websocket wrapper that can be configured to reconnect automatically and buffer messages when the websocket is not connected.
|
|
17
34
|
*/
|
|
18
35
|
export class Websocket {
|
|
19
|
-
private readonly
|
|
36
|
+
private readonly _urlProvider: UrlProvider; // the url or url provider
|
|
37
|
+
private _url!: string; // the last resolved url
|
|
20
38
|
private readonly _protocols?: string | string[]; // the protocols to use
|
|
21
39
|
|
|
22
40
|
private _closedByUser: boolean = false; // whether the websocket was closed by the user
|
|
23
41
|
private _lastConnection?: Date; // timestamp of the last connection
|
|
24
|
-
private
|
|
42
|
+
private _binaryType?: BinaryType; // binaryType chosen by the user, undefined if never set
|
|
43
|
+
private _underlyingWebsocket!: WebSocket; // the underlying websocket, e.g. native browser websocket; assigned by tryConnect()
|
|
25
44
|
private retryTimeout?: ReturnType<typeof globalThis.setTimeout>; // timeout for the next retry, if any
|
|
26
45
|
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
// incremented by close() and reconnect() to invalidate in-flight lifecycle
|
|
47
|
+
// work: event handlers and retry timers belonging to an older generation
|
|
48
|
+
// must not commit further state transitions or create sockets
|
|
49
|
+
private _connectionGeneration = 0;
|
|
50
|
+
|
|
51
|
+
// for each listener-registration made with an AbortSignal, the function that
|
|
52
|
+
// unhooks the 'abort'-handler again; used to avoid piling up dead handlers on
|
|
53
|
+
// long-lived signals when listeners are removed by other means
|
|
54
|
+
private readonly abortHandlerCleanups = new WeakMap<object, () => void>();
|
|
55
|
+
|
|
56
|
+
// options/config for the websocket; internally the retry options and the
|
|
57
|
+
// listeners for every event-type are always present, even when the
|
|
58
|
+
// user-supplied options omitted them
|
|
59
|
+
private _options: Omit<WebsocketOptions, "listeners" | "retry"> & {
|
|
60
|
+
readonly retry: WebsocketConnectionRetryOptions;
|
|
61
|
+
readonly listeners: WebsocketEventListeners;
|
|
62
|
+
};
|
|
29
63
|
|
|
30
64
|
/**
|
|
31
65
|
* Creates a new websocket.
|
|
32
66
|
*
|
|
33
|
-
* @param url to connect to.
|
|
67
|
+
* @param url to connect to, or a function that returns a URL.
|
|
34
68
|
* @param protocols optional protocols to use.
|
|
35
69
|
* @param options optional options to use.
|
|
70
|
+
* @throws Error if retry options (maxRetries, instantReconnect) are set without a backoff.
|
|
71
|
+
* @throws Error if maxRetries is neither Infinity nor a non-negative integer.
|
|
36
72
|
*/
|
|
37
73
|
constructor(
|
|
38
|
-
url:
|
|
74
|
+
url: UrlProvider,
|
|
39
75
|
protocols?: string | string[],
|
|
40
76
|
options?: WebsocketOptions,
|
|
41
77
|
) {
|
|
42
|
-
|
|
78
|
+
if (
|
|
79
|
+
options?.retry?.backoff === undefined &&
|
|
80
|
+
(options?.retry?.maxRetries !== undefined ||
|
|
81
|
+
options?.retry?.instantReconnect !== undefined)
|
|
82
|
+
) {
|
|
83
|
+
// retrying is driven by the backoff; without one, the other retry options would silently do nothing
|
|
84
|
+
throw new Error(
|
|
85
|
+
"Retry options (maxRetries, instantReconnect) require a backoff to be configured",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const maxRetries = options?.retry?.maxRetries;
|
|
89
|
+
if (
|
|
90
|
+
maxRetries !== undefined &&
|
|
91
|
+
maxRetries !== Infinity &&
|
|
92
|
+
(!Number.isInteger(maxRetries) || maxRetries < 0)
|
|
93
|
+
) {
|
|
94
|
+
// NaN would silently never exhaust, negative values exhaust before any
|
|
95
|
+
// retry, and fractions break the promise that the exhausted-detail
|
|
96
|
+
// retries equal the configured limit; fail fast like the backoffs do.
|
|
97
|
+
// Infinity is accepted as an explicit 'no limit', like undefined
|
|
98
|
+
throw new Error(
|
|
99
|
+
"MaxRetries must be undefined, Infinity or a non-negative integer",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this._urlProvider = url;
|
|
43
104
|
this._protocols = protocols;
|
|
44
105
|
|
|
45
106
|
// make a copy of the options to prevent the user from changing them
|
|
@@ -51,20 +112,45 @@ export class Websocket {
|
|
|
51
112
|
backoff: options?.retry?.backoff,
|
|
52
113
|
},
|
|
53
114
|
listeners: {
|
|
54
|
-
open: [
|
|
55
|
-
close: [
|
|
56
|
-
error: [
|
|
57
|
-
message: [
|
|
58
|
-
retry: [
|
|
59
|
-
reconnect: [
|
|
115
|
+
open: [],
|
|
116
|
+
close: [],
|
|
117
|
+
error: [],
|
|
118
|
+
message: [],
|
|
119
|
+
retry: [],
|
|
120
|
+
reconnect: [],
|
|
121
|
+
exhausted: [],
|
|
60
122
|
},
|
|
61
123
|
};
|
|
62
124
|
|
|
63
|
-
|
|
125
|
+
try {
|
|
126
|
+
// register the initial listeners through addEventListener so that
|
|
127
|
+
// listener options (e.g. 'signal') are honored for them as well; the
|
|
128
|
+
// generic helper keeps each event's listeners typed for that event
|
|
129
|
+
const register = <K extends WebsocketEvent>(type: K) =>
|
|
130
|
+
options?.listeners?.[type]?.forEach((l) =>
|
|
131
|
+
this.addEventListener(type, l.listener, l.options),
|
|
132
|
+
);
|
|
133
|
+
Object.values(WebsocketEvent).forEach(register);
|
|
134
|
+
|
|
135
|
+
// this first attempt always assigns the underlying websocket: the URL
|
|
136
|
+
// provider is called without the instance as 'this', so it cannot reach
|
|
137
|
+
// close() or reconnect() to supersede it
|
|
138
|
+
this.tryConnect();
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// the caller never receives this instance and cannot remove its
|
|
141
|
+
// listeners, so unhook their abort-handlers; otherwise a long-lived
|
|
142
|
+
// signal would retain the failed instance and everything it references
|
|
143
|
+
Object.values(this._options.listeners).forEach((listeners) =>
|
|
144
|
+
listeners.forEach((l: object) => this.cleanupAbortHandler(l)),
|
|
145
|
+
);
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
64
148
|
}
|
|
65
149
|
|
|
66
150
|
/**
|
|
67
|
-
* Getter for the url.
|
|
151
|
+
* Getter for the url of the most recently constructed underlying websocket. This is not
|
|
152
|
+
* necessarily the URL of the last successful connection, and a URL that the WebSocket
|
|
153
|
+
* constructor rejected, or whose connection attempt was superseded, is not recorded.
|
|
68
154
|
*
|
|
69
155
|
* @return the url.
|
|
70
156
|
*/
|
|
@@ -127,12 +213,14 @@ export class Websocket {
|
|
|
127
213
|
}
|
|
128
214
|
|
|
129
215
|
/**
|
|
130
|
-
* Getter for the last 'open' event, e.
|
|
216
|
+
* Getter for the time of the last 'open' event, i.e. the last time the websocket was connected.
|
|
131
217
|
*
|
|
132
|
-
* @return the last 'open' event, or undefined if the websocket was never connected.
|
|
218
|
+
* @return the time of the last 'open' event, or undefined if the websocket was never connected.
|
|
133
219
|
*/
|
|
134
220
|
get lastConnection(): Date | undefined {
|
|
135
|
-
|
|
221
|
+
// defensive copy: retry/reconnect/exhausted event details are derived
|
|
222
|
+
// from the internal timestamp, so callers must not be able to mutate it
|
|
223
|
+
return this._lastConnection && new Date(this._lastConnection);
|
|
136
224
|
}
|
|
137
225
|
|
|
138
226
|
/**
|
|
@@ -186,11 +274,13 @@ export class Websocket {
|
|
|
186
274
|
}
|
|
187
275
|
|
|
188
276
|
/**
|
|
189
|
-
* Setter for the binaryType of the underlying websocket.
|
|
277
|
+
* Setter for the binaryType of the underlying websocket. The value is remembered
|
|
278
|
+
* and re-applied whenever a new underlying websocket is created after a reconnect.
|
|
190
279
|
*
|
|
191
280
|
* @param value to set, 'blob' or 'arraybuffer'.
|
|
192
281
|
*/
|
|
193
282
|
set binaryType(value: BinaryType) {
|
|
283
|
+
this._binaryType = value;
|
|
194
284
|
this._underlyingWebsocket.binaryType = value;
|
|
195
285
|
}
|
|
196
286
|
|
|
@@ -203,7 +293,7 @@ export class Websocket {
|
|
|
203
293
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
|
|
204
294
|
* @param data to send.
|
|
205
295
|
*/
|
|
206
|
-
public send(data: string |
|
|
296
|
+
public send(data: string | Blob | BufferSource): void {
|
|
207
297
|
if (this.closedByUser) return; // no-op if closed by user
|
|
208
298
|
|
|
209
299
|
if (
|
|
@@ -216,20 +306,47 @@ export class Websocket {
|
|
|
216
306
|
}
|
|
217
307
|
|
|
218
308
|
/**
|
|
219
|
-
* Close the websocket. No connection-retry will be attempted after this
|
|
309
|
+
* Close the websocket. No automatic connection-retry will be attempted after this,
|
|
310
|
+
* until reconnect() is called.
|
|
220
311
|
*
|
|
221
312
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close
|
|
222
313
|
* @param code optional close code.
|
|
223
314
|
* @param reason optional close reason.
|
|
224
315
|
*/
|
|
225
316
|
public close(code?: number, reason?: string): void {
|
|
317
|
+
this._connectionGeneration++; // invalidate in-flight lifecycle work and pending retries
|
|
226
318
|
this.cancelScheduledConnectionRetry(); // cancel any scheduled retries
|
|
227
319
|
this._closedByUser = true; // mark websocket as closed by user
|
|
228
320
|
this._underlyingWebsocket.close(code, reason); // close underlying websocket with provided code and reason
|
|
229
321
|
}
|
|
230
322
|
|
|
231
323
|
/**
|
|
232
|
-
*
|
|
324
|
+
* (Re-)connects the websocket immediately, regardless of its current state:
|
|
325
|
+
* any scheduled retry is cancelled, the backoff and its retry-counter are
|
|
326
|
+
* reset, the URL provider is resolved again and a new connection is opened.
|
|
327
|
+
* This also revives a websocket that was closed by the user or that gave up
|
|
328
|
+
* after exceeding maxRetries (see the 'exhausted' event).
|
|
329
|
+
*
|
|
330
|
+
* Typical uses: a "reconnect" button after the 'exhausted' event, reacting to
|
|
331
|
+
* the browser's 'online' event, or forcing the URL provider to pick up a new
|
|
332
|
+
* auth token without waiting for the connection to drop.
|
|
333
|
+
*
|
|
334
|
+
* @throws whatever the URL provider or the WebSocket constructor throws.
|
|
335
|
+
*/
|
|
336
|
+
public reconnect(): void {
|
|
337
|
+
this._connectionGeneration++; // invalidate in-flight lifecycle work and pending retries
|
|
338
|
+
this.cancelScheduledConnectionRetry(); // cancel any scheduled retries
|
|
339
|
+
this._closedByUser = false; // revive if the websocket was closed by the user
|
|
340
|
+
this.backoff?.reset(); // start the next disconnection episode with a fresh retry budget
|
|
341
|
+
this.clearWebsocket(); // detach and close the current underlying websocket
|
|
342
|
+
this.tryConnect();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Adds an event listener for the given event-type. When an AbortSignal is
|
|
347
|
+
* provided in the options, aborting it removes exactly this registration;
|
|
348
|
+
* a listener whose signal is already aborted is never registered, mirroring
|
|
349
|
+
* the native EventTarget.
|
|
233
350
|
*
|
|
234
351
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
|
|
235
352
|
* @param type of the event to add the listener for.
|
|
@@ -241,24 +358,56 @@ export class Websocket {
|
|
|
241
358
|
listener: WebsocketEventListener<K>,
|
|
242
359
|
options?: WebsocketEventListenerOptions,
|
|
243
360
|
): void {
|
|
244
|
-
|
|
361
|
+
const signal = options?.signal;
|
|
362
|
+
if (signal?.aborted) {
|
|
363
|
+
return; // the signal is already aborted, never register the listener
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const entry: WebsocketEventListenerWithOptions<K> = { listener, options };
|
|
367
|
+
|
|
368
|
+
if (signal !== undefined) {
|
|
369
|
+
// aborting the signal removes exactly this registration; the same
|
|
370
|
+
// listener-function may be registered again with a different signal
|
|
371
|
+
const removeOnAbort = () => {
|
|
372
|
+
this.abortHandlerCleanups.delete(entry);
|
|
373
|
+
(this._options.listeners[
|
|
374
|
+
type
|
|
375
|
+
] as WebsocketEventListenerWithOptions<K>[]) = this._options.listeners[
|
|
376
|
+
type
|
|
377
|
+
].filter((l) => l !== entry);
|
|
378
|
+
};
|
|
379
|
+
signal.addEventListener("abort", removeOnAbort, { once: true });
|
|
380
|
+
this.abortHandlerCleanups.set(entry, () =>
|
|
381
|
+
signal.removeEventListener("abort", removeOnAbort),
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
this._options.listeners[type].push(entry); // add listener to list of listeners
|
|
245
386
|
}
|
|
246
387
|
|
|
247
388
|
/**
|
|
248
|
-
* Removes
|
|
389
|
+
* Removes all event listeners for the given event-type that match the given listener.
|
|
390
|
+
*
|
|
391
|
+
* Matching is done by listener identity only, mirroring the native EventTarget:
|
|
392
|
+
* the options a listener was added with are ignored when matching.
|
|
249
393
|
*
|
|
250
394
|
* @param type of the event to remove the listener for.
|
|
251
395
|
* @param listener to remove.
|
|
252
|
-
* @param options
|
|
396
|
+
* @param options ignored when matching, present for backward compatibility.
|
|
253
397
|
*/
|
|
254
398
|
public removeEventListener<K extends WebsocketEvent>(
|
|
255
399
|
type: K,
|
|
256
400
|
listener: WebsocketEventListener<K>,
|
|
401
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
257
402
|
options?: WebsocketEventListenerOptions,
|
|
258
403
|
): void {
|
|
259
404
|
const isListenerNotToBeRemoved = (
|
|
260
405
|
l: WebsocketEventListenerWithOptions<K>,
|
|
261
|
-
) => l.listener !== listener
|
|
406
|
+
) => l.listener !== listener;
|
|
407
|
+
|
|
408
|
+
this._options.listeners[type]
|
|
409
|
+
.filter((l) => !isListenerNotToBeRemoved(l))
|
|
410
|
+
.forEach((l) => this.cleanupAbortHandler(l)); // unhook abort-handlers of removed listeners
|
|
262
411
|
|
|
263
412
|
(this._options.listeners[type] as WebsocketEventListenerWithOptions<K>[]) =
|
|
264
413
|
this._options.listeners[type].filter(isListenerNotToBeRemoved); // only keep listeners that are not to be removed
|
|
@@ -266,12 +415,26 @@ export class Websocket {
|
|
|
266
415
|
|
|
267
416
|
/**
|
|
268
417
|
* Creates a new browser-native websocket and connects it to the given URL with the given protocols
|
|
269
|
-
* and adds all event listeners to the browser-native websocket.
|
|
270
|
-
*
|
|
271
|
-
*
|
|
418
|
+
* and adds all event listeners to the browser-native websocket. The new websocket is stored in the
|
|
419
|
+
* '_underlyingWebsocket' property, unless the URL provider superseded this attempt by calling close()
|
|
420
|
+
* or reconnect().
|
|
272
421
|
*/
|
|
273
|
-
private tryConnect():
|
|
274
|
-
|
|
422
|
+
private tryConnect(): void {
|
|
423
|
+
const generation = this._connectionGeneration;
|
|
424
|
+
const urlProvider = this._urlProvider; // call the provider without the wrapper as receiver
|
|
425
|
+
const url = typeof urlProvider === "function" ? urlProvider() : urlProvider;
|
|
426
|
+
// the URL provider is user code and may have re-entered close() or
|
|
427
|
+
// reconnect(); that call now owns the lifecycle, so this attempt must
|
|
428
|
+
// neither open a socket after close() nor overwrite (and thereby orphan)
|
|
429
|
+
// the socket a nested reconnect() already committed
|
|
430
|
+
if (generation !== this._connectionGeneration) return;
|
|
431
|
+
|
|
432
|
+
const socket = new WebSocket(url, this.protocols); // create new browser-native websocket and add all event listeners
|
|
433
|
+
this._url = url;
|
|
434
|
+
this._underlyingWebsocket = socket;
|
|
435
|
+
if (this._binaryType !== undefined) {
|
|
436
|
+
this._underlyingWebsocket.binaryType = this._binaryType; // re-apply the user-chosen binaryType
|
|
437
|
+
}
|
|
275
438
|
this._underlyingWebsocket.addEventListener(
|
|
276
439
|
WebsocketEvent.open,
|
|
277
440
|
this.handleOpenEvent,
|
|
@@ -288,8 +451,6 @@ export class Websocket {
|
|
|
288
451
|
WebsocketEvent.message,
|
|
289
452
|
this.handleMessageEvent,
|
|
290
453
|
);
|
|
291
|
-
|
|
292
|
-
return this._underlyingWebsocket;
|
|
293
454
|
}
|
|
294
455
|
|
|
295
456
|
/**
|
|
@@ -353,23 +514,55 @@ export class Websocket {
|
|
|
353
514
|
type: K,
|
|
354
515
|
event: WebsocketEventMap[K],
|
|
355
516
|
) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
517
|
+
// iterate over a snapshot so that listeners registered during dispatch are
|
|
518
|
+
// not invoked in this round, while mutations of the live list persist
|
|
519
|
+
const snapshot: WebsocketEventListenerWithOptions<K>[] = [
|
|
520
|
+
...this._options.listeners[type],
|
|
521
|
+
];
|
|
522
|
+
|
|
523
|
+
snapshot.forEach((listenerWithOptions) => {
|
|
524
|
+
// re-read the live list on every invocation: a listener may have
|
|
525
|
+
// added/removed listeners of this type, or replaced the list entirely
|
|
526
|
+
const listeners: WebsocketEventListenerWithOptions<K>[] =
|
|
527
|
+
this._options.listeners[type];
|
|
528
|
+
|
|
529
|
+
const index = listeners.indexOf(listenerWithOptions);
|
|
530
|
+
if (index === -1) {
|
|
531
|
+
return; // listener was removed during dispatch, don't invoke it
|
|
532
|
+
}
|
|
533
|
+
if (listenerWithOptions.options?.once) {
|
|
534
|
+
listeners.splice(index, 1); // remove once-listener before invoking it
|
|
535
|
+
this.cleanupAbortHandler(listenerWithOptions); // unhook its abort-handler, if any
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
listenerWithOptions.listener(this, event); // invoke listener with event
|
|
540
|
+
} catch (error) {
|
|
541
|
+
// a throwing listener must neither stop the remaining listeners nor the
|
|
542
|
+
// lifecycle work that follows dispatch (retry scheduling, reconnection),
|
|
543
|
+
// mirroring how the native EventTarget isolates listener exceptions
|
|
544
|
+
this.reportListenerError(error);
|
|
369
545
|
}
|
|
370
546
|
});
|
|
547
|
+
}
|
|
371
548
|
|
|
372
|
-
|
|
549
|
+
/**
|
|
550
|
+
* Reports an exception thrown by a user-provided event listener. Uses
|
|
551
|
+
* globalThis.reportError where available (browsers); otherwise the exception
|
|
552
|
+
* is rethrown asynchronously so it still reaches the runtime's global
|
|
553
|
+
* error handling (e.g. 'uncaughtException' in Node.js) without breaking
|
|
554
|
+
* the dispatch it escaped from.
|
|
555
|
+
*
|
|
556
|
+
* @param error the exception thrown by the listener.
|
|
557
|
+
*/
|
|
558
|
+
private reportListenerError(error: unknown) {
|
|
559
|
+
if (typeof globalThis.reportError === "function") {
|
|
560
|
+
globalThis.reportError(error);
|
|
561
|
+
} else {
|
|
562
|
+
globalThis.setTimeout(() => {
|
|
563
|
+
throw error;
|
|
564
|
+
}, 0);
|
|
565
|
+
}
|
|
373
566
|
}
|
|
374
567
|
|
|
375
568
|
/**
|
|
@@ -382,35 +575,63 @@ export class Websocket {
|
|
|
382
575
|
type: K,
|
|
383
576
|
event: WebsocketEventMap[K],
|
|
384
577
|
) {
|
|
578
|
+
// a listener may re-enter close() or reconnect() during dispatch; both bump
|
|
579
|
+
// the generation, in which case the transitions below must not be committed
|
|
580
|
+
const generation = this._connectionGeneration;
|
|
581
|
+
|
|
385
582
|
switch (type) {
|
|
386
583
|
case WebsocketEvent.close:
|
|
387
584
|
this.dispatchEvent(type, event);
|
|
585
|
+
if (generation !== this._connectionGeneration) break; // a listener already closed/reconnected
|
|
388
586
|
this.scheduleConnectionRetryIfNeeded(); // schedule a new connection retry if the websocket was closed by the server
|
|
389
587
|
break;
|
|
390
588
|
|
|
391
|
-
case WebsocketEvent.open:
|
|
392
|
-
if (this.backoff !== undefined && this.
|
|
393
|
-
//
|
|
589
|
+
case WebsocketEvent.open: {
|
|
590
|
+
if (this.backoff !== undefined && this.backoff.retries > 0) {
|
|
591
|
+
// a retry preceded this open, so the websocket was reconnected; this
|
|
592
|
+
// includes recovery from a server that was unavailable at construction,
|
|
593
|
+
// where no previous connection exists yet
|
|
394
594
|
const detail: ReconnectEventDetail = {
|
|
395
595
|
retries: this.backoff.retries,
|
|
396
|
-
lastConnection:
|
|
596
|
+
lastConnection:
|
|
597
|
+
this._lastConnection && new Date(this._lastConnection),
|
|
397
598
|
};
|
|
599
|
+
this.backoff.reset(); // reset before dispatch, so listeners observe a fresh retry budget
|
|
398
600
|
const event: CustomEvent<ReconnectEventDetail> =
|
|
399
601
|
new CustomEvent<ReconnectEventDetail>(WebsocketEvent.reconnect, {
|
|
400
602
|
detail,
|
|
401
603
|
});
|
|
402
604
|
this.dispatchEvent(WebsocketEvent.reconnect, event);
|
|
403
|
-
this.
|
|
605
|
+
if (generation !== this._connectionGeneration) break;
|
|
404
606
|
}
|
|
405
607
|
this._lastConnection = new Date();
|
|
406
608
|
this.dispatchEvent(type, event); // dispatch open event and send buffered data
|
|
609
|
+
if (generation !== this._connectionGeneration) break;
|
|
407
610
|
this.sendBufferedData();
|
|
408
611
|
break;
|
|
612
|
+
}
|
|
409
613
|
|
|
410
614
|
case WebsocketEvent.retry:
|
|
411
615
|
this.dispatchEvent(type, event); // dispatch retry event and try to connect
|
|
616
|
+
if (generation !== this._connectionGeneration) break; // this attempt was superseded during dispatch
|
|
412
617
|
this.clearWebsocket(); // clear the old websocket
|
|
413
|
-
|
|
618
|
+
try {
|
|
619
|
+
this.tryConnect();
|
|
620
|
+
} catch (error) {
|
|
621
|
+
// the url provider or websocket-construction threw; surface the thrown
|
|
622
|
+
// value as an 'error' event and keep the retry chain alive under the
|
|
623
|
+
// usual rules. Native error events are plain Events, so listeners can
|
|
624
|
+
// tell this case apart with 'instanceof ErrorEvent'
|
|
625
|
+
this.dispatchEvent(
|
|
626
|
+
WebsocketEvent.error,
|
|
627
|
+
new ErrorEvent(WebsocketEvent.error, {
|
|
628
|
+
error,
|
|
629
|
+
message: error instanceof Error ? error.message : "",
|
|
630
|
+
}),
|
|
631
|
+
);
|
|
632
|
+
if (generation !== this._connectionGeneration) break;
|
|
633
|
+
this.scheduleConnectionRetryIfNeeded();
|
|
634
|
+
}
|
|
414
635
|
break;
|
|
415
636
|
|
|
416
637
|
default:
|
|
@@ -420,18 +641,24 @@ export class Websocket {
|
|
|
420
641
|
}
|
|
421
642
|
|
|
422
643
|
/**
|
|
423
|
-
* Sends buffered data if there is a buffer defined.
|
|
644
|
+
* Sends buffered data if there is a buffer defined. Draining stops as soon as the
|
|
645
|
+
* websocket is no longer open, e.g. when a listener closed it during the drain;
|
|
646
|
+
* remaining elements stay buffered for the next successful connection. Without
|
|
647
|
+
* this guard, send() would re-add each read element to the buffer, cycling forever.
|
|
424
648
|
*/
|
|
425
649
|
private sendBufferedData() {
|
|
426
650
|
if (this.buffer === undefined) {
|
|
427
651
|
return; // no buffer defined, nothing to send
|
|
428
652
|
}
|
|
429
653
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
ele = this.buffer.read()
|
|
654
|
+
while (
|
|
655
|
+
!this.closedByUser &&
|
|
656
|
+
this._underlyingWebsocket.readyState === this._underlyingWebsocket.OPEN
|
|
434
657
|
) {
|
|
658
|
+
const ele = this.buffer.read();
|
|
659
|
+
if (ele === undefined) {
|
|
660
|
+
return; // buffer is empty
|
|
661
|
+
}
|
|
435
662
|
this.send(ele); // send buffered data
|
|
436
663
|
}
|
|
437
664
|
}
|
|
@@ -456,27 +683,50 @@ export class Websocket {
|
|
|
456
683
|
this.handleEvent(WebsocketEvent.retry, event);
|
|
457
684
|
};
|
|
458
685
|
|
|
459
|
-
//
|
|
686
|
+
// when the maximum number of retries is exceeded, give up and dispatch the
|
|
687
|
+
// exhausted event; checked before advancing the backoff so that the counter
|
|
688
|
+
// equals the number of retries actually performed
|
|
689
|
+
if (
|
|
690
|
+
this._options.retry.maxRetries !== undefined &&
|
|
691
|
+
this.backoff.retries >= this._options.retry.maxRetries
|
|
692
|
+
) {
|
|
693
|
+
const detail: ExhaustedEventDetail = {
|
|
694
|
+
retries: this.backoff.retries,
|
|
695
|
+
lastConnection: this._lastConnection && new Date(this._lastConnection),
|
|
696
|
+
};
|
|
697
|
+
const event: CustomEvent<ExhaustedEventDetail> =
|
|
698
|
+
new CustomEvent<ExhaustedEventDetail>(WebsocketEvent.exhausted, {
|
|
699
|
+
detail,
|
|
700
|
+
});
|
|
701
|
+
this.dispatchEvent(WebsocketEvent.exhausted, event);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// the backoff is reset on every successful reconnect, so a retry-count of
|
|
706
|
+
// zero means this is the first retry of the current disconnection episode
|
|
707
|
+
const isFirstRetryOfEpisode = this.backoff.retries === 0;
|
|
708
|
+
|
|
709
|
+
// advance the backoff so the retry-count is accurate and maxRetries applies;
|
|
710
|
+
// with 'instantReconnect' only the episode's first retry is instant. Browsers
|
|
711
|
+
// store timer delays as signed 32-bit integers, so longer delays would
|
|
712
|
+
// overflow and fire (almost) immediately; cap them at the maximum instead
|
|
713
|
+
const backoff = Math.min(this.backoff.next(), 2 ** 31 - 1);
|
|
460
714
|
const retryEventDetail: RetryEventDetail = {
|
|
461
715
|
backoff:
|
|
462
|
-
this._options.retry.instantReconnect === true
|
|
463
|
-
retries:
|
|
464
|
-
this._options.retry.instantReconnect === true
|
|
716
|
+
this._options.retry.instantReconnect === true && isFirstRetryOfEpisode
|
|
465
717
|
? 0
|
|
466
|
-
:
|
|
467
|
-
|
|
718
|
+
: backoff,
|
|
719
|
+
retries: this.backoff.retries,
|
|
720
|
+
lastConnection: this._lastConnection && new Date(this._lastConnection), // copy so listeners can't mutate our state
|
|
468
721
|
};
|
|
469
722
|
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
retryEventDetail.backoff,
|
|
478
|
-
);
|
|
479
|
-
}
|
|
723
|
+
// no generation check is needed here: close() and reconnect() bump the
|
|
724
|
+
// generation and synchronously cancel this timeout, so a superseded
|
|
725
|
+
// attempt can never fire
|
|
726
|
+
this.retryTimeout = globalThis.setTimeout(() => {
|
|
727
|
+
this.retryTimeout = undefined;
|
|
728
|
+
handleRetryEvent(retryEventDetail);
|
|
729
|
+
}, retryEventDetail.backoff);
|
|
480
730
|
}
|
|
481
731
|
|
|
482
732
|
/**
|
|
@@ -484,5 +734,20 @@ export class Websocket {
|
|
|
484
734
|
*/
|
|
485
735
|
private cancelScheduledConnectionRetry() {
|
|
486
736
|
globalThis.clearTimeout(this.retryTimeout);
|
|
737
|
+
this.retryTimeout = undefined;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Unhooks the 'abort'-handler that was registered on the AbortSignal of the
|
|
742
|
+
* given listener-registration, if there is one.
|
|
743
|
+
*
|
|
744
|
+
* @param entry the listener-registration to unhook the abort-handler for.
|
|
745
|
+
*/
|
|
746
|
+
private cleanupAbortHandler(entry: object) {
|
|
747
|
+
const cleanup = this.abortHandlerCleanups.get(entry);
|
|
748
|
+
if (cleanup !== undefined) {
|
|
749
|
+
cleanup();
|
|
750
|
+
this.abortHandlerCleanups.delete(entry);
|
|
751
|
+
}
|
|
487
752
|
}
|
|
488
753
|
}
|
package/src/websocket_buffer.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A WebsocketBuffer is used to store messages temporarily until they can be sent.
|
|
3
3
|
*/
|
|
4
|
-
export interface WebsocketBuffer<
|
|
5
|
-
E = string | ArrayBufferLike | Blob | ArrayBufferView,
|
|
6
|
-
> {
|
|
4
|
+
export interface WebsocketBuffer<E = string | Blob | BufferSource> {
|
|
7
5
|
/**
|
|
8
6
|
* Adds an element to the buffer.
|
|
9
7
|
* @param element the element to add
|