@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.
Files changed (250) 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 +162 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +200 -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 +29 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +329 -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 +2274 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1499 -348
  107. package/dist/src/Test.d.ts +150 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +97 -0
  110. package/dist/src/Time.d.ts +202 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +199 -88
  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 +587 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +370 -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 +130 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +274 -113
  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 +621 -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 +34 -49
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -177
  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 +54 -58
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +127 -179
  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 +37 -24
  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 +429 -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 +520 -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 +3696 -740
  206. package/src/Test.ts +202 -0
  207. package/src/Time.ts +357 -147
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +866 -430
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +404 -167
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +867 -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 +441 -307
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +119 -273
  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 +211 -283
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/dist/src/Instances.d.ts +0 -34
  228. package/dist/src/Instances.d.ts.map +0 -1
  229. package/dist/src/Instances.js +0 -44
  230. package/dist/src/Resources.d.ts +0 -118
  231. package/dist/src/Resources.d.ts.map +0 -1
  232. package/dist/src/Resources.js +0 -197
  233. package/dist/src/local-first/Platform.d.ts +0 -25
  234. package/dist/src/local-first/Platform.d.ts.map +0 -1
  235. package/dist/src/local-first/Platform.js +0 -1
  236. package/dist/src/local-first/Public.d.ts +0 -22
  237. package/dist/src/local-first/Public.d.ts.map +0 -1
  238. package/dist/src/local-first/Public.js +0 -15
  239. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  240. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  241. package/dist/src/local-first/PublicKysely.js +0 -184
  242. package/dist/src/local-first/Sync.d.ts +0 -112
  243. package/dist/src/local-first/Sync.d.ts.map +0 -1
  244. package/dist/src/local-first/Sync.js +0 -526
  245. package/src/Instances.ts +0 -92
  246. package/src/Resources.ts +0 -367
  247. package/src/local-first/Platform.ts +0 -27
  248. package/src/local-first/Public.ts +0 -42
  249. package/src/local-first/PublicKysely.ts +0 -239
  250. package/src/local-first/Sync.ts +0 -949
@@ -1,131 +1,292 @@
1
- import { err, ok } from "./Result.js";
2
- import { retry } from "./Task.js";
3
- import { maxPositiveInt } from "./Type.js";
4
1
  /**
5
- * Create a new {@link WebSocket}.
6
- *
7
- * The default behavior is that WebSocket tries to reconnect repeatedly in case
8
- * the application is offline, because online events (both web and native) are
9
- * not reliable. Once it connects and the connection is closed, it tries to
10
- * reconnect again. Retrying the connection can be controlled using the
11
- * retryOptions retryable predicate.
12
- *
13
- * ### How Binary Messages Work in WebSockets
14
- *
15
- * The Server Chooses the Message Type:
16
- *
17
- * - Text (0x1) → Sent as UTF-8 encoded text (always received as a string in the
18
- * browser).
19
- * - Binary (0x2) → Sent as raw binary data (received as a Blob or ArrayBuffer,
20
- * depending on binaryType).
2
+ * WebSocket with auto-reconnect.
21
3
  *
22
- * The Client's binaryType Controls How Binary Data is Processed:
23
- *
24
- * - If the server sends a text frame (0x1), the browser always delivers
25
- * event.data as a string, regardless of binaryType.
26
- * - If the server sends a binary frame (0x2), the browser delivers event.data as:
27
- *
28
- * - A Blob (default: "blob")
29
- * - An ArrayBuffer ("arraybuffer")
30
- *
31
- * ### Example
32
- *
33
- * TODO:
4
+ * @module
34
5
  */
35
- export const createWebSocket = (url, { protocols, binaryType, onOpen, onClose, onMessage, onError, retryOptions, WebSocketConstructor = globalThis.WebSocket, } = {}) => {
36
- let isDisposed = false;
37
- const reconnectController = new AbortController();
38
- const defaultRetryOptions = {
39
- retries: maxPositiveInt, // Practically infinite retries
40
- };
41
- let socket = null;
42
- const disposeSocket = () => {
43
- if (!socket)
44
- return;
45
- // Remove all listeners before closing
46
- socket.onopen = null;
47
- socket.onclose = null;
48
- socket.onmessage = null;
49
- socket.onerror = null;
50
- if (socket.readyState !== socket.CLOSING &&
51
- socket.readyState !== socket.CLOSED) {
52
- socket.close();
6
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
7
+ if (value !== null && value !== void 0) {
8
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
9
+ var dispose, inner;
10
+ if (async) {
11
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
12
+ dispose = value[Symbol.asyncDispose];
13
+ }
14
+ if (dispose === void 0) {
15
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
16
+ dispose = value[Symbol.dispose];
17
+ if (async) inner = dispose;
53
18
  }
54
- socket = null;
19
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
20
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
21
+ env.stack.push({ value: value, dispose: dispose, async: async });
22
+ }
23
+ else if (async) {
24
+ env.stack.push({ async: true });
25
+ }
26
+ return value;
27
+ };
28
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
29
+ return function (env) {
30
+ function fail(e) {
31
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
32
+ env.hasError = true;
33
+ }
34
+ var r, s = 0;
35
+ function next() {
36
+ while (r = env.stack.pop()) {
37
+ try {
38
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
39
+ if (r.dispose) {
40
+ var result = r.dispose.call(r.value);
41
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
42
+ }
43
+ else s |= 1;
44
+ }
45
+ catch (e) {
46
+ fail(e);
47
+ }
48
+ }
49
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
50
+ if (env.hasError) throw env.error;
51
+ }
52
+ return next();
55
53
  };
56
- // To prevent a memory leak from pending connection promise.
57
- let disposePromise = null;
58
- /**
59
- * This promise represents continuous connection which:
60
- *
61
- * - Is rejected when a connection cannot be established.
62
- * - Is rejected when a connection is closed.
63
- * - Is resolved when WebSocket is disposed().
64
- */
65
- void retry({
66
- ...defaultRetryOptions,
67
- ...retryOptions,
68
- }, () => new Promise((resolve) => {
69
- disposePromise = () => {
70
- resolve(ok());
71
- };
72
- if (isDisposed)
73
- disposePromise();
74
- disposeSocket();
75
- socket = new WebSocketConstructor(url, protocols);
76
- if (binaryType)
77
- socket.binaryType = binaryType;
78
- let isOpen = false;
79
- socket.onopen = () => {
80
- isOpen = true;
81
- onOpen?.();
54
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
55
+ var e = new Error(message);
56
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
57
+ });
58
+ import { assert } from "./Assert.js";
59
+ import { lazyTrue } from "./Function.js";
60
+ import { err, ok } from "./Result.js";
61
+ import { exponential, jitter, maxDelay } from "./Schedule.js";
62
+ import { callback, retry } from "./Task.js";
63
+ import { ArrayBuffer, String, Uint8Array } from "./Type.js";
64
+ /** Create a new {@link WebSocket}. */
65
+ export const createWebSocket = (url, { protocols, binaryType, onOpen, onClose, shouldRetryOnClose = lazyTrue, onMessage, onError, schedule = jitter(1)(maxDelay("30s")(exponential("100ms"))), WebSocketConstructor = globalThis.WebSocket, } = {}) => async (run) => {
66
+ const env_1 = { stack: [], error: void 0, hasError: false };
67
+ try {
68
+ const stack = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
69
+ let socket = null;
70
+ const closeSocket = () => {
71
+ if (!socket)
72
+ return;
73
+ socket.onopen = null;
74
+ socket.onclose = null;
75
+ socket.onmessage = null;
76
+ socket.onerror = null;
77
+ if (socket.readyState !== socket.CLOSING &&
78
+ socket.readyState !== socket.CLOSED) {
79
+ socket.close();
80
+ }
81
+ socket = null;
82
82
  };
83
- socket.onerror = (event) => {
84
- const error = isOpen
85
- ? { type: "WebSocketConnectionError", event }
86
- : { type: "WebSocketConnectError", event };
87
- onError?.(error);
88
- // Trigger reconnect only on WebSocketConnectError.
89
- if (error.type === "WebSocketConnectError") {
90
- resolve(err(error));
83
+ /**
84
+ * A task that connects and stays connected until the connection closes or
85
+ * errors. Returns error to trigger retry.
86
+ */
87
+ const connect = callback(({ err, ok }) => {
88
+ closeSocket();
89
+ socket = new WebSocketConstructor(url, String.is(protocols) ? protocols : protocols && [...protocols]);
90
+ if (binaryType)
91
+ socket.binaryType = binaryType;
92
+ let isOpen = false;
93
+ socket.onopen = () => {
94
+ isOpen = true;
95
+ onOpen?.();
96
+ };
97
+ socket.onclose = (event) => {
98
+ onClose?.(event);
99
+ if (shouldRetryOnClose(event)) {
100
+ err({ type: "WebSocketConnectionCloseError", event });
101
+ }
102
+ else {
103
+ ok();
104
+ }
105
+ };
106
+ socket.onmessage = (event) => {
107
+ onMessage?.(event.data);
108
+ };
109
+ socket.onerror = (event) => {
110
+ const error = isOpen
111
+ ? { type: "WebSocketConnectionError", event }
112
+ : { type: "WebSocketConnectError", event };
113
+ onError?.(error);
114
+ if (error.type === "WebSocketConnectError")
115
+ err(error);
116
+ };
117
+ return closeSocket;
118
+ });
119
+ const retryFiber = stack.use(run.daemon(retry(connect, schedule)));
120
+ // Report RetryError (schedule exhausted) via onError callback
121
+ void retryFiber.then((result) => {
122
+ if (!result.ok && result.error.type === "RetryError") {
123
+ onError?.(result.error);
91
124
  }
125
+ });
126
+ const moved = stack.move();
127
+ return ok({
128
+ send: (data) => {
129
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
130
+ if (!socket || socket.readyState === socket.CONNECTING) {
131
+ return err({ type: "WebSocketSendError" });
132
+ }
133
+ socket.send(ensureSendableData(data));
134
+ return ok();
135
+ },
136
+ getReadyState: () => {
137
+ if (moved.disposed)
138
+ return "closed";
139
+ return socket ? nativeToStringState[socket.readyState] : "connecting";
140
+ },
141
+ isOpen: () => !moved.disposed && socket?.readyState === globalThis.WebSocket.OPEN,
142
+ [Symbol.asyncDispose]: () => moved.disposeAsync(),
143
+ });
144
+ }
145
+ catch (e_1) {
146
+ env_1.error = e_1;
147
+ env_1.hasError = true;
148
+ }
149
+ finally {
150
+ const result_1 = __disposeResources(env_1);
151
+ if (result_1)
152
+ await result_1;
153
+ }
154
+ };
155
+ /** Clones SharedArrayBuffer-backed Uint8Array values before WebSocket.send. */
156
+ const ensureSendableData = (data) => {
157
+ if (!Uint8Array.is(data))
158
+ return data;
159
+ return ArrayBuffer.is(data.buffer)
160
+ ? data
161
+ : new globalThis.Uint8Array(data);
162
+ };
163
+ const nativeToStringState = {
164
+ [globalThis.WebSocket.CONNECTING]: "connecting",
165
+ [globalThis.WebSocket.OPEN]: "open",
166
+ [globalThis.WebSocket.CLOSING]: "closing",
167
+ [globalThis.WebSocket.CLOSED]: "closed",
168
+ };
169
+ /** Creates {@link TestCreateWebSocket}. */
170
+ export const testCreateWebSocket = (options = {}) => {
171
+ const createdUrls = [];
172
+ const sentMessages = [];
173
+ const stateByUrl = new Map();
174
+ const getState = (url) => {
175
+ const state = stateByUrl.get(url);
176
+ assert(state, `Test WebSocket for ${url} does not exist.`);
177
+ return state;
178
+ };
179
+ const createWebSocket = (url, socketOptions) => () => {
180
+ if (options.throwOnCreate) {
181
+ throw new Error("testCreateWebSocket is configured to throw on create");
182
+ }
183
+ createdUrls.push(url);
184
+ stateByUrl.set(url, {
185
+ options: socketOptions,
186
+ isOpen: options.isOpen ?? true,
187
+ isDisposed: false,
188
+ });
189
+ return ok({
190
+ send: (data) => {
191
+ const state = getState(url);
192
+ if (state.isDisposed || !state.isOpen) {
193
+ return err({ type: "WebSocketSendError" });
194
+ }
195
+ sentMessages.push({
196
+ url,
197
+ data: ensureSendableData(data),
198
+ });
199
+ return ok();
200
+ },
201
+ getReadyState: () => {
202
+ const state = getState(url);
203
+ if (state.isDisposed)
204
+ return "closed";
205
+ return state.isOpen ? "open" : "closed";
206
+ },
207
+ isOpen: () => {
208
+ const state = getState(url);
209
+ return !state.isDisposed && state.isOpen;
210
+ },
211
+ [Symbol.asyncDispose]: () => {
212
+ const state = getState(url);
213
+ state.isDisposed = true;
214
+ state.isOpen = false;
215
+ return Promise.resolve();
216
+ },
217
+ });
218
+ };
219
+ return Object.assign(createWebSocket, {
220
+ createdUrls,
221
+ sentMessages,
222
+ message: (url, data) => {
223
+ getState(url).options?.onMessage?.(data);
224
+ },
225
+ open: (url) => {
226
+ const state = getState(url);
227
+ state.isOpen = true;
228
+ state.options?.onOpen?.();
229
+ },
230
+ });
231
+ };
232
+ /** Opens a native {@link WebSocket} and returns {@link TestSetupWebSocket}. */
233
+ export const testSetupWebSocket = async (url) => {
234
+ const socket = new globalThis.WebSocket(url);
235
+ socket.binaryType = "arraybuffer";
236
+ await new Promise((resolve, reject) => {
237
+ const onOpen = () => {
238
+ cleanup();
239
+ resolve();
92
240
  };
93
- socket.onclose = (event) => {
94
- onClose?.(event);
95
- resolve(err({ type: "WebSocketConnectionCloseError", event }));
241
+ const onError = () => {
242
+ cleanup();
243
+ socket.close();
244
+ reject(new Error("WebSocket connection failed"));
96
245
  };
97
- socket.onmessage = (event) => {
98
- onMessage?.(event.data);
246
+ const cleanup = () => {
247
+ socket.removeEventListener("open", onOpen);
248
+ socket.removeEventListener("error", onError);
99
249
  };
100
- }))(reconnectController).then((result) => {
101
- if (result.ok || result.error.type === "AbortError")
102
- return;
103
- onError?.(result.error);
250
+ socket.addEventListener("open", onOpen, { once: true });
251
+ socket.addEventListener("error", onError, { once: true });
104
252
  });
105
253
  return {
254
+ socket,
106
255
  send: (data) => {
107
- // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
108
- if (!socket || socket.readyState === socket.CONNECTING) {
109
- return err({ type: "WebSocketSendError" });
110
- }
111
- socket.send(data);
112
- return ok();
256
+ socket.send(ensureSendableData(data));
113
257
  },
114
- getReadyState: () => socket ? nativeToStringState[socket.readyState] : "connecting",
115
- isOpen: () => (socket ? socket.readyState === socket.OPEN : false),
116
- [Symbol.dispose]() {
117
- if (isDisposed)
258
+ waitForMessage: () => new Promise((resolve, reject) => {
259
+ if (socket.readyState === globalThis.WebSocket.CLOSED) {
260
+ reject(new Error("WebSocket closed before message"));
118
261
  return;
119
- isDisposed = true;
120
- reconnectController.abort();
121
- disposeSocket();
122
- disposePromise?.();
262
+ }
263
+ const onMessage = (event) => {
264
+ cleanup();
265
+ if (typeof event.data === "string") {
266
+ resolve(event.data);
267
+ return;
268
+ }
269
+ resolve(new globalThis.Uint8Array(event.data));
270
+ };
271
+ const onClose = () => {
272
+ cleanup();
273
+ reject(new Error("WebSocket closed before message"));
274
+ };
275
+ const cleanup = () => {
276
+ socket.removeEventListener("message", onMessage);
277
+ socket.removeEventListener("close", onClose);
278
+ };
279
+ socket.addEventListener("message", onMessage, { once: true });
280
+ socket.addEventListener("close", onClose, { once: true });
281
+ }),
282
+ [Symbol.asyncDispose]: async () => {
283
+ if (socket.readyState === globalThis.WebSocket.CLOSED)
284
+ return;
285
+ const closed = new Promise((resolve) => {
286
+ socket.addEventListener("close", () => resolve(), { once: true });
287
+ });
288
+ socket.close();
289
+ await closed;
123
290
  },
124
291
  };
125
292
  };
126
- const nativeToStringState = {
127
- [WebSocket.CONNECTING]: "connecting",
128
- [WebSocket.OPEN]: "open",
129
- [WebSocket.CLOSING]: "closing",
130
- [WebSocket.CLOSED]: "closed",
131
- };