@evolu/common 8.10.0 → 8.11.0

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