@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.
Files changed (251) hide show
  1. package/README.md +4 -61
  2. package/dist/src/Array.d.ts +343 -102
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +181 -85
  5. package/dist/src/Assert.d.ts +57 -11
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +47 -11
  8. package/dist/src/BigInt.d.ts +6 -1
  9. package/dist/src/BigInt.d.ts.map +1 -1
  10. package/dist/src/BigInt.js +5 -0
  11. package/dist/src/Brand.d.ts +40 -12
  12. package/dist/src/Brand.d.ts.map +1 -1
  13. package/dist/src/Brand.js +5 -0
  14. package/dist/src/Buffer.d.ts +6 -1
  15. package/dist/src/Buffer.d.ts.map +1 -1
  16. package/dist/src/Buffer.js +7 -2
  17. package/dist/src/Cache.d.ts +10 -2
  18. package/dist/src/Cache.d.ts.map +1 -1
  19. package/dist/src/Cache.js +6 -0
  20. package/dist/src/Callbacks.d.ts +10 -7
  21. package/dist/src/Callbacks.d.ts.map +1 -1
  22. package/dist/src/Callbacks.js +23 -13
  23. package/dist/src/Console.d.ts +361 -69
  24. package/dist/src/Console.d.ts.map +1 -1
  25. package/dist/src/Console.js +217 -117
  26. package/dist/src/Crypto.d.ts +70 -21
  27. package/dist/src/Crypto.d.ts.map +1 -1
  28. package/dist/src/Crypto.js +72 -27
  29. package/dist/src/Eq.d.ts +22 -2
  30. package/dist/src/Eq.d.ts.map +1 -1
  31. package/dist/src/Eq.js +21 -1
  32. package/dist/src/Error.d.ts +23 -12
  33. package/dist/src/Error.d.ts.map +1 -1
  34. package/dist/src/Error.js +27 -11
  35. package/dist/src/Function.d.ts +128 -56
  36. package/dist/src/Function.d.ts.map +1 -1
  37. package/dist/src/Function.js +118 -14
  38. package/dist/src/Identicon.d.ts +5 -0
  39. package/dist/src/Identicon.d.ts.map +1 -1
  40. package/dist/src/Identicon.js +6 -1
  41. package/dist/src/Lookup.d.ts +160 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +192 -0
  44. package/dist/src/Microtask.d.ts +21 -0
  45. package/dist/src/Microtask.d.ts.map +1 -0
  46. package/dist/src/Microtask.js +37 -0
  47. package/dist/src/Number.d.ts +18 -4
  48. package/dist/src/Number.d.ts.map +1 -1
  49. package/dist/src/Number.js +23 -1
  50. package/dist/src/Object.d.ts +110 -5
  51. package/dist/src/Object.d.ts.map +1 -1
  52. package/dist/src/Object.js +108 -5
  53. package/dist/src/Option.d.ts +58 -0
  54. package/dist/src/Option.d.ts.map +1 -0
  55. package/dist/src/Option.js +43 -0
  56. package/dist/src/Order.d.ts +5 -0
  57. package/dist/src/Order.d.ts.map +1 -1
  58. package/dist/src/Order.js +8 -3
  59. package/dist/src/Platform.d.ts +36 -2
  60. package/dist/src/Platform.d.ts.map +1 -1
  61. package/dist/src/Platform.js +10 -6
  62. package/dist/src/Polyfills.d.ts +27 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +299 -0
  65. package/dist/src/Random.d.ts +18 -7
  66. package/dist/src/Random.d.ts.map +1 -1
  67. package/dist/src/Random.js +9 -7
  68. package/dist/src/Redacted.d.ts +7 -4
  69. package/dist/src/Redacted.d.ts.map +1 -1
  70. package/dist/src/Redacted.js +5 -0
  71. package/dist/src/Ref.d.ts +31 -16
  72. package/dist/src/Ref.d.ts.map +1 -1
  73. package/dist/src/Ref.js +35 -7
  74. package/dist/src/RefCount.d.ts +62 -0
  75. package/dist/src/RefCount.d.ts.map +1 -0
  76. package/dist/src/RefCount.js +83 -0
  77. package/dist/src/Relation.d.ts +84 -29
  78. package/dist/src/Relation.d.ts.map +1 -1
  79. package/dist/src/Relation.js +83 -105
  80. package/dist/src/Resource.d.ts +263 -0
  81. package/dist/src/Resource.d.ts.map +1 -0
  82. package/dist/src/Resource.js +389 -0
  83. package/dist/src/Result.d.ts +390 -374
  84. package/dist/src/Result.d.ts.map +1 -1
  85. package/dist/src/Result.js +142 -70
  86. package/dist/src/Schedule.d.ts +953 -0
  87. package/dist/src/Schedule.d.ts.map +1 -0
  88. package/dist/src/Schedule.js +1199 -0
  89. package/dist/src/Set.d.ts +181 -0
  90. package/dist/src/Set.d.ts.map +1 -0
  91. package/dist/src/Set.js +137 -0
  92. package/dist/src/Skiplist.d.ts +6 -1
  93. package/dist/src/Skiplist.d.ts.map +1 -1
  94. package/dist/src/Skiplist.js +5 -0
  95. package/dist/src/Sqlite.d.ts +160 -60
  96. package/dist/src/Sqlite.d.ts.map +1 -1
  97. package/dist/src/Sqlite.js +290 -190
  98. package/dist/src/Store.d.ts +36 -28
  99. package/dist/src/Store.d.ts.map +1 -1
  100. package/dist/src/Store.js +55 -15
  101. package/dist/src/String.d.ts +5 -0
  102. package/dist/src/String.d.ts.map +1 -1
  103. package/dist/src/String.js +5 -0
  104. package/dist/src/Task.d.ts +2263 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1488 -348
  107. package/dist/src/Test.d.ts +122 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +66 -0
  110. package/dist/src/Time.d.ts +201 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +190 -86
  113. package/dist/src/Tracer.d.ts +48 -0
  114. package/dist/src/Tracer.d.ts.map +1 -0
  115. package/dist/src/Tracer.js +6 -0
  116. package/dist/src/Type.d.ts +582 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +362 -240
  119. package/dist/src/Types.d.ts +160 -6
  120. package/dist/src/Types.d.ts.map +1 -1
  121. package/dist/src/Types.js +22 -2
  122. package/dist/src/WebSocket.d.ts +113 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +210 -118
  125. package/dist/src/Worker.d.ts +245 -60
  126. package/dist/src/Worker.d.ts.map +1 -1
  127. package/dist/src/Worker.js +220 -64
  128. package/dist/src/index.d.ts +25 -3
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +19 -3
  131. package/dist/src/local-first/Db.d.ts +24 -213
  132. package/dist/src/local-first/Db.d.ts.map +1 -1
  133. package/dist/src/local-first/Db.js +622 -293
  134. package/dist/src/local-first/Error.d.ts +12 -0
  135. package/dist/src/local-first/Error.d.ts.map +1 -0
  136. package/dist/src/local-first/Error.js +6 -0
  137. package/dist/src/local-first/Evolu.d.ts +330 -285
  138. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  139. package/dist/src/local-first/Evolu.js +472 -402
  140. package/dist/src/local-first/LocalAuth.d.ts +10 -10
  141. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  142. package/dist/src/local-first/LocalAuth.js +6 -1
  143. package/dist/src/local-first/Owner.d.ts +70 -27
  144. package/dist/src/local-first/Owner.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.js +27 -14
  146. package/dist/src/local-first/Protocol.d.ts +58 -50
  147. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  148. package/dist/src/local-first/Protocol.js +253 -155
  149. package/dist/src/local-first/Query.d.ts +133 -43
  150. package/dist/src/local-first/Query.d.ts.map +1 -1
  151. package/dist/src/local-first/Query.js +139 -90
  152. package/dist/src/local-first/Relay.d.ts +18 -38
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -171
  155. package/dist/src/local-first/Schema.d.ts +139 -144
  156. package/dist/src/local-first/Schema.d.ts.map +1 -1
  157. package/dist/src/local-first/Schema.js +78 -163
  158. package/dist/src/local-first/Shared.d.ts +125 -0
  159. package/dist/src/local-first/Shared.d.ts.map +1 -0
  160. package/dist/src/local-first/Shared.js +569 -0
  161. package/dist/src/local-first/Storage.d.ts +50 -53
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +121 -173
  164. package/dist/src/local-first/Timestamp.d.ts +28 -35
  165. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  166. package/dist/src/local-first/Timestamp.js +24 -25
  167. package/dist/src/local-first/index.d.ts +2 -15
  168. package/dist/src/local-first/index.d.ts.map +1 -1
  169. package/dist/src/local-first/index.js +2 -15
  170. package/package.json +36 -26
  171. package/src/Array.ts +467 -106
  172. package/src/Assert.ts +87 -12
  173. package/src/BigInt.ts +7 -1
  174. package/src/Brand.ts +41 -12
  175. package/src/Buffer.ts +10 -4
  176. package/src/Cache.ts +11 -2
  177. package/src/Callbacks.ts +33 -23
  178. package/src/Console.ts +567 -178
  179. package/src/Crypto.ts +111 -62
  180. package/src/Eq.ts +30 -3
  181. package/src/Error.ts +50 -34
  182. package/src/Function.ts +140 -68
  183. package/src/Identicon.ts +7 -1
  184. package/src/Lookup.ts +415 -0
  185. package/src/Microtask.ts +58 -0
  186. package/src/Number.ts +40 -7
  187. package/src/Object.ts +155 -16
  188. package/src/Option.ts +74 -0
  189. package/src/Order.ts +11 -3
  190. package/src/Platform.ts +43 -7
  191. package/src/Polyfills.ts +465 -0
  192. package/src/Random.ts +25 -11
  193. package/src/Redacted.ts +8 -4
  194. package/src/Ref.ts +72 -21
  195. package/src/RefCount.ts +170 -0
  196. package/src/Relation.ts +188 -136
  197. package/src/Resource.ts +864 -0
  198. package/src/Result.ts +548 -381
  199. package/src/Schedule.ts +1471 -0
  200. package/src/Set.ts +247 -0
  201. package/src/Skiplist.ts +7 -1
  202. package/src/Sqlite.ts +409 -304
  203. package/src/Store.ts +94 -48
  204. package/src/String.ts +6 -0
  205. package/src/Task.ts +3685 -740
  206. package/src/Test.ts +162 -0
  207. package/src/Time.ts +348 -145
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +851 -430
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +311 -178
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +871 -597
  215. package/src/local-first/Error.ts +17 -0
  216. package/src/local-first/Evolu.ts +884 -824
  217. package/src/local-first/LocalAuth.ts +10 -10
  218. package/src/local-first/Owner.ts +94 -42
  219. package/src/local-first/Protocol.ts +440 -303
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +91 -252
  222. package/src/local-first/Schema.ts +284 -394
  223. package/src/local-first/Shared.ts +932 -0
  224. package/src/local-first/Storage.ts +206 -277
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/LICENSE +0 -21
  228. package/dist/src/Instances.d.ts +0 -34
  229. package/dist/src/Instances.d.ts.map +0 -1
  230. package/dist/src/Instances.js +0 -44
  231. package/dist/src/Resources.d.ts +0 -118
  232. package/dist/src/Resources.d.ts.map +0 -1
  233. package/dist/src/Resources.js +0 -197
  234. package/dist/src/local-first/Platform.d.ts +0 -25
  235. package/dist/src/local-first/Platform.d.ts.map +0 -1
  236. package/dist/src/local-first/Platform.js +0 -1
  237. package/dist/src/local-first/Public.d.ts +0 -22
  238. package/dist/src/local-first/Public.d.ts.map +0 -1
  239. package/dist/src/local-first/Public.js +0 -15
  240. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  241. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  242. package/dist/src/local-first/PublicKysely.js +0 -184
  243. package/dist/src/local-first/Sync.d.ts +0 -112
  244. package/dist/src/local-first/Sync.d.ts.map +0 -1
  245. package/dist/src/local-first/Sync.js +0 -526
  246. package/src/Instances.ts +0 -92
  247. package/src/Resources.ts +0 -367
  248. package/src/local-first/Platform.ts +0 -27
  249. package/src/local-first/Public.ts +0 -42
  250. package/src/local-first/PublicKysely.ts +0 -239
  251. package/src/local-first/Sync.ts +0 -949
package/src/WebSocket.ts CHANGED
@@ -1,10 +1,69 @@
1
- import { constVoid } from "./Function.js";
2
- import { err, ok, Result } from "./Result.js";
3
- import { retry, RetryError, RetryOptions } from "./Task.js";
4
- import { maxPositiveInt } from "./Type.js";
1
+ /**
2
+ * WebSocket with auto-reconnect.
3
+ *
4
+ * @module
5
+ */
5
6
 
6
- /** WebSocket with auto-reconnect and offline support. */
7
- export interface WebSocket extends Disposable {
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 | Array<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
- /** Options for retry behavior. */
65
- retryOptions?: Omit<RetryOptions<WebSocketRetryError>, "signal">;
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 suppors blob:
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 a connection is closed due to an issue (e.g.,
93
- * failure to send some data).
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
- export interface WebSocketConnectionCloseError {
107
- readonly type: "WebSocketConnectionCloseError";
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
- * Create a new {@link WebSocket}.
113
- *
114
- * The default behavior is that WebSocket tries to reconnect repeatedly in case
115
- * the application is offline, because online events (both web and native) are
116
- * not reliable. Once it connects and the connection is closed, it tries to
117
- * reconnect again. Retrying the connection can be controlled using the
118
- * retryOptions retryable predicate.
119
- *
120
- * ### How Binary Messages Work in WebSockets
121
- *
122
- * The Server Chooses the Message Type:
123
- *
124
- * - Text (0x1) → Sent as UTF-8 encoded text (always received as a string in the
125
- * browser).
126
- * - Binary (0x2) → Sent as raw binary data (received as a Blob or ArrayBuffer,
127
- * depending on binaryType).
128
- *
129
- * The Client's binaryType Controls How Binary Data is Processed:
130
- *
131
- * - If the server sends a text frame (0x1), the browser always delivers
132
- * event.data as a string, regardless of binaryType.
133
- * - If the server sends a binary frame (0x2), the browser delivers event.data as:
134
- *
135
- * - A Blob (default: "blob")
136
- * - An ArrayBuffer ("arraybuffer")
137
- *
138
- * ### Example
139
- *
140
- * TODO:
141
- */
142
- export const createWebSocket: CreateWebSocket = (
143
- url,
144
- {
145
- protocols,
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();
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
- const defaultRetryOptions: RetryOptions<WebSocketRetryError> = {
160
- retries: maxPositiveInt, // Practically infinite retries
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
- let socket: globalThis.WebSocket | null = null;
164
-
165
- const disposeSocket = () => {
166
- if (!socket) return;
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
- // Remove all listeners before closing
169
- socket.onopen = null;
170
- socket.onclose = null;
171
- socket.onmessage = null;
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
- if (
175
- socket.readyState !== socket.CLOSING &&
176
- socket.readyState !== socket.CLOSED
177
- ) {
178
- socket.close();
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
- socket = null;
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
- // To prevent a memory leak from pending connection promise.
184
- let disposePromise: null | typeof constVoid = null;
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
- * This promise represents continuous connection which:
188
- *
189
- * - Is rejected when a connection cannot be established.
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
- };
203
-
204
- if (isDisposed) disposePromise();
205
-
206
- disposeSocket();
207
-
208
- socket = new WebSocketConstructor(url, protocols);
209
- if (binaryType) socket.binaryType = binaryType;
210
-
211
- let isOpen = false;
212
-
213
- socket.onopen = () => {
214
- isOpen = true;
215
- onOpen?.();
216
- };
217
-
218
- socket.onerror = (event) => {
219
- const error: WebSocketConnectionError | WebSocketConnectError = isOpen
220
- ? { type: "WebSocketConnectionError", event }
221
- : { type: "WebSocketConnectError", event };
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
- send: (data) => {
246
- // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
247
- if (!socket || socket.readyState === socket.CONNECTING) {
248
- return err({ type: "WebSocketSendError" });
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
- getReadyState: () =>
255
- socket ? nativeToStringState[socket.readyState] : "connecting",
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
  };