@evolu/common 8.10.0 → 8.11.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/dist/src/Config.d.ts +22 -22
- package/dist/src/Config.d.ts.map +1 -1
- package/dist/src/Console.d.ts +62 -7
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +20 -4
- package/dist/src/Crypto.d.ts +76 -4
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +55 -4
- package/dist/src/Error.d.ts +45 -0
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +69 -0
- package/dist/src/Fs.d.ts +92 -18
- package/dist/src/Fs.d.ts.map +1 -1
- package/dist/src/Fs.js +2 -0
- package/dist/src/Identicon.d.ts +2 -2
- package/dist/src/Identicon.js +2 -2
- package/dist/src/LeakDetector.d.ts +22 -3
- package/dist/src/LeakDetector.d.ts.map +1 -1
- package/dist/src/LeakDetector.js +12 -2
- package/dist/src/LockManager.d.ts +8 -0
- package/dist/src/LockManager.d.ts.map +1 -1
- package/dist/src/LockManager.js +6 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +5 -0
- package/dist/src/Platform.d.ts +47 -7
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +24 -5
- package/dist/src/Random.d.ts +25 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +14 -2
- package/dist/src/Resource.d.ts +156 -1
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +201 -72
- package/dist/src/Schedule.d.ts +11 -10
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +1 -1
- package/dist/src/Sqlite.d.ts +132 -16
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +63 -9
- package/dist/src/Task.d.ts +15 -4
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +41 -15
- package/dist/src/Test.d.ts +9 -0
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +4 -0
- package/dist/src/Time.d.ts +106 -9
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +55 -4
- package/dist/src/Type.d.ts +1455 -1310
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1274 -517
- package/dist/src/WebSocket.d.ts +164 -13
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +133 -24
- package/dist/src/Worker.d.ts +90 -8
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +28 -2
- package/dist/src/index.d.ts +6 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/dist/src/local-first/Db.d.ts +52 -3
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +412 -137
- package/dist/src/local-first/Evolu.d.ts +336 -211
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +102 -15
- package/dist/src/local-first/Owner.d.ts +13 -30
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +13 -30
- package/dist/src/local-first/Protocol.d.ts +94 -16
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +118 -38
- package/dist/src/local-first/Query.d.ts +8 -15
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Schema.d.ts +335 -21
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +214 -17
- package/dist/src/local-first/Shared.d.ts +537 -22
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +1437 -234
- package/dist/src/local-first/Storage.d.ts +192 -14
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +81 -20
- package/dist/src/local-first/Timestamp.d.ts +392 -41
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +403 -81
- package/dist/src/local-first/index.d.ts +0 -1
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +0 -1
- package/package.json +1 -1
- package/src/Assert.test.ts +2 -5
- package/src/Config.test.ts +2 -6
- package/src/Config.ts +133 -133
- package/src/Console.ts +62 -7
- package/src/Crypto.ts +76 -4
- package/src/Eq.test.ts +2 -3
- package/src/Error.test.ts +76 -3
- package/src/Error.ts +71 -0
- package/src/Fs.ts +92 -18
- package/src/Identicon.ts +2 -2
- package/src/LeakDetector.ts +22 -3
- package/src/LockManager.ts +8 -0
- package/src/Object.test.ts +27 -12
- package/src/Object.ts +5 -0
- package/src/Platform.ts +50 -8
- package/src/Random.ts +25 -2
- package/src/Resource.test.ts +837 -0
- package/src/Resource.ts +235 -15
- package/src/Schedule.test.ts +50 -12
- package/src/Schedule.ts +24 -14
- package/src/Sqlite.ts +137 -17
- package/src/Task.test.ts +189 -8
- package/src/Task.ts +56 -17
- package/src/Test.ts +9 -0
- package/src/Time.ts +106 -9
- package/src/Type.test.ts +946 -1028
- package/src/Type.ts +4195 -3136
- package/src/Types.test.ts +4 -14
- package/src/WebSocket.ts +313 -40
- package/src/Worker.ts +90 -8
- package/src/index.ts +15 -6
- package/src/local-first/Db.ts +644 -339
- package/src/local-first/Evolu.test.ts +686 -21
- package/src/local-first/Evolu.ts +450 -228
- package/src/local-first/Owner.ts +13 -30
- package/src/local-first/Protocol.test.ts +617 -10
- package/src/local-first/Protocol.ts +196 -72
- package/src/local-first/Query.ts +8 -15
- package/src/local-first/Schema.test.ts +143 -0
- package/src/local-first/Schema.ts +363 -24
- package/src/local-first/Shared.test.ts +7731 -559
- package/src/local-first/Shared.ts +2036 -267
- package/src/local-first/Storage.ts +218 -32
- package/src/local-first/Timestamp.test.ts +344 -70
- package/src/local-first/Timestamp.ts +434 -118
- package/src/local-first/index.ts +0 -1
- package/dist/src/local-first/Error.d.ts +0 -12
- package/dist/src/local-first/Error.d.ts.map +0 -1
- package/dist/src/local-first/Error.js +0 -6
- package/dist/src/local-first/LocalAuth.d.ts +0 -150
- package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
- package/dist/src/local-first/LocalAuth.js +0 -179
- package/src/local-first/Error.ts +0 -17
- package/src/local-first/LocalAuth.ts +0 -457
package/dist/src/WebSocket.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { Result } from "./Result.ts";
|
|
7
7
|
import type { Schedule } from "./Schedule.ts";
|
|
8
8
|
import type { RetryError, Task } from "./Task.ts";
|
|
9
|
-
import type { Millis } from "./Time.ts";
|
|
9
|
+
import type { Duration, Millis } from "./Time.ts";
|
|
10
10
|
import { type Typed } from "./Type.ts";
|
|
11
11
|
/**
|
|
12
12
|
* WebSocket with auto-reconnect.
|
|
@@ -84,6 +84,8 @@ import { type Typed } from "./Type.ts";
|
|
|
84
84
|
* { url: "wss://example.com", data: "Hello" },
|
|
85
85
|
* );
|
|
86
86
|
* ```
|
|
87
|
+
*
|
|
88
|
+
* @group Core
|
|
87
89
|
*/
|
|
88
90
|
export interface WebSocket extends AsyncDisposable {
|
|
89
91
|
/**
|
|
@@ -94,23 +96,82 @@ export interface WebSocket extends AsyncDisposable {
|
|
|
94
96
|
readonly getReadyState: () => WebSocketReadyState;
|
|
95
97
|
/** Returns true if the WebSocket is open and ready to send data. */
|
|
96
98
|
readonly isOpen: () => boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Abandons the current connection and connects again.
|
|
101
|
+
*
|
|
102
|
+
* Use it when the connection is dead although it never closed, for example
|
|
103
|
+
* when a request stays unanswered. The wrapper cannot detect that by itself:
|
|
104
|
+
* no close or error event arrives, so its own reconnect never runs.
|
|
105
|
+
*
|
|
106
|
+
* The connection is dropped without waiting for a close handshake, and
|
|
107
|
+
* neither {@link WebSocketOptions.onClose} nor
|
|
108
|
+
* {@link WebSocketOptions.shouldRetryOnClose} is consulted, because the close
|
|
109
|
+
* is this call rather than something to learn about or veto. A connection
|
|
110
|
+
* that was open restarts the {@link WebSocketOptions.schedule}, as a close
|
|
111
|
+
* after {@link WebSocketOptions.healthyConnectionDuration} does; one that was
|
|
112
|
+
* still connecting keeps its backoff, having proved nothing.
|
|
113
|
+
*
|
|
114
|
+
* A connection already closing or closed is left to settle on its own, and
|
|
115
|
+
* after disposal this does nothing, so a timer or handler that outlives the
|
|
116
|
+
* connection is safe to call it from.
|
|
117
|
+
*/
|
|
118
|
+
readonly reconnect: () => void;
|
|
97
119
|
}
|
|
98
120
|
/**
|
|
99
121
|
* An error that occurs when trying to send data but WebSocket is not available
|
|
100
122
|
* or is in the CONNECTING state.
|
|
101
123
|
*
|
|
102
124
|
* https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
|
|
125
|
+
*
|
|
126
|
+
* @group Errors
|
|
103
127
|
*/
|
|
104
128
|
export interface WebSocketSendError extends Typed<"WebSocketSendError"> {
|
|
105
129
|
}
|
|
106
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* WebSocket connection states.
|
|
132
|
+
*
|
|
133
|
+
* @group Core
|
|
134
|
+
*/
|
|
107
135
|
export type WebSocketReadyState = "connecting" | "open" | "closing" | "closed";
|
|
108
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* What a {@link WebSocket} reports when a connection closes.
|
|
138
|
+
*
|
|
139
|
+
* These are the fields every platform provides. Evolu does not use the DOM
|
|
140
|
+
* `CloseEvent` type: React Native delivers its own close event and exposes no
|
|
141
|
+
* `CloseEvent` global, so promising the DOM type would promise an inheritance
|
|
142
|
+
* chain and an `instanceof` that do not hold there. A platform's own close
|
|
143
|
+
* event is structurally assignable to this, and {@link createWebSocket} passes
|
|
144
|
+
* it through unchanged, so a caller that knows its platform can narrow it.
|
|
145
|
+
*
|
|
146
|
+
* @group Core
|
|
147
|
+
*/
|
|
148
|
+
export interface WebSocketCloseEvent {
|
|
149
|
+
/** https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code */
|
|
150
|
+
readonly code: number;
|
|
151
|
+
/** https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/reason */
|
|
152
|
+
readonly reason: string;
|
|
153
|
+
/** Whether the connection closed after a completed closing handshake. */
|
|
154
|
+
readonly wasClean: boolean;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* {@link Task} that creates a {@link WebSocket}.
|
|
158
|
+
*
|
|
159
|
+
* @group Core
|
|
160
|
+
*/
|
|
109
161
|
export type CreateWebSocket = (url: string, options?: WebSocketOptions) => Task<WebSocket>;
|
|
162
|
+
/**
|
|
163
|
+
* Dependency wrapper for {@link CreateWebSocket}.
|
|
164
|
+
*
|
|
165
|
+
* @group Core
|
|
166
|
+
*/
|
|
110
167
|
export interface CreateWebSocketDep {
|
|
111
168
|
readonly createWebSocket: CreateWebSocket;
|
|
112
169
|
}
|
|
113
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Options for creating {@link WebSocket}.
|
|
172
|
+
*
|
|
173
|
+
* @group Core
|
|
174
|
+
*/
|
|
114
175
|
export interface WebSocketOptions {
|
|
115
176
|
/** Protocol(s) to use with the WebSocket connection. */
|
|
116
177
|
readonly protocols?: string | ReadonlyArray<string>;
|
|
@@ -121,13 +182,13 @@ export interface WebSocketOptions {
|
|
|
121
182
|
/** Callback when an error occurs. */
|
|
122
183
|
readonly onError?: (error: WebSocketError) => void;
|
|
123
184
|
/** Callback when the connection is closed. */
|
|
124
|
-
readonly onClose?: (event:
|
|
185
|
+
readonly onClose?: (event: WebSocketCloseEvent) => void;
|
|
125
186
|
/**
|
|
126
187
|
* Determines whether a closed connection should trigger a retry.
|
|
127
188
|
*
|
|
128
189
|
* Return false to stop retrying, for example on auth errors or maintenance.
|
|
129
190
|
*/
|
|
130
|
-
readonly shouldRetryOnClose?: (event:
|
|
191
|
+
readonly shouldRetryOnClose?: (event: WebSocketCloseEvent) => boolean;
|
|
131
192
|
/** Callback when message data is received. */
|
|
132
193
|
readonly onMessage?: (data: string | ArrayBuffer | Blob) => void;
|
|
133
194
|
/**
|
|
@@ -135,6 +196,27 @@ export interface WebSocketOptions {
|
|
|
135
196
|
* {@link webSocketReconnectSchedule}.
|
|
136
197
|
*/
|
|
137
198
|
readonly schedule?: Schedule<Millis, WebSocketRetryError>;
|
|
199
|
+
/**
|
|
200
|
+
* How long a connection must stay open for its close to start
|
|
201
|
+
* {@link WebSocketOptions.schedule} over. Defaults to 30 seconds, the delay
|
|
202
|
+
* cap of {@link webSocketReconnectSchedule}.
|
|
203
|
+
*
|
|
204
|
+
* The schedule is stateful and spans every reconnect, so without this a
|
|
205
|
+
* client that has disconnected often keeps waiting the longest backoff delay
|
|
206
|
+
* forever, even after hours of healthy connection. A connection that outlasts
|
|
207
|
+
* the longest delay the schedule can produce shows the endpoint works, so the
|
|
208
|
+
* next disconnect starts from the base delay again. Shorter connections reset
|
|
209
|
+
* nothing, so an endpoint that accepts and immediately drops connections
|
|
210
|
+
* still backs off.
|
|
211
|
+
*
|
|
212
|
+
* Choose it with the schedule rather than on its own: it is the schedule's
|
|
213
|
+
* delay cap, which only the schedule knows. A schedule cannot be asked for
|
|
214
|
+
* that cap, which is why this is an option rather than something derived. The
|
|
215
|
+
* delays a schedule has already produced are no substitute, because a
|
|
216
|
+
* jittered one produces delays near zero early on, and a threshold that low
|
|
217
|
+
* would reset the backoff for exactly the endpoint it protects against.
|
|
218
|
+
*/
|
|
219
|
+
readonly healthyConnectionDuration?: Duration;
|
|
138
220
|
/**
|
|
139
221
|
* For custom WebSocket implementations.
|
|
140
222
|
*
|
|
@@ -144,10 +226,18 @@ export interface WebSocketOptions {
|
|
|
144
226
|
*/
|
|
145
227
|
readonly WebSocketConstructor?: typeof globalThis.WebSocket;
|
|
146
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Any error reported by a {@link WebSocket}, including exhausted reconnect
|
|
231
|
+
* retries.
|
|
232
|
+
*
|
|
233
|
+
* @group Errors
|
|
234
|
+
*/
|
|
147
235
|
export type WebSocketError = WebSocketConnectError | WebSocketConnectionError | RetryError<WebSocketRetryError>;
|
|
148
236
|
/**
|
|
149
237
|
* An error that occurs when a connection cannot be established due to a network
|
|
150
238
|
* error. Fires before `onclose`.
|
|
239
|
+
*
|
|
240
|
+
* @group Errors
|
|
151
241
|
*/
|
|
152
242
|
export interface WebSocketConnectError extends Typed<"WebSocketConnectError"> {
|
|
153
243
|
readonly event: Event;
|
|
@@ -160,53 +250,114 @@ export interface WebSocketConnectError extends Typed<"WebSocketConnectError"> {
|
|
|
160
250
|
* Chromium and Firefox only fire `onclose` without a preceding error event.
|
|
161
251
|
*
|
|
162
252
|
* https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event
|
|
253
|
+
*
|
|
254
|
+
* @group Errors
|
|
163
255
|
*/
|
|
164
256
|
export interface WebSocketConnectionError extends Typed<"WebSocketConnectionError"> {
|
|
165
257
|
readonly event: Event;
|
|
166
258
|
}
|
|
167
|
-
|
|
168
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Errors that trigger a reconnect attempt under
|
|
261
|
+
* {@link webSocketReconnectSchedule}.
|
|
262
|
+
*
|
|
263
|
+
* @group Errors
|
|
264
|
+
*/
|
|
265
|
+
export type WebSocketRetryError = WebSocketConnectError | WebSocketConnectionCloseError | WebSocketReconnectError;
|
|
266
|
+
/**
|
|
267
|
+
* The error {@link WebSocket.reconnect} settles the current connection with.
|
|
268
|
+
*
|
|
269
|
+
* Reconnecting is not a close, so it carries no close event: nothing observed
|
|
270
|
+
* one to report.
|
|
271
|
+
*
|
|
272
|
+
* @group Errors
|
|
273
|
+
*/
|
|
274
|
+
export interface WebSocketReconnectError extends Typed<"WebSocketReconnectError"> {
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* An error that occurs when the connection is closed by the server.
|
|
278
|
+
*
|
|
279
|
+
* @group Errors
|
|
280
|
+
*/
|
|
169
281
|
export interface WebSocketConnectionCloseError extends Typed<"WebSocketConnectionCloseError"> {
|
|
170
|
-
readonly event:
|
|
282
|
+
readonly event: WebSocketCloseEvent;
|
|
171
283
|
}
|
|
172
284
|
/**
|
|
173
285
|
* Default WebSocket reconnect schedule.
|
|
174
286
|
*
|
|
175
287
|
* Uses unlimited exponential backoff with a 100ms base, 30s cap, and full
|
|
176
288
|
* jitter.
|
|
289
|
+
*
|
|
290
|
+
* @group Core
|
|
177
291
|
*/
|
|
178
292
|
export declare const webSocketReconnectSchedule: Schedule<Millis, WebSocketRetryError>;
|
|
179
|
-
/**
|
|
293
|
+
/**
|
|
294
|
+
* Create a new {@link WebSocket}.
|
|
295
|
+
*
|
|
296
|
+
* @group Core
|
|
297
|
+
*/
|
|
180
298
|
export declare const createWebSocket: CreateWebSocket;
|
|
181
299
|
/**
|
|
182
300
|
* An inspectable in-memory {@link CreateWebSocket} for testing by
|
|
183
301
|
* {@link testCreateWebSocket}.
|
|
302
|
+
*
|
|
303
|
+
* Sockets report the states {@link createWebSocket} reports, including the
|
|
304
|
+
* `connecting` a socket is in before it opens and while the wrapper retries
|
|
305
|
+
* after a close or a {@link WebSocket.reconnect}. While it retries,
|
|
306
|
+
* {@link WebSocket.reconnect} does nothing, as the wrapper has no socket until
|
|
307
|
+
* {@link TestCreateWebSocket.open}. Only disposal ends in `closed`. The
|
|
308
|
+
* `closing` state is not modeled: no helper starts a close handshake. The event
|
|
309
|
+
* helpers throw for a disposed socket, which cannot receive events:
|
|
310
|
+
* {@link createWebSocket} detaches its handlers on disposal.
|
|
311
|
+
*
|
|
312
|
+
* @group Testing
|
|
184
313
|
*/
|
|
185
314
|
export interface TestCreateWebSocket extends CreateWebSocket {
|
|
186
315
|
readonly createdUrls: Array<string>;
|
|
316
|
+
/** URLs whose newest socket was reconnected, in call order. */
|
|
317
|
+
readonly reconnectedUrls: Array<string>;
|
|
187
318
|
readonly sentMessages: Array<{
|
|
188
319
|
readonly url: string;
|
|
189
320
|
readonly data: BufferSource | Blob | string | globalThis.Uint8Array;
|
|
190
321
|
}>;
|
|
191
322
|
readonly message: (url: string, data: string | ArrayBuffer | Blob) => void;
|
|
192
323
|
readonly open: (url: string) => void;
|
|
324
|
+
/**
|
|
325
|
+
* Reports the close event, with `code` defaulting to 1006, and leaves the
|
|
326
|
+
* socket connecting.
|
|
327
|
+
*/
|
|
328
|
+
readonly close: (url: string, event?: Partial<WebSocketCloseEvent>) => void;
|
|
329
|
+
readonly error: (url: string, error: WebSocketError) => void;
|
|
193
330
|
}
|
|
194
|
-
/**
|
|
331
|
+
/**
|
|
332
|
+
* Creates {@link TestCreateWebSocket}.
|
|
333
|
+
*
|
|
334
|
+
* @group Testing
|
|
335
|
+
*/
|
|
195
336
|
export declare const testCreateWebSocket: (options?: {
|
|
196
337
|
/** Throw immediately when a socket is created. */
|
|
197
338
|
readonly throwOnCreate?: boolean;
|
|
198
|
-
/**
|
|
339
|
+
/**
|
|
340
|
+
* Whether created sockets start open, skipping
|
|
341
|
+
* {@link TestCreateWebSocket.open}. Defaults to true. A socket that does not
|
|
342
|
+
* start open is connecting.
|
|
343
|
+
*/
|
|
199
344
|
readonly isOpen?: boolean;
|
|
200
345
|
}) => TestCreateWebSocket;
|
|
201
346
|
/**
|
|
202
347
|
* A native {@link WebSocket} prepared for integration tests by
|
|
203
348
|
* {@link testSetupWebSocket}.
|
|
349
|
+
*
|
|
350
|
+
* @group Testing
|
|
204
351
|
*/
|
|
205
352
|
export interface TestSetupWebSocket extends AsyncDisposable {
|
|
206
353
|
readonly socket: globalThis.WebSocket;
|
|
207
354
|
readonly send: (data: BufferSource | Blob | string | globalThis.Uint8Array) => void;
|
|
208
355
|
readonly waitForMessage: () => Promise<string | globalThis.Uint8Array>;
|
|
209
356
|
}
|
|
210
|
-
/**
|
|
357
|
+
/**
|
|
358
|
+
* Opens a native {@link WebSocket} and returns {@link TestSetupWebSocket}.
|
|
359
|
+
*
|
|
360
|
+
* @group Testing
|
|
361
|
+
*/
|
|
211
362
|
export declare const testSetupWebSocket: (url: string) => Promise<TestSetupWebSocket>;
|
|
212
363
|
//# sourceMappingURL=WebSocket.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebSocket.d.ts","sourceRoot":"","sources":["../../src/WebSocket.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAElD,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"WebSocket.d.ts","sourceRoot":"","sources":["../../src/WebSocket.ts"],"names":[],"mappings":"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"}
|
package/dist/src/WebSocket.js
CHANGED
|
@@ -60,22 +60,36 @@ import { constTrue } from "./Function.js";
|
|
|
60
60
|
import { err, ok } from "./Result.js";
|
|
61
61
|
import { exponential, jitter, maxDelay } from "./Schedule.js";
|
|
62
62
|
import { callback, retry } from "./Task.js";
|
|
63
|
+
import { durationToMillis, performanceDurationBetween } from "./Time.js";
|
|
63
64
|
import { ArrayBuffer, String, Uint8Array } from "./Type.js";
|
|
64
65
|
/**
|
|
65
66
|
* Default WebSocket reconnect schedule.
|
|
66
67
|
*
|
|
67
68
|
* Uses unlimited exponential backoff with a 100ms base, 30s cap, and full
|
|
68
69
|
* jitter.
|
|
70
|
+
*
|
|
71
|
+
* @group Core
|
|
69
72
|
*/
|
|
70
73
|
export const webSocketReconnectSchedule =
|
|
71
74
|
/*#__PURE__*/ jitter("100%")(
|
|
72
75
|
/*#__PURE__*/ maxDelay("30s")(/*#__PURE__*/ exponential("100ms")));
|
|
73
|
-
/**
|
|
74
|
-
|
|
76
|
+
/** The delay cap of {@link webSocketReconnectSchedule}. */
|
|
77
|
+
const defaultHealthyConnectionDuration = /*#__PURE__*/ durationToMillis("30s");
|
|
78
|
+
/**
|
|
79
|
+
* Create a new {@link WebSocket}.
|
|
80
|
+
*
|
|
81
|
+
* @group Core
|
|
82
|
+
*/
|
|
83
|
+
export const createWebSocket = (url, { protocols, binaryType, onOpen, onClose, shouldRetryOnClose = constTrue, onMessage, onError, schedule = webSocketReconnectSchedule, healthyConnectionDuration = defaultHealthyConnectionDuration, WebSocketConstructor = globalThis.WebSocket, } = {}) => async (run) => {
|
|
75
84
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
76
85
|
try {
|
|
77
86
|
const disposer = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
|
|
87
|
+
const healthyConnectionMillis = durationToMillis(healthyConnectionDuration);
|
|
78
88
|
let socket = null;
|
|
89
|
+
// Set by a close that proves the endpoint works and by `reconnect`, and
|
|
90
|
+
// consumed by the next schedule step.
|
|
91
|
+
let shouldResetSchedule = false;
|
|
92
|
+
let resolveConnect = null;
|
|
79
93
|
const closeSocket = () => {
|
|
80
94
|
if (!socket)
|
|
81
95
|
return;
|
|
@@ -99,17 +113,27 @@ export const createWebSocket = (url, { protocols, binaryType, onOpen, onClose, s
|
|
|
99
113
|
*/
|
|
100
114
|
const connect = callback(({ resolve }) => {
|
|
101
115
|
closeSocket();
|
|
116
|
+
resolveConnect = resolve;
|
|
102
117
|
socket = new WebSocketConstructor(url, String.is(protocols) ? protocols : protocols && [...protocols]);
|
|
103
118
|
if (binaryType)
|
|
104
119
|
socket.binaryType = binaryType;
|
|
105
120
|
let isOpen = false;
|
|
121
|
+
// Monotonic: how long the connection lasted must not follow a system
|
|
122
|
+
// clock adjustment, which would reset the schedule for a connection that
|
|
123
|
+
// proved nothing, or withhold the reset from one that proved the
|
|
124
|
+
// endpoint works.
|
|
125
|
+
let openedAt = null;
|
|
106
126
|
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
|
|
107
127
|
socket.onopen = () => {
|
|
108
128
|
isOpen = true;
|
|
129
|
+
openedAt = run.deps.time.performance.now();
|
|
109
130
|
onOpen?.();
|
|
110
131
|
};
|
|
111
132
|
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
|
|
112
133
|
socket.onclose = (event) => {
|
|
134
|
+
if (openedAt !== null &&
|
|
135
|
+
performanceDurationBetween(openedAt, run.deps.time.performance.now()) >= healthyConnectionMillis)
|
|
136
|
+
shouldResetSchedule = true;
|
|
113
137
|
onClose?.(event);
|
|
114
138
|
if (shouldRetryOnClose(event)) {
|
|
115
139
|
resolve(err({ type: "WebSocketConnectionCloseError", event }));
|
|
@@ -131,9 +155,29 @@ export const createWebSocket = (url, { protocols, binaryType, onOpen, onClose, s
|
|
|
131
155
|
if (error.type === "WebSocketConnectError")
|
|
132
156
|
resolve(err(error));
|
|
133
157
|
};
|
|
134
|
-
return
|
|
158
|
+
return () => {
|
|
159
|
+
resolveConnect = null;
|
|
160
|
+
closeSocket();
|
|
161
|
+
};
|
|
135
162
|
});
|
|
136
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Wraps `schedule` so a healthy connection starts its backoff over.
|
|
165
|
+
*
|
|
166
|
+
* `retry` builds one schedule step per call and `connect` settles only when
|
|
167
|
+
* a connection closes, so a single step would otherwise accumulate backoff
|
|
168
|
+
* across every close for this wrapper's lifetime.
|
|
169
|
+
*/
|
|
170
|
+
const reconnectSchedule = (deps) => {
|
|
171
|
+
let step = schedule(deps);
|
|
172
|
+
return (error) => {
|
|
173
|
+
if (shouldResetSchedule) {
|
|
174
|
+
shouldResetSchedule = false;
|
|
175
|
+
step = schedule(deps);
|
|
176
|
+
}
|
|
177
|
+
return step(error);
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
const retryFiber = disposer.use(run.daemon(retry(connect, reconnectSchedule)));
|
|
137
181
|
// Report RetryError (schedule exhausted) via onError callback
|
|
138
182
|
void retryFiber.then((result) => {
|
|
139
183
|
if (!result.ok && result.error.type === "RetryError") {
|
|
@@ -157,6 +201,26 @@ export const createWebSocket = (url, { protocols, binaryType, onOpen, onClose, s
|
|
|
157
201
|
},
|
|
158
202
|
isOpen: () => !disposables.disposed &&
|
|
159
203
|
socket?.readyState === globalThis.WebSocket.OPEN,
|
|
204
|
+
reconnect: () => {
|
|
205
|
+
const resolve = resolveConnect;
|
|
206
|
+
if (disposables.disposed || !resolve || !socket)
|
|
207
|
+
return;
|
|
208
|
+
// A closing or closed socket settles the connection on its own.
|
|
209
|
+
// Reconnecting it would discard `shouldRetryOnClose` and restart the
|
|
210
|
+
// schedule for a close that proved nothing. `onClose` and `onError`
|
|
211
|
+
// run before that settlement, so a handler can reach this.
|
|
212
|
+
if (socket.readyState !== socket.CONNECTING &&
|
|
213
|
+
socket.readyState !== socket.OPEN)
|
|
214
|
+
return;
|
|
215
|
+
// Only a connection that was open makes the backoff before it stale.
|
|
216
|
+
if (socket.readyState === socket.OPEN)
|
|
217
|
+
shouldResetSchedule = true;
|
|
218
|
+
resolveConnect = null;
|
|
219
|
+
// Handlers are cleared before settling, so the abandoned connection
|
|
220
|
+
// reports nothing while `retry` waits out its delay.
|
|
221
|
+
closeSocket();
|
|
222
|
+
resolve(err({ type: "WebSocketReconnectError" }));
|
|
223
|
+
},
|
|
160
224
|
[Symbol.asyncDispose]: () => disposables.disposeAsync(),
|
|
161
225
|
});
|
|
162
226
|
}
|
|
@@ -184,9 +248,14 @@ const nativeToStringState = {
|
|
|
184
248
|
[globalThis.WebSocket.CLOSING]: "closing",
|
|
185
249
|
[globalThis.WebSocket.CLOSED]: "closed",
|
|
186
250
|
};
|
|
187
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* Creates {@link TestCreateWebSocket}.
|
|
253
|
+
*
|
|
254
|
+
* @group Testing
|
|
255
|
+
*/
|
|
188
256
|
export const testCreateWebSocket = (options = {}) => {
|
|
189
257
|
const createdUrls = [];
|
|
258
|
+
const reconnectedUrls = [];
|
|
190
259
|
const sentMessages = [];
|
|
191
260
|
const stateByUrl = new Map();
|
|
192
261
|
const getState = (url) => {
|
|
@@ -199,15 +268,18 @@ export const testCreateWebSocket = (options = {}) => {
|
|
|
199
268
|
throw new Error("testCreateWebSocket is configured to throw on create");
|
|
200
269
|
}
|
|
201
270
|
createdUrls.push(url);
|
|
202
|
-
|
|
271
|
+
// A URL can be created again after its socket was disposed. Each socket
|
|
272
|
+
// keeps its own state; the helpers address the newest socket for a URL.
|
|
273
|
+
const state = {
|
|
203
274
|
options: socketOptions,
|
|
204
|
-
|
|
275
|
+
readyState: (options.isOpen ?? true) ? "open" : "connecting",
|
|
205
276
|
isDisposed: false,
|
|
206
|
-
|
|
277
|
+
isWaitingToRetry: false,
|
|
278
|
+
};
|
|
279
|
+
stateByUrl.set(url, state);
|
|
207
280
|
return ok({
|
|
208
281
|
send: (data) => {
|
|
209
|
-
|
|
210
|
-
if (state.isDisposed || !state.isOpen) {
|
|
282
|
+
if (state.isDisposed || state.readyState !== "open") {
|
|
211
283
|
return err({ type: "WebSocketSendError" });
|
|
212
284
|
}
|
|
213
285
|
sentMessages.push({
|
|
@@ -216,38 +288,75 @@ export const testCreateWebSocket = (options = {}) => {
|
|
|
216
288
|
});
|
|
217
289
|
return ok();
|
|
218
290
|
},
|
|
219
|
-
getReadyState: () =>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
291
|
+
getReadyState: () => (state.isDisposed ? "closed" : state.readyState),
|
|
292
|
+
isOpen: () => !state.isDisposed && state.readyState === "open",
|
|
293
|
+
reconnect: () => {
|
|
294
|
+
// A closed socket settles on its own, and a retrying wrapper has no
|
|
295
|
+
// socket, as in `createWebSocket`.
|
|
296
|
+
if (state.isDisposed ||
|
|
297
|
+
state.readyState === "closed" ||
|
|
298
|
+
state.isWaitingToRetry)
|
|
299
|
+
return;
|
|
300
|
+
state.readyState = "connecting";
|
|
301
|
+
state.isWaitingToRetry = true;
|
|
302
|
+
reconnectedUrls.push(url);
|
|
228
303
|
},
|
|
229
304
|
[Symbol.asyncDispose]: () => {
|
|
230
|
-
const state = getState(url);
|
|
231
305
|
state.isDisposed = true;
|
|
232
|
-
state.
|
|
306
|
+
state.readyState = "closed";
|
|
233
307
|
return Promise.resolve();
|
|
234
308
|
},
|
|
235
309
|
});
|
|
236
310
|
};
|
|
237
311
|
return Object.assign(createWebSocket, {
|
|
238
312
|
createdUrls,
|
|
313
|
+
reconnectedUrls,
|
|
239
314
|
sentMessages,
|
|
240
315
|
message: (url, data) => {
|
|
241
|
-
getState(url)
|
|
316
|
+
const state = getState(url);
|
|
317
|
+
assert(!state.isDisposed, `Test WebSocket for ${url} is disposed.`);
|
|
318
|
+
state.options?.onMessage?.(data);
|
|
242
319
|
},
|
|
243
320
|
open: (url) => {
|
|
244
321
|
const state = getState(url);
|
|
245
|
-
state.
|
|
322
|
+
assert(!state.isDisposed, `Test WebSocket for ${url} is disposed.`);
|
|
323
|
+
state.readyState = "open";
|
|
324
|
+
state.isWaitingToRetry = false;
|
|
246
325
|
state.options?.onOpen?.();
|
|
247
326
|
},
|
|
327
|
+
close: (url, event = {}) => {
|
|
328
|
+
const state = getState(url);
|
|
329
|
+
assert(!state.isDisposed, `Test WebSocket for ${url} is disposed.`);
|
|
330
|
+
state.readyState = "closed";
|
|
331
|
+
state.options?.onClose?.({
|
|
332
|
+
code: 1006,
|
|
333
|
+
reason: "",
|
|
334
|
+
wasClean: false,
|
|
335
|
+
...event,
|
|
336
|
+
});
|
|
337
|
+
// `createWebSocket` still holds the closed socket while it reports the
|
|
338
|
+
// close, and drops it to retry once that settles. Anything the handler
|
|
339
|
+
// schedules therefore still reads `closed`; anything later reads
|
|
340
|
+
// `connecting`.
|
|
341
|
+
queueMicrotask(() => {
|
|
342
|
+
if (state.readyState === "closed" && !state.isDisposed) {
|
|
343
|
+
state.readyState = "connecting";
|
|
344
|
+
state.isWaitingToRetry = true;
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
error: (url, error) => {
|
|
349
|
+
const state = getState(url);
|
|
350
|
+
assert(!state.isDisposed, `Test WebSocket for ${url} is disposed.`);
|
|
351
|
+
state.options?.onError?.(error);
|
|
352
|
+
},
|
|
248
353
|
});
|
|
249
354
|
};
|
|
250
|
-
/**
|
|
355
|
+
/**
|
|
356
|
+
* Opens a native {@link WebSocket} and returns {@link TestSetupWebSocket}.
|
|
357
|
+
*
|
|
358
|
+
* @group Testing
|
|
359
|
+
*/
|
|
251
360
|
export const testSetupWebSocket = async (url) => {
|
|
252
361
|
const socket = new globalThis.WebSocket(url);
|
|
253
362
|
socket.binaryType = "arraybuffer";
|