@evolu/common 7.4.1 → 8.0.0-next.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/README.md +4 -61
- package/dist/src/Array.d.ts +343 -102
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +181 -85
- package/dist/src/Assert.d.ts +57 -11
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +47 -11
- package/dist/src/BigInt.d.ts +6 -1
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -0
- package/dist/src/Brand.d.ts +40 -12
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Brand.js +5 -0
- package/dist/src/Buffer.d.ts +6 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +7 -2
- package/dist/src/Cache.d.ts +10 -2
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +6 -0
- package/dist/src/Callbacks.d.ts +10 -7
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +23 -13
- package/dist/src/Console.d.ts +361 -69
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +217 -117
- package/dist/src/Crypto.d.ts +70 -21
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +72 -27
- package/dist/src/Eq.d.ts +22 -2
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +21 -1
- package/dist/src/Error.d.ts +23 -12
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +27 -11
- package/dist/src/Function.d.ts +128 -56
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +118 -14
- package/dist/src/Identicon.d.ts +5 -0
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +6 -1
- package/dist/src/Lookup.d.ts +160 -0
- package/dist/src/Lookup.d.ts.map +1 -0
- package/dist/src/Lookup.js +192 -0
- package/dist/src/Microtask.d.ts +21 -0
- package/dist/src/Microtask.d.ts.map +1 -0
- package/dist/src/Microtask.js +37 -0
- package/dist/src/Number.d.ts +18 -4
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +23 -1
- package/dist/src/Object.d.ts +110 -5
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +108 -5
- package/dist/src/Option.d.ts +58 -0
- package/dist/src/Option.d.ts.map +1 -0
- package/dist/src/Option.js +43 -0
- package/dist/src/Order.d.ts +5 -0
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +8 -3
- package/dist/src/Platform.d.ts +36 -2
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +10 -6
- package/dist/src/Polyfills.d.ts +27 -0
- package/dist/src/Polyfills.d.ts.map +1 -0
- package/dist/src/Polyfills.js +299 -0
- package/dist/src/Random.d.ts +18 -7
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +9 -7
- package/dist/src/Redacted.d.ts +7 -4
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +5 -0
- package/dist/src/Ref.d.ts +31 -16
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +35 -7
- package/dist/src/RefCount.d.ts +62 -0
- package/dist/src/RefCount.d.ts.map +1 -0
- package/dist/src/RefCount.js +83 -0
- package/dist/src/Relation.d.ts +84 -29
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +83 -105
- package/dist/src/Resource.d.ts +263 -0
- package/dist/src/Resource.d.ts.map +1 -0
- package/dist/src/Resource.js +389 -0
- package/dist/src/Result.d.ts +390 -374
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +142 -70
- package/dist/src/Schedule.d.ts +953 -0
- package/dist/src/Schedule.d.ts.map +1 -0
- package/dist/src/Schedule.js +1199 -0
- package/dist/src/Set.d.ts +181 -0
- package/dist/src/Set.d.ts.map +1 -0
- package/dist/src/Set.js +137 -0
- package/dist/src/Skiplist.d.ts +6 -1
- package/dist/src/Skiplist.d.ts.map +1 -1
- package/dist/src/Skiplist.js +5 -0
- package/dist/src/Sqlite.d.ts +160 -60
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +290 -190
- package/dist/src/Store.d.ts +36 -28
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +55 -15
- package/dist/src/String.d.ts +5 -0
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +5 -0
- package/dist/src/Task.d.ts +2263 -423
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1488 -348
- package/dist/src/Test.d.ts +122 -0
- package/dist/src/Test.d.ts.map +1 -0
- package/dist/src/Test.js +66 -0
- package/dist/src/Time.d.ts +201 -48
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +190 -86
- package/dist/src/Tracer.d.ts +48 -0
- package/dist/src/Tracer.d.ts.map +1 -0
- package/dist/src/Tracer.js +6 -0
- package/dist/src/Type.d.ts +582 -209
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +362 -240
- package/dist/src/Types.d.ts +160 -6
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +22 -2
- package/dist/src/WebSocket.d.ts +113 -58
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +210 -118
- package/dist/src/Worker.d.ts +245 -60
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +220 -64
- package/dist/src/index.d.ts +25 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +19 -3
- package/dist/src/local-first/Db.d.ts +24 -213
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +622 -293
- package/dist/src/local-first/Error.d.ts +12 -0
- package/dist/src/local-first/Error.d.ts.map +1 -0
- package/dist/src/local-first/Error.js +6 -0
- package/dist/src/local-first/Evolu.d.ts +330 -285
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +472 -402
- package/dist/src/local-first/LocalAuth.d.ts +10 -10
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/LocalAuth.js +6 -1
- package/dist/src/local-first/Owner.d.ts +70 -27
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +27 -14
- package/dist/src/local-first/Protocol.d.ts +58 -50
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +253 -155
- package/dist/src/local-first/Query.d.ts +133 -43
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +139 -90
- package/dist/src/local-first/Relay.d.ts +18 -38
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +59 -171
- package/dist/src/local-first/Schema.d.ts +139 -144
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +78 -163
- package/dist/src/local-first/Shared.d.ts +125 -0
- package/dist/src/local-first/Shared.d.ts.map +1 -0
- package/dist/src/local-first/Shared.js +569 -0
- package/dist/src/local-first/Storage.d.ts +50 -53
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +121 -173
- package/dist/src/local-first/Timestamp.d.ts +28 -35
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +24 -25
- package/dist/src/local-first/index.d.ts +2 -15
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +2 -15
- package/package.json +36 -26
- package/src/Array.ts +467 -106
- package/src/Assert.ts +87 -12
- package/src/BigInt.ts +7 -1
- package/src/Brand.ts +41 -12
- package/src/Buffer.ts +10 -4
- package/src/Cache.ts +11 -2
- package/src/Callbacks.ts +33 -23
- package/src/Console.ts +567 -178
- package/src/Crypto.ts +111 -62
- package/src/Eq.ts +30 -3
- package/src/Error.ts +50 -34
- package/src/Function.ts +140 -68
- package/src/Identicon.ts +7 -1
- package/src/Lookup.ts +415 -0
- package/src/Microtask.ts +58 -0
- package/src/Number.ts +40 -7
- package/src/Object.ts +155 -16
- package/src/Option.ts +74 -0
- package/src/Order.ts +11 -3
- package/src/Platform.ts +43 -7
- package/src/Polyfills.ts +465 -0
- package/src/Random.ts +25 -11
- package/src/Redacted.ts +8 -4
- package/src/Ref.ts +72 -21
- package/src/RefCount.ts +170 -0
- package/src/Relation.ts +188 -136
- package/src/Resource.ts +864 -0
- package/src/Result.ts +548 -381
- package/src/Schedule.ts +1471 -0
- package/src/Set.ts +247 -0
- package/src/Skiplist.ts +7 -1
- package/src/Sqlite.ts +409 -304
- package/src/Store.ts +94 -48
- package/src/String.ts +6 -0
- package/src/Task.ts +3685 -740
- package/src/Test.ts +162 -0
- package/src/Time.ts +348 -145
- package/src/Tracer.ts +54 -0
- package/src/Type.ts +851 -430
- package/src/Types.ts +202 -7
- package/src/WebSocket.ts +311 -178
- package/src/Worker.ts +527 -147
- package/src/index.ts +74 -3
- package/src/local-first/Db.ts +871 -597
- package/src/local-first/Error.ts +17 -0
- package/src/local-first/Evolu.ts +884 -824
- package/src/local-first/LocalAuth.ts +10 -10
- package/src/local-first/Owner.ts +94 -42
- package/src/local-first/Protocol.ts +440 -303
- package/src/local-first/Query.ts +243 -194
- package/src/local-first/Relay.ts +91 -252
- package/src/local-first/Schema.ts +284 -394
- package/src/local-first/Shared.ts +932 -0
- package/src/local-first/Storage.ts +206 -277
- package/src/local-first/Timestamp.ts +38 -45
- package/src/local-first/index.ts +2 -16
- package/LICENSE +0 -21
- package/dist/src/Instances.d.ts +0 -34
- package/dist/src/Instances.d.ts.map +0 -1
- package/dist/src/Instances.js +0 -44
- package/dist/src/Resources.d.ts +0 -118
- package/dist/src/Resources.d.ts.map +0 -1
- package/dist/src/Resources.js +0 -197
- package/dist/src/local-first/Platform.d.ts +0 -25
- package/dist/src/local-first/Platform.d.ts.map +0 -1
- package/dist/src/local-first/Platform.js +0 -1
- package/dist/src/local-first/Public.d.ts +0 -22
- package/dist/src/local-first/Public.d.ts.map +0 -1
- package/dist/src/local-first/Public.js +0 -15
- package/dist/src/local-first/PublicKysely.d.ts +0 -148
- package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
- package/dist/src/local-first/PublicKysely.js +0 -184
- package/dist/src/local-first/Sync.d.ts +0 -112
- package/dist/src/local-first/Sync.d.ts.map +0 -1
- package/dist/src/local-first/Sync.js +0 -526
- package/src/Instances.ts +0 -92
- package/src/Resources.ts +0 -367
- package/src/local-first/Platform.ts +0 -27
- package/src/local-first/Public.ts +0 -42
- package/src/local-first/PublicKysely.ts +0 -239
- package/src/local-first/Sync.ts +0 -949
package/src/WebSocket.ts
CHANGED
|
@@ -1,10 +1,69 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket with auto-reconnect.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
import { lazyTrue } from "./Function.js";
|
|
8
|
+
import type { Result } from "./Result.js";
|
|
9
|
+
import { err, ok } from "./Result.js";
|
|
10
|
+
import type { Schedule } from "./Schedule.js";
|
|
11
|
+
import { exponential, jitter, maxDelay } from "./Schedule.js";
|
|
12
|
+
import type { RetryError, Task } from "./Task.js";
|
|
13
|
+
import { callback, retry } from "./Task.js";
|
|
14
|
+
import type { Millis } from "./Time.js";
|
|
15
|
+
import { String, type Typed } from "./Type.js";
|
|
16
|
+
import { assert } from "./Assert.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* WebSocket with auto-reconnect.
|
|
20
|
+
*
|
|
21
|
+
* The API mirrors native
|
|
22
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket | WebSocket}
|
|
23
|
+
* but retries connections indefinitely by default. This design accounts for the
|
|
24
|
+
* fact that browser and React Native online/offline detection APIs are
|
|
25
|
+
* unreliable — they may report online status incorrectly, so the only reliable
|
|
26
|
+
* approach is to keep attempting reconnection.
|
|
27
|
+
*
|
|
28
|
+
* Created via {@link createWebSocket} which returns a {@link Task}.
|
|
29
|
+
*
|
|
30
|
+
* Disposing the WebSocket closes the connection.
|
|
31
|
+
*
|
|
32
|
+
* ## How Binary Messages Work
|
|
33
|
+
*
|
|
34
|
+
* The Server Chooses the Message Type:
|
|
35
|
+
*
|
|
36
|
+
* - Text (0x1) → Sent as UTF-8 encoded text (always received as a string in the
|
|
37
|
+
* browser).
|
|
38
|
+
* - Binary (0x2) → Sent as raw binary data (received as a Blob or ArrayBuffer,
|
|
39
|
+
* depending on binaryType).
|
|
40
|
+
*
|
|
41
|
+
* The Client's binaryType Controls How Binary Data is Processed:
|
|
42
|
+
*
|
|
43
|
+
* - If the server sends a text frame (0x1), the browser always delivers
|
|
44
|
+
* event.data as a string, regardless of binaryType.
|
|
45
|
+
* - If the server sends a binary frame (0x2), the browser delivers event.data as:
|
|
46
|
+
*
|
|
47
|
+
* - A Blob (default: "blob")
|
|
48
|
+
* - An ArrayBuffer ("arraybuffer")
|
|
49
|
+
*
|
|
50
|
+
* ### Example
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* const ws = await run(
|
|
54
|
+
* createWebSocket("wss://example.com", {
|
|
55
|
+
* onMessage: (data) => console.log("Received:", data),
|
|
56
|
+
* onOpen: () => console.log("Connected"),
|
|
57
|
+
* onClose: () => console.log("Disconnected"),
|
|
58
|
+
* }),
|
|
59
|
+
* );
|
|
60
|
+
* if (ws.ok) {
|
|
61
|
+
* ws.value.send("Hello");
|
|
62
|
+
* // Later: await ws.value[Symbol.asyncDispose]();
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export interface WebSocket extends AsyncDisposable {
|
|
8
67
|
/**
|
|
9
68
|
* Send data through the WebSocket connection. Returns {@link Result} with an
|
|
10
69
|
* error if the data couldn't be sent.
|
|
@@ -25,53 +84,66 @@ export interface WebSocket extends Disposable {
|
|
|
25
84
|
*
|
|
26
85
|
* https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
|
|
27
86
|
*/
|
|
28
|
-
export interface WebSocketSendError {
|
|
29
|
-
readonly type: "WebSocketSendError";
|
|
30
|
-
}
|
|
87
|
+
export interface WebSocketSendError extends Typed<"WebSocketSendError"> {}
|
|
31
88
|
|
|
32
89
|
/** WebSocket connection states. */
|
|
33
90
|
export type WebSocketReadyState = "connecting" | "open" | "closing" | "closed";
|
|
34
91
|
|
|
92
|
+
/** {@link Task} that creates a {@link WebSocket}. */
|
|
35
93
|
export type CreateWebSocket = (
|
|
36
94
|
url: string,
|
|
37
95
|
options?: WebSocketOptions,
|
|
38
|
-
) => WebSocket
|
|
96
|
+
) => Task<WebSocket>;
|
|
39
97
|
|
|
40
98
|
export interface CreateWebSocketDep {
|
|
41
99
|
readonly createWebSocket: CreateWebSocket;
|
|
42
100
|
}
|
|
43
101
|
|
|
44
|
-
/** Options for creating {@link WebSocket} */
|
|
102
|
+
/** Options for creating {@link WebSocket}. */
|
|
45
103
|
export interface WebSocketOptions {
|
|
46
104
|
/** Protocol(s) to use with the WebSocket connection. */
|
|
47
|
-
protocols?: string |
|
|
105
|
+
readonly protocols?: string | ReadonlyArray<string>;
|
|
48
106
|
|
|
49
107
|
/** Sets the binary type for the data being received. */
|
|
50
|
-
binaryType?: "blob" | "arraybuffer";
|
|
108
|
+
readonly binaryType?: "blob" | "arraybuffer";
|
|
51
109
|
|
|
52
110
|
/** Callback when the connection is established. */
|
|
53
|
-
onOpen?: () => void;
|
|
111
|
+
readonly onOpen?: () => void;
|
|
54
112
|
|
|
55
113
|
/** Callback when an error occurs. */
|
|
56
|
-
onError?: (error: WebSocketError) => void;
|
|
114
|
+
readonly onError?: (error: WebSocketError) => void;
|
|
57
115
|
|
|
58
116
|
/** Callback when the connection is closed. */
|
|
59
|
-
onClose?: (event: CloseEvent) => void;
|
|
117
|
+
readonly onClose?: (event: CloseEvent) => void;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Determines whether a closed connection should trigger a retry.
|
|
121
|
+
*
|
|
122
|
+
* Return false to stop retrying, for example on auth errors or maintenance.
|
|
123
|
+
*/
|
|
124
|
+
readonly shouldRetryOnClose?: (event: CloseEvent) => boolean;
|
|
60
125
|
|
|
61
126
|
/** Callback when message data is received. */
|
|
62
|
-
onMessage?: (data: string | ArrayBuffer | Blob) => void;
|
|
127
|
+
readonly onMessage?: (data: string | ArrayBuffer | Blob) => void;
|
|
63
128
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Retry schedule for reconnection. Defaults to:
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* // A jittered, capped, unlimited exponential backoff.
|
|
134
|
+
* jitter(1)(maxDelay("30s")(exponential("100ms")));
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
readonly schedule?: Schedule<Millis, WebSocketRetryError>;
|
|
66
138
|
|
|
67
139
|
/**
|
|
68
140
|
* For custom WebSocket implementations.
|
|
69
141
|
*
|
|
70
|
-
* This
|
|
142
|
+
* This supports blob:
|
|
71
143
|
*
|
|
72
144
|
* https://github.com/callstackincubator/react-native-fast-io
|
|
73
145
|
*/
|
|
74
|
-
WebSocketConstructor?: typeof globalThis.WebSocket;
|
|
146
|
+
readonly WebSocketConstructor?: typeof globalThis.WebSocket;
|
|
75
147
|
}
|
|
76
148
|
|
|
77
149
|
export type WebSocketError =
|
|
@@ -81,21 +153,22 @@ export type WebSocketError =
|
|
|
81
153
|
|
|
82
154
|
/**
|
|
83
155
|
* An error that occurs when a connection cannot be established due to a network
|
|
84
|
-
* error.
|
|
156
|
+
* error. Fires before `onclose`.
|
|
85
157
|
*/
|
|
86
|
-
export interface WebSocketConnectError {
|
|
87
|
-
readonly type: "WebSocketConnectError";
|
|
158
|
+
export interface WebSocketConnectError extends Typed<"WebSocketConnectError"> {
|
|
88
159
|
readonly event: Event;
|
|
89
160
|
}
|
|
90
161
|
|
|
91
162
|
/**
|
|
92
|
-
* An error that occurs when
|
|
93
|
-
* failure to send
|
|
163
|
+
* An error that occurs when an established connection encounters an issue
|
|
164
|
+
* (e.g., failure to send data). Fires before `onclose`.
|
|
165
|
+
*
|
|
166
|
+
* Note: Only Node.js and WebKit fire this error on abrupt server termination.
|
|
167
|
+
* Chromium and Firefox only fire `onclose` without a preceding error event.
|
|
94
168
|
*
|
|
95
169
|
* https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event
|
|
96
170
|
*/
|
|
97
|
-
export interface WebSocketConnectionError {
|
|
98
|
-
readonly type: "WebSocketConnectionError";
|
|
171
|
+
export interface WebSocketConnectionError extends Typed<"WebSocketConnectionError"> {
|
|
99
172
|
readonly event: Event;
|
|
100
173
|
}
|
|
101
174
|
|
|
@@ -103,172 +176,232 @@ export type WebSocketRetryError =
|
|
|
103
176
|
| WebSocketConnectError
|
|
104
177
|
| WebSocketConnectionCloseError;
|
|
105
178
|
|
|
106
|
-
|
|
107
|
-
|
|
179
|
+
/** An error that occurs when the connection is closed by the server. */
|
|
180
|
+
export interface WebSocketConnectionCloseError extends Typed<"WebSocketConnectionCloseError"> {
|
|
108
181
|
readonly event: CloseEvent;
|
|
109
182
|
}
|
|
110
183
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
onOpen,
|
|
148
|
-
onClose,
|
|
149
|
-
onMessage,
|
|
150
|
-
onError,
|
|
151
|
-
retryOptions,
|
|
152
|
-
WebSocketConstructor = globalThis.WebSocket,
|
|
153
|
-
} = {},
|
|
154
|
-
) => {
|
|
155
|
-
let isDisposed = false;
|
|
156
|
-
|
|
157
|
-
const reconnectController = new AbortController();
|
|
184
|
+
/** Create a new {@link WebSocket}. */
|
|
185
|
+
export const createWebSocket: CreateWebSocket =
|
|
186
|
+
(
|
|
187
|
+
url,
|
|
188
|
+
{
|
|
189
|
+
protocols,
|
|
190
|
+
binaryType,
|
|
191
|
+
onOpen,
|
|
192
|
+
onClose,
|
|
193
|
+
shouldRetryOnClose = lazyTrue,
|
|
194
|
+
onMessage,
|
|
195
|
+
onError,
|
|
196
|
+
schedule = jitter(1)(maxDelay("30s")(exponential("100ms"))),
|
|
197
|
+
WebSocketConstructor = globalThis.WebSocket,
|
|
198
|
+
} = {},
|
|
199
|
+
) =>
|
|
200
|
+
async (run) => {
|
|
201
|
+
await using stack = new AsyncDisposableStack();
|
|
202
|
+
|
|
203
|
+
let socket: globalThis.WebSocket | null = null;
|
|
204
|
+
let disposed = false;
|
|
205
|
+
|
|
206
|
+
const closeSocket = () => {
|
|
207
|
+
if (!socket) return;
|
|
208
|
+
|
|
209
|
+
socket.onopen = null;
|
|
210
|
+
socket.onclose = null;
|
|
211
|
+
socket.onmessage = null;
|
|
212
|
+
socket.onerror = null;
|
|
213
|
+
|
|
214
|
+
if (
|
|
215
|
+
socket.readyState !== socket.CLOSING &&
|
|
216
|
+
socket.readyState !== socket.CLOSED
|
|
217
|
+
) {
|
|
218
|
+
socket.close();
|
|
219
|
+
}
|
|
158
220
|
|
|
159
|
-
|
|
160
|
-
|
|
221
|
+
socket = null;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A task that connects and stays connected until the connection closes or
|
|
226
|
+
* errors. Returns error to trigger retry.
|
|
227
|
+
*/
|
|
228
|
+
const connect: Task<void, WebSocketRetryError> = callback(({ err, ok }) => {
|
|
229
|
+
closeSocket();
|
|
230
|
+
|
|
231
|
+
socket = new WebSocketConstructor(
|
|
232
|
+
url,
|
|
233
|
+
String.is(protocols) ? protocols : protocols && [...protocols],
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (binaryType) socket.binaryType = binaryType;
|
|
237
|
+
|
|
238
|
+
let isOpen = false;
|
|
239
|
+
|
|
240
|
+
socket.onopen = () => {
|
|
241
|
+
isOpen = true;
|
|
242
|
+
onOpen?.();
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
socket.onclose = (event) => {
|
|
246
|
+
onClose?.(event);
|
|
247
|
+
if (shouldRetryOnClose(event)) {
|
|
248
|
+
err({ type: "WebSocketConnectionCloseError", event });
|
|
249
|
+
} else {
|
|
250
|
+
ok();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
socket.onmessage = (event: MessageEvent<string | ArrayBuffer | Blob>) => {
|
|
255
|
+
onMessage?.(event.data);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
socket.onerror = (event) => {
|
|
259
|
+
const error: WebSocketConnectionError | WebSocketConnectError = isOpen
|
|
260
|
+
? { type: "WebSocketConnectionError", event }
|
|
261
|
+
: { type: "WebSocketConnectError", event };
|
|
262
|
+
onError?.(error);
|
|
263
|
+
|
|
264
|
+
if (error.type === "WebSocketConnectError") err(error);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return closeSocket;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const retryFiber = stack.use(run.daemon(retry(connect, schedule)));
|
|
271
|
+
|
|
272
|
+
// Report RetryError (schedule exhausted) via onError callback
|
|
273
|
+
void retryFiber.then((result) => {
|
|
274
|
+
if (!result.ok && result.error.type === "RetryError") {
|
|
275
|
+
onError?.(result.error);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const moved = stack.move();
|
|
280
|
+
|
|
281
|
+
return ok<WebSocket>({
|
|
282
|
+
send: (data) => {
|
|
283
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
|
|
284
|
+
if (!socket || socket.readyState === socket.CONNECTING) {
|
|
285
|
+
return err({ type: "WebSocketSendError" });
|
|
286
|
+
}
|
|
287
|
+
socket.send(data);
|
|
288
|
+
return ok();
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
getReadyState: () => {
|
|
292
|
+
if (disposed) return "closed";
|
|
293
|
+
return socket ? nativeToStringState[socket.readyState] : "connecting";
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
isOpen: () =>
|
|
297
|
+
!disposed && socket?.readyState === globalThis.WebSocket.OPEN,
|
|
298
|
+
|
|
299
|
+
[Symbol.asyncDispose]: async () => {
|
|
300
|
+
disposed = true;
|
|
301
|
+
await moved.disposeAsync();
|
|
302
|
+
},
|
|
303
|
+
});
|
|
161
304
|
};
|
|
162
305
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
306
|
+
/** Creates a deterministic in-memory {@link CreateWebSocket} for testing. */
|
|
307
|
+
export interface TestCreateWebSocket extends CreateWebSocket {
|
|
308
|
+
readonly createdUrls: Array<string>;
|
|
309
|
+
readonly sentMessages: Array<{
|
|
310
|
+
readonly url: string;
|
|
311
|
+
readonly data: string | ArrayBufferLike | Blob | ArrayBufferView;
|
|
312
|
+
}>;
|
|
313
|
+
readonly message: (url: string, data: string | ArrayBuffer | Blob) => void;
|
|
314
|
+
readonly open: (url: string) => void;
|
|
315
|
+
}
|
|
167
316
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
socket.
|
|
171
|
-
|
|
172
|
-
socket.onerror = null;
|
|
317
|
+
export const testCreateWebSocket = (
|
|
318
|
+
options: {
|
|
319
|
+
/** Throw immediately when a socket is created. */
|
|
320
|
+
readonly throwOnCreate?: boolean;
|
|
173
321
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
322
|
+
/** Initial open state of created sockets. Defaults to true. */
|
|
323
|
+
readonly isOpen?: boolean;
|
|
324
|
+
} = {},
|
|
325
|
+
): TestCreateWebSocket => {
|
|
326
|
+
const createdUrls: Array<string> = [];
|
|
327
|
+
const sentMessages: Array<{
|
|
328
|
+
readonly url: string;
|
|
329
|
+
readonly data: string | ArrayBufferLike | Blob | ArrayBufferView;
|
|
330
|
+
}> = [];
|
|
331
|
+
const stateByUrl = new Map<
|
|
332
|
+
string,
|
|
333
|
+
{
|
|
334
|
+
options: WebSocketOptions | undefined;
|
|
335
|
+
isOpen: boolean;
|
|
336
|
+
isDisposed: boolean;
|
|
179
337
|
}
|
|
180
|
-
|
|
338
|
+
>();
|
|
339
|
+
|
|
340
|
+
const getState = (url: string) => {
|
|
341
|
+
const state = stateByUrl.get(url);
|
|
342
|
+
assert(state, `Test WebSocket for ${url} does not exist.`);
|
|
343
|
+
return state;
|
|
181
344
|
};
|
|
182
345
|
|
|
183
|
-
|
|
184
|
-
|
|
346
|
+
const createWebSocket: CreateWebSocket = (url, socketOptions) => () => {
|
|
347
|
+
if (options.throwOnCreate) {
|
|
348
|
+
throw new Error("testCreateWebSocket is configured to throw on create");
|
|
349
|
+
}
|
|
185
350
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
onError?.(error);
|
|
223
|
-
|
|
224
|
-
// Trigger reconnect only on WebSocketConnectError.
|
|
225
|
-
if (error.type === "WebSocketConnectError") {
|
|
226
|
-
resolve(err(error));
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
socket.onclose = (event) => {
|
|
231
|
-
onClose?.(event);
|
|
232
|
-
resolve(err({ type: "WebSocketConnectionCloseError", event }));
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
socket.onmessage = (event) => {
|
|
236
|
-
onMessage?.(event.data as string | ArrayBuffer | Blob);
|
|
237
|
-
};
|
|
238
|
-
}),
|
|
239
|
-
)(reconnectController).then((result) => {
|
|
240
|
-
if (result.ok || result.error.type === "AbortError") return;
|
|
241
|
-
onError?.(result.error as WebSocketError);
|
|
242
|
-
});
|
|
351
|
+
createdUrls.push(url);
|
|
352
|
+
stateByUrl.set(url, {
|
|
353
|
+
options: socketOptions,
|
|
354
|
+
isOpen: options.isOpen ?? true,
|
|
355
|
+
isDisposed: false,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return ok({
|
|
359
|
+
send: (data) => {
|
|
360
|
+
const state = getState(url);
|
|
361
|
+
if (state.isDisposed || !state.isOpen) {
|
|
362
|
+
return err({ type: "WebSocketSendError" });
|
|
363
|
+
}
|
|
364
|
+
sentMessages.push({ url, data });
|
|
365
|
+
return ok();
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
getReadyState: () => {
|
|
369
|
+
const state = getState(url);
|
|
370
|
+
if (state.isDisposed) return "closed";
|
|
371
|
+
return state.isOpen ? "open" : "closed";
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
isOpen: () => {
|
|
375
|
+
const state = getState(url);
|
|
376
|
+
return !state.isDisposed && state.isOpen;
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
[Symbol.asyncDispose]: () => {
|
|
380
|
+
const state = getState(url);
|
|
381
|
+
state.isDisposed = true;
|
|
382
|
+
state.isOpen = false;
|
|
383
|
+
return Promise.resolve();
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
};
|
|
243
387
|
|
|
244
|
-
return {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
socket.send(data);
|
|
251
|
-
return ok();
|
|
388
|
+
return Object.assign(createWebSocket, {
|
|
389
|
+
createdUrls,
|
|
390
|
+
sentMessages,
|
|
391
|
+
message: (url: string, data: string | ArrayBuffer | Blob) => {
|
|
392
|
+
getState(url).options?.onMessage?.(data);
|
|
252
393
|
},
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
isOpen: () => (socket ? socket.readyState === socket.OPEN : false),
|
|
258
|
-
|
|
259
|
-
[Symbol.dispose]() {
|
|
260
|
-
if (isDisposed) return;
|
|
261
|
-
isDisposed = true;
|
|
262
|
-
reconnectController.abort();
|
|
263
|
-
disposeSocket();
|
|
264
|
-
disposePromise?.();
|
|
394
|
+
open: (url: string) => {
|
|
395
|
+
const state = getState(url);
|
|
396
|
+
state.isOpen = true;
|
|
397
|
+
state.options?.onOpen?.();
|
|
265
398
|
},
|
|
266
|
-
};
|
|
399
|
+
});
|
|
267
400
|
};
|
|
268
401
|
|
|
269
402
|
const nativeToStringState: Record<number, WebSocketReadyState> = {
|
|
270
|
-
[WebSocket.CONNECTING]: "connecting",
|
|
271
|
-
[WebSocket.OPEN]: "open",
|
|
272
|
-
[WebSocket.CLOSING]: "closing",
|
|
273
|
-
[WebSocket.CLOSED]: "closed",
|
|
403
|
+
[globalThis.WebSocket.CONNECTING]: "connecting",
|
|
404
|
+
[globalThis.WebSocket.OPEN]: "open",
|
|
405
|
+
[globalThis.WebSocket.CLOSING]: "closing",
|
|
406
|
+
[globalThis.WebSocket.CLOSED]: "closed",
|
|
274
407
|
};
|