@evolu/common 7.4.1 → 8.0.0-next.1
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 +162 -0
- package/dist/src/Lookup.d.ts.map +1 -0
- package/dist/src/Lookup.js +200 -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 +29 -0
- package/dist/src/Polyfills.d.ts.map +1 -0
- package/dist/src/Polyfills.js +329 -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 +2274 -423
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1499 -348
- package/dist/src/Test.d.ts +150 -0
- package/dist/src/Test.d.ts.map +1 -0
- package/dist/src/Test.js +97 -0
- package/dist/src/Time.d.ts +202 -48
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +199 -88
- 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 +587 -209
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +370 -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 +130 -58
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +274 -113
- 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 +621 -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 +34 -49
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +59 -177
- 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 +54 -58
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +127 -179
- 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 +37 -24
- 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 +429 -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 +520 -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 +3696 -740
- package/src/Test.ts +202 -0
- package/src/Time.ts +357 -147
- package/src/Tracer.ts +54 -0
- package/src/Type.ts +866 -430
- package/src/Types.ts +202 -7
- package/src/WebSocket.ts +404 -167
- package/src/Worker.ts +527 -147
- package/src/index.ts +74 -3
- package/src/local-first/Db.ts +867 -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 +441 -307
- package/src/local-first/Query.ts +243 -194
- package/src/local-first/Relay.ts +119 -273
- package/src/local-first/Schema.ts +284 -394
- package/src/local-first/Shared.ts +932 -0
- package/src/local-first/Storage.ts +211 -283
- package/src/local-first/Timestamp.ts +38 -45
- package/src/local-first/index.ts +2 -16
- 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,16 +1,77 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket with auto-reconnect.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { assert } from "./Assert.js";
|
|
8
|
+
import { lazyTrue } from "./Function.js";
|
|
9
|
+
import type { Result } from "./Result.js";
|
|
10
|
+
import { err, ok } from "./Result.js";
|
|
11
|
+
import type { Schedule } from "./Schedule.js";
|
|
12
|
+
import { exponential, jitter, maxDelay } from "./Schedule.js";
|
|
13
|
+
import type { RetryError, Task } from "./Task.js";
|
|
14
|
+
import { callback, retry } from "./Task.js";
|
|
15
|
+
import type { Millis } from "./Time.js";
|
|
16
|
+
import { ArrayBuffer, String, Uint8Array, type Typed } from "./Type.js";
|
|
5
17
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
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 starts closing the connection without waiting for the close event
|
|
31
|
+
* so disposal stays immediate. This wrapper treats disposal as local teardown,
|
|
32
|
+
* not as waiting for the full WebSocket close handshake to finish.
|
|
33
|
+
*
|
|
34
|
+
* ## How Binary Messages Work
|
|
35
|
+
*
|
|
36
|
+
* The Server Chooses the Message Type:
|
|
37
|
+
*
|
|
38
|
+
* - Text (0x1) → Sent as UTF-8 encoded text (always received as a string in the
|
|
39
|
+
* browser).
|
|
40
|
+
* - Binary (0x2) → Sent as raw binary data (received as a Blob or ArrayBuffer,
|
|
41
|
+
* depending on binaryType).
|
|
42
|
+
*
|
|
43
|
+
* The Client's binaryType Controls How Binary Data is Processed:
|
|
44
|
+
*
|
|
45
|
+
* - If the server sends a text frame (0x1), the browser always delivers
|
|
46
|
+
* event.data as a string, regardless of binaryType.
|
|
47
|
+
* - If the server sends a binary frame (0x2), the browser delivers event.data as:
|
|
48
|
+
*
|
|
49
|
+
* - A Blob (default: "blob")
|
|
50
|
+
* - An ArrayBuffer ("arraybuffer")
|
|
51
|
+
*
|
|
52
|
+
* ### Example
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* const ws = await run(
|
|
56
|
+
* createWebSocket("wss://example.com", {
|
|
57
|
+
* onMessage: (data) => console.log("Received:", data),
|
|
58
|
+
* onOpen: () => console.log("Connected"),
|
|
59
|
+
* onClose: () => console.log("Disconnected"),
|
|
60
|
+
* }),
|
|
61
|
+
* );
|
|
62
|
+
* if (ws.ok) {
|
|
63
|
+
* ws.value.send("Hello");
|
|
64
|
+
* // Later: await ws.value[Symbol.asyncDispose]();
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export interface WebSocket extends AsyncDisposable {
|
|
8
69
|
/**
|
|
9
70
|
* Send data through the WebSocket connection. Returns {@link Result} with an
|
|
10
71
|
* error if the data couldn't be sent.
|
|
11
72
|
*/
|
|
12
73
|
send: (
|
|
13
|
-
data:
|
|
74
|
+
data: BufferSource | Blob | string | globalThis.Uint8Array,
|
|
14
75
|
) => Result<void, WebSocketSendError>;
|
|
15
76
|
|
|
16
77
|
readonly getReadyState: () => WebSocketReadyState;
|
|
@@ -25,53 +86,66 @@ export interface WebSocket extends Disposable {
|
|
|
25
86
|
*
|
|
26
87
|
* https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
|
|
27
88
|
*/
|
|
28
|
-
export interface WebSocketSendError {
|
|
29
|
-
readonly type: "WebSocketSendError";
|
|
30
|
-
}
|
|
89
|
+
export interface WebSocketSendError extends Typed<"WebSocketSendError"> {}
|
|
31
90
|
|
|
32
91
|
/** WebSocket connection states. */
|
|
33
92
|
export type WebSocketReadyState = "connecting" | "open" | "closing" | "closed";
|
|
34
93
|
|
|
94
|
+
/** {@link Task} that creates a {@link WebSocket}. */
|
|
35
95
|
export type CreateWebSocket = (
|
|
36
96
|
url: string,
|
|
37
97
|
options?: WebSocketOptions,
|
|
38
|
-
) => WebSocket
|
|
98
|
+
) => Task<WebSocket>;
|
|
39
99
|
|
|
40
100
|
export interface CreateWebSocketDep {
|
|
41
101
|
readonly createWebSocket: CreateWebSocket;
|
|
42
102
|
}
|
|
43
103
|
|
|
44
|
-
/** Options for creating {@link WebSocket} */
|
|
104
|
+
/** Options for creating {@link WebSocket}. */
|
|
45
105
|
export interface WebSocketOptions {
|
|
46
106
|
/** Protocol(s) to use with the WebSocket connection. */
|
|
47
|
-
protocols?: string |
|
|
107
|
+
readonly protocols?: string | ReadonlyArray<string>;
|
|
48
108
|
|
|
49
109
|
/** Sets the binary type for the data being received. */
|
|
50
|
-
binaryType?: "blob" | "arraybuffer";
|
|
110
|
+
readonly binaryType?: "blob" | "arraybuffer";
|
|
51
111
|
|
|
52
112
|
/** Callback when the connection is established. */
|
|
53
|
-
onOpen?: () => void;
|
|
113
|
+
readonly onOpen?: () => void;
|
|
54
114
|
|
|
55
115
|
/** Callback when an error occurs. */
|
|
56
|
-
onError?: (error: WebSocketError) => void;
|
|
116
|
+
readonly onError?: (error: WebSocketError) => void;
|
|
57
117
|
|
|
58
118
|
/** Callback when the connection is closed. */
|
|
59
|
-
onClose?: (event: CloseEvent) => void;
|
|
119
|
+
readonly onClose?: (event: CloseEvent) => void;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Determines whether a closed connection should trigger a retry.
|
|
123
|
+
*
|
|
124
|
+
* Return false to stop retrying, for example on auth errors or maintenance.
|
|
125
|
+
*/
|
|
126
|
+
readonly shouldRetryOnClose?: (event: CloseEvent) => boolean;
|
|
60
127
|
|
|
61
128
|
/** Callback when message data is received. */
|
|
62
|
-
onMessage?: (data: string | ArrayBuffer | Blob) => void;
|
|
129
|
+
readonly onMessage?: (data: string | ArrayBuffer | Blob) => void;
|
|
63
130
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Retry schedule for reconnection. Defaults to:
|
|
133
|
+
*
|
|
134
|
+
* ```ts
|
|
135
|
+
* // A jittered, capped, unlimited exponential backoff.
|
|
136
|
+
* jitter(1)(maxDelay("30s")(exponential("100ms")));
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
readonly schedule?: Schedule<Millis, WebSocketRetryError>;
|
|
66
140
|
|
|
67
141
|
/**
|
|
68
142
|
* For custom WebSocket implementations.
|
|
69
143
|
*
|
|
70
|
-
* This
|
|
144
|
+
* This supports blob:
|
|
71
145
|
*
|
|
72
146
|
* https://github.com/callstackincubator/react-native-fast-io
|
|
73
147
|
*/
|
|
74
|
-
WebSocketConstructor?: typeof globalThis.WebSocket;
|
|
148
|
+
readonly WebSocketConstructor?: typeof globalThis.WebSocket;
|
|
75
149
|
}
|
|
76
150
|
|
|
77
151
|
export type WebSocketError =
|
|
@@ -81,21 +155,22 @@ export type WebSocketError =
|
|
|
81
155
|
|
|
82
156
|
/**
|
|
83
157
|
* An error that occurs when a connection cannot be established due to a network
|
|
84
|
-
* error.
|
|
158
|
+
* error. Fires before `onclose`.
|
|
85
159
|
*/
|
|
86
|
-
export interface WebSocketConnectError {
|
|
87
|
-
readonly type: "WebSocketConnectError";
|
|
160
|
+
export interface WebSocketConnectError extends Typed<"WebSocketConnectError"> {
|
|
88
161
|
readonly event: Event;
|
|
89
162
|
}
|
|
90
163
|
|
|
91
164
|
/**
|
|
92
|
-
* An error that occurs when
|
|
93
|
-
* failure to send
|
|
165
|
+
* An error that occurs when an established connection encounters an issue
|
|
166
|
+
* (e.g., failure to send data). Fires before `onclose`.
|
|
167
|
+
*
|
|
168
|
+
* Note: Only Node.js and WebKit fire this error on abrupt server termination.
|
|
169
|
+
* Chromium and Firefox only fire `onclose` without a preceding error event.
|
|
94
170
|
*
|
|
95
171
|
* https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event
|
|
96
172
|
*/
|
|
97
|
-
export interface WebSocketConnectionError {
|
|
98
|
-
readonly type: "WebSocketConnectionError";
|
|
173
|
+
export interface WebSocketConnectionError extends Typed<"WebSocketConnectionError"> {
|
|
99
174
|
readonly event: Event;
|
|
100
175
|
}
|
|
101
176
|
|
|
@@ -103,172 +178,334 @@ export type WebSocketRetryError =
|
|
|
103
178
|
| WebSocketConnectError
|
|
104
179
|
| WebSocketConnectionCloseError;
|
|
105
180
|
|
|
106
|
-
|
|
107
|
-
|
|
181
|
+
/** An error that occurs when the connection is closed by the server. */
|
|
182
|
+
export interface WebSocketConnectionCloseError extends Typed<"WebSocketConnectionCloseError"> {
|
|
108
183
|
readonly event: CloseEvent;
|
|
109
184
|
}
|
|
110
185
|
|
|
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
|
-
binaryType,
|
|
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();
|
|
186
|
+
/** Create a new {@link WebSocket}. */
|
|
187
|
+
export const createWebSocket: CreateWebSocket =
|
|
188
|
+
(
|
|
189
|
+
url,
|
|
190
|
+
{
|
|
191
|
+
protocols,
|
|
192
|
+
binaryType,
|
|
193
|
+
onOpen,
|
|
194
|
+
onClose,
|
|
195
|
+
shouldRetryOnClose = lazyTrue,
|
|
196
|
+
onMessage,
|
|
197
|
+
onError,
|
|
198
|
+
schedule = jitter(1)(maxDelay("30s")(exponential("100ms"))),
|
|
199
|
+
WebSocketConstructor = globalThis.WebSocket,
|
|
200
|
+
} = {},
|
|
201
|
+
) =>
|
|
202
|
+
async (run) => {
|
|
203
|
+
await using stack = new AsyncDisposableStack();
|
|
204
|
+
|
|
205
|
+
let socket: globalThis.WebSocket | null = null;
|
|
206
|
+
|
|
207
|
+
const closeSocket = () => {
|
|
208
|
+
if (!socket) return;
|
|
209
|
+
|
|
210
|
+
socket.onopen = null;
|
|
211
|
+
socket.onclose = null;
|
|
212
|
+
socket.onmessage = null;
|
|
213
|
+
socket.onerror = null;
|
|
214
|
+
|
|
215
|
+
if (
|
|
216
|
+
socket.readyState !== socket.CLOSING &&
|
|
217
|
+
socket.readyState !== socket.CLOSED
|
|
218
|
+
) {
|
|
219
|
+
socket.close();
|
|
220
|
+
}
|
|
158
221
|
|
|
159
|
-
|
|
160
|
-
|
|
222
|
+
socket = null;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A task that connects and stays connected until the connection closes or
|
|
227
|
+
* errors. Returns error to trigger retry.
|
|
228
|
+
*/
|
|
229
|
+
const connect: Task<void, WebSocketRetryError> = callback(({ err, ok }) => {
|
|
230
|
+
closeSocket();
|
|
231
|
+
|
|
232
|
+
socket = new WebSocketConstructor(
|
|
233
|
+
url,
|
|
234
|
+
String.is(protocols) ? protocols : protocols && [...protocols],
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
if (binaryType) socket.binaryType = binaryType;
|
|
238
|
+
|
|
239
|
+
let isOpen = false;
|
|
240
|
+
|
|
241
|
+
socket.onopen = () => {
|
|
242
|
+
isOpen = true;
|
|
243
|
+
onOpen?.();
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
socket.onclose = (event) => {
|
|
247
|
+
onClose?.(event);
|
|
248
|
+
if (shouldRetryOnClose(event)) {
|
|
249
|
+
err({ type: "WebSocketConnectionCloseError", event });
|
|
250
|
+
} else {
|
|
251
|
+
ok();
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
socket.onmessage = (event: MessageEvent<string | ArrayBuffer | Blob>) => {
|
|
256
|
+
onMessage?.(event.data);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
socket.onerror = (event) => {
|
|
260
|
+
const error: WebSocketConnectionError | WebSocketConnectError = isOpen
|
|
261
|
+
? { type: "WebSocketConnectionError", event }
|
|
262
|
+
: { type: "WebSocketConnectError", event };
|
|
263
|
+
onError?.(error);
|
|
264
|
+
|
|
265
|
+
if (error.type === "WebSocketConnectError") err(error);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
return closeSocket;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const retryFiber = stack.use(run.daemon(retry(connect, schedule)));
|
|
272
|
+
|
|
273
|
+
// Report RetryError (schedule exhausted) via onError callback
|
|
274
|
+
void retryFiber.then((result) => {
|
|
275
|
+
if (!result.ok && result.error.type === "RetryError") {
|
|
276
|
+
onError?.(result.error);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const moved = stack.move();
|
|
281
|
+
|
|
282
|
+
return ok<WebSocket>({
|
|
283
|
+
send: (data) => {
|
|
284
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
|
|
285
|
+
if (!socket || socket.readyState === socket.CONNECTING) {
|
|
286
|
+
return err({ type: "WebSocketSendError" });
|
|
287
|
+
}
|
|
288
|
+
socket.send(ensureSendableData(data));
|
|
289
|
+
return ok();
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
getReadyState: () => {
|
|
293
|
+
if (moved.disposed) return "closed";
|
|
294
|
+
return socket ? nativeToStringState[socket.readyState] : "connecting";
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
isOpen: () =>
|
|
298
|
+
!moved.disposed && socket?.readyState === globalThis.WebSocket.OPEN,
|
|
299
|
+
|
|
300
|
+
[Symbol.asyncDispose]: () => moved.disposeAsync(),
|
|
301
|
+
});
|
|
161
302
|
};
|
|
162
303
|
|
|
163
|
-
|
|
304
|
+
/** Clones SharedArrayBuffer-backed Uint8Array values before WebSocket.send. */
|
|
305
|
+
const ensureSendableData = (
|
|
306
|
+
data: BufferSource | Blob | string | globalThis.Uint8Array,
|
|
307
|
+
): BufferSource | Blob | string => {
|
|
308
|
+
if (!Uint8Array.is(data)) return data;
|
|
309
|
+
return ArrayBuffer.is(data.buffer)
|
|
310
|
+
? (data as globalThis.Uint8Array<ArrayBuffer>)
|
|
311
|
+
: new globalThis.Uint8Array(data);
|
|
312
|
+
};
|
|
164
313
|
|
|
165
|
-
|
|
166
|
-
|
|
314
|
+
const nativeToStringState: Record<number, WebSocketReadyState> = {
|
|
315
|
+
[globalThis.WebSocket.CONNECTING]: "connecting",
|
|
316
|
+
[globalThis.WebSocket.OPEN]: "open",
|
|
317
|
+
[globalThis.WebSocket.CLOSING]: "closing",
|
|
318
|
+
[globalThis.WebSocket.CLOSED]: "closed",
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* An inspectable in-memory {@link CreateWebSocket} for testing by
|
|
323
|
+
* {@link testCreateWebSocket}.
|
|
324
|
+
*/
|
|
325
|
+
export interface TestCreateWebSocket extends CreateWebSocket {
|
|
326
|
+
readonly createdUrls: Array<string>;
|
|
327
|
+
readonly sentMessages: Array<{
|
|
328
|
+
readonly url: string;
|
|
329
|
+
readonly data: BufferSource | Blob | string | globalThis.Uint8Array;
|
|
330
|
+
}>;
|
|
331
|
+
readonly message: (url: string, data: string | ArrayBuffer | Blob) => void;
|
|
332
|
+
readonly open: (url: string) => void;
|
|
333
|
+
}
|
|
167
334
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
socket.
|
|
172
|
-
|
|
335
|
+
/** Creates {@link TestCreateWebSocket}. */
|
|
336
|
+
export const testCreateWebSocket = (
|
|
337
|
+
options: {
|
|
338
|
+
/** Throw immediately when a socket is created. */
|
|
339
|
+
readonly throwOnCreate?: boolean;
|
|
173
340
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
341
|
+
/** Initial open state of created sockets. Defaults to true. */
|
|
342
|
+
readonly isOpen?: boolean;
|
|
343
|
+
} = {},
|
|
344
|
+
): TestCreateWebSocket => {
|
|
345
|
+
const createdUrls: Array<string> = [];
|
|
346
|
+
const sentMessages: Array<{
|
|
347
|
+
readonly url: string;
|
|
348
|
+
readonly data: BufferSource | Blob | string | globalThis.Uint8Array;
|
|
349
|
+
}> = [];
|
|
350
|
+
const stateByUrl = new Map<
|
|
351
|
+
string,
|
|
352
|
+
{
|
|
353
|
+
options: WebSocketOptions | undefined;
|
|
354
|
+
isOpen: boolean;
|
|
355
|
+
isDisposed: boolean;
|
|
179
356
|
}
|
|
180
|
-
|
|
181
|
-
};
|
|
357
|
+
>();
|
|
182
358
|
|
|
183
|
-
|
|
184
|
-
|
|
359
|
+
const getState = (url: string) => {
|
|
360
|
+
const state = stateByUrl.get(url);
|
|
361
|
+
assert(state, `Test WebSocket for ${url} does not exist.`);
|
|
362
|
+
return state;
|
|
363
|
+
};
|
|
185
364
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
* - Is rejected when a connection is closed.
|
|
191
|
-
* - Is resolved when WebSocket is disposed().
|
|
192
|
-
*/
|
|
193
|
-
void retry(
|
|
194
|
-
{
|
|
195
|
-
...defaultRetryOptions,
|
|
196
|
-
...retryOptions,
|
|
197
|
-
},
|
|
198
|
-
(): Promise<Result<void, WebSocketRetryError>> =>
|
|
199
|
-
new Promise((resolve) => {
|
|
200
|
-
disposePromise = () => {
|
|
201
|
-
resolve(ok());
|
|
202
|
-
};
|
|
365
|
+
const createWebSocket: CreateWebSocket = (url, socketOptions) => () => {
|
|
366
|
+
if (options.throwOnCreate) {
|
|
367
|
+
throw new Error("testCreateWebSocket is configured to throw on create");
|
|
368
|
+
}
|
|
203
369
|
|
|
204
|
-
|
|
370
|
+
createdUrls.push(url);
|
|
371
|
+
stateByUrl.set(url, {
|
|
372
|
+
options: socketOptions,
|
|
373
|
+
isOpen: options.isOpen ?? true,
|
|
374
|
+
isDisposed: false,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
return ok({
|
|
378
|
+
send: (data) => {
|
|
379
|
+
const state = getState(url);
|
|
380
|
+
if (state.isDisposed || !state.isOpen) {
|
|
381
|
+
return err({ type: "WebSocketSendError" });
|
|
382
|
+
}
|
|
383
|
+
sentMessages.push({
|
|
384
|
+
url,
|
|
385
|
+
data: ensureSendableData(data),
|
|
386
|
+
});
|
|
387
|
+
return ok();
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
getReadyState: () => {
|
|
391
|
+
const state = getState(url);
|
|
392
|
+
if (state.isDisposed) return "closed";
|
|
393
|
+
return state.isOpen ? "open" : "closed";
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
isOpen: () => {
|
|
397
|
+
const state = getState(url);
|
|
398
|
+
return !state.isDisposed && state.isOpen;
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
[Symbol.asyncDispose]: () => {
|
|
402
|
+
const state = getState(url);
|
|
403
|
+
state.isDisposed = true;
|
|
404
|
+
state.isOpen = false;
|
|
405
|
+
return Promise.resolve();
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
};
|
|
205
409
|
|
|
206
|
-
|
|
410
|
+
return Object.assign(createWebSocket, {
|
|
411
|
+
createdUrls,
|
|
412
|
+
sentMessages,
|
|
413
|
+
message: (url: string, data: string | ArrayBuffer | Blob) => {
|
|
414
|
+
getState(url).options?.onMessage?.(data);
|
|
415
|
+
},
|
|
416
|
+
open: (url: string) => {
|
|
417
|
+
const state = getState(url);
|
|
418
|
+
state.isOpen = true;
|
|
419
|
+
state.options?.onOpen?.();
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
};
|
|
207
423
|
|
|
208
|
-
|
|
209
|
-
|
|
424
|
+
/**
|
|
425
|
+
* A native {@link WebSocket} prepared for integration tests by
|
|
426
|
+
* {@link testSetupWebSocket}.
|
|
427
|
+
*/
|
|
428
|
+
export interface TestSetupWebSocket extends AsyncDisposable {
|
|
429
|
+
readonly socket: globalThis.WebSocket;
|
|
430
|
+
readonly send: (
|
|
431
|
+
data: BufferSource | Blob | string | globalThis.Uint8Array,
|
|
432
|
+
) => void;
|
|
433
|
+
readonly waitForMessage: () => Promise<string | globalThis.Uint8Array>;
|
|
434
|
+
}
|
|
210
435
|
|
|
211
|
-
|
|
436
|
+
/** Opens a native {@link WebSocket} and returns {@link TestSetupWebSocket}. */
|
|
437
|
+
export const testSetupWebSocket = async (
|
|
438
|
+
url: string,
|
|
439
|
+
): Promise<TestSetupWebSocket> => {
|
|
440
|
+
const socket = new globalThis.WebSocket(url);
|
|
441
|
+
socket.binaryType = "arraybuffer";
|
|
442
|
+
|
|
443
|
+
await new Promise<void>((resolve, reject) => {
|
|
444
|
+
const onOpen = () => {
|
|
445
|
+
cleanup();
|
|
446
|
+
resolve();
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const onError = () => {
|
|
450
|
+
cleanup();
|
|
451
|
+
socket.close();
|
|
452
|
+
reject(new Error("WebSocket connection failed"));
|
|
453
|
+
};
|
|
212
454
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
455
|
+
const cleanup = () => {
|
|
456
|
+
socket.removeEventListener("open", onOpen);
|
|
457
|
+
socket.removeEventListener("error", onError);
|
|
458
|
+
};
|
|
217
459
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
: { type: "WebSocketConnectError", event };
|
|
222
|
-
onError?.(error);
|
|
460
|
+
socket.addEventListener("open", onOpen, { once: true });
|
|
461
|
+
socket.addEventListener("error", onError, { once: true });
|
|
462
|
+
});
|
|
223
463
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
464
|
+
return {
|
|
465
|
+
socket,
|
|
466
|
+
send: (data) => {
|
|
467
|
+
socket.send(ensureSendableData(data));
|
|
468
|
+
},
|
|
469
|
+
waitForMessage: () =>
|
|
470
|
+
new Promise((resolve, reject) => {
|
|
471
|
+
if (socket.readyState === globalThis.WebSocket.CLOSED) {
|
|
472
|
+
reject(new Error("WebSocket closed before message"));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const onMessage = (event: MessageEvent) => {
|
|
477
|
+
cleanup();
|
|
478
|
+
|
|
479
|
+
if (typeof event.data === "string") {
|
|
480
|
+
resolve(event.data);
|
|
481
|
+
return;
|
|
227
482
|
}
|
|
228
|
-
};
|
|
229
483
|
|
|
230
|
-
|
|
231
|
-
onClose?.(event);
|
|
232
|
-
resolve(err({ type: "WebSocketConnectionCloseError", event }));
|
|
484
|
+
resolve(new globalThis.Uint8Array(event.data as ArrayBuffer));
|
|
233
485
|
};
|
|
234
486
|
|
|
235
|
-
|
|
236
|
-
|
|
487
|
+
const onClose = () => {
|
|
488
|
+
cleanup();
|
|
489
|
+
reject(new Error("WebSocket closed before message"));
|
|
237
490
|
};
|
|
238
|
-
}),
|
|
239
|
-
)(reconnectController).then((result) => {
|
|
240
|
-
if (result.ok || result.error.type === "AbortError") return;
|
|
241
|
-
onError?.(result.error as WebSocketError);
|
|
242
|
-
});
|
|
243
491
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return err({ type: "WebSocketSendError" });
|
|
249
|
-
}
|
|
250
|
-
socket.send(data);
|
|
251
|
-
return ok();
|
|
252
|
-
},
|
|
492
|
+
const cleanup = () => {
|
|
493
|
+
socket.removeEventListener("message", onMessage);
|
|
494
|
+
socket.removeEventListener("close", onClose);
|
|
495
|
+
};
|
|
253
496
|
|
|
254
|
-
|
|
255
|
-
|
|
497
|
+
socket.addEventListener("message", onMessage, { once: true });
|
|
498
|
+
socket.addEventListener("close", onClose, { once: true });
|
|
499
|
+
}),
|
|
500
|
+
[Symbol.asyncDispose]: async () => {
|
|
501
|
+
if (socket.readyState === globalThis.WebSocket.CLOSED) return;
|
|
256
502
|
|
|
257
|
-
|
|
503
|
+
const closed = new Promise<void>((resolve) => {
|
|
504
|
+
socket.addEventListener("close", () => resolve(), { once: true });
|
|
505
|
+
});
|
|
258
506
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
isDisposed = true;
|
|
262
|
-
reconnectController.abort();
|
|
263
|
-
disposeSocket();
|
|
264
|
-
disposePromise?.();
|
|
507
|
+
socket.close();
|
|
508
|
+
await closed;
|
|
265
509
|
},
|
|
266
510
|
};
|
|
267
511
|
};
|
|
268
|
-
|
|
269
|
-
const nativeToStringState: Record<number, WebSocketReadyState> = {
|
|
270
|
-
[WebSocket.CONNECTING]: "connecting",
|
|
271
|
-
[WebSocket.OPEN]: "open",
|
|
272
|
-
[WebSocket.CLOSING]: "closing",
|
|
273
|
-
[WebSocket.CLOSED]: "closed",
|
|
274
|
-
};
|