@kyneta/websocket-transport 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -1,69 +1,190 @@
1
1
  import {
2
2
  wrapStandardWebsocket
3
- } from "./chunk-5FHT54WT.js";
3
+ } from "./chunk-PSG3LLT5.js";
4
4
 
5
- // src/client-transport.ts
6
- import { Transport } from "@kyneta/exchange";
7
- import {
8
- cborCodec,
9
- decodeBinaryFrame,
10
- encodeComplete,
11
- FragmentReassembler,
12
- fragmentPayload,
13
- wrapCompleteMessage
14
- } from "@kyneta/wire";
15
-
16
- // src/client-state-machine.ts
17
- import { ClientStateMachine } from "@kyneta/exchange";
18
- var WS_VALID_TRANSITIONS = {
19
- disconnected: ["connecting"],
20
- connecting: ["connected", "disconnected", "reconnecting"],
21
- connected: ["ready", "disconnected", "reconnecting"],
22
- ready: ["disconnected", "reconnecting"],
23
- reconnecting: ["connecting", "disconnected"]
24
- };
25
- var WebsocketClientStateMachine = class extends ClientStateMachine {
26
- constructor() {
27
- super({
28
- initialState: { status: "disconnected" },
29
- validTransitions: WS_VALID_TRANSITIONS
30
- });
31
- }
32
- /**
33
- * Check if the client is in a "connected" state (either connected or ready).
34
- */
35
- isConnectedOrReady() {
36
- const s = this.getStatus();
37
- return s === "connected" || s === "ready";
38
- }
39
- /**
40
- * Check if the client is ready (server ready signal received).
41
- */
42
- isReady() {
43
- return this.getStatus() === "ready";
5
+ // src/client-program.ts
6
+ import { computeBackoffDelay, DEFAULT_RECONNECT } from "@kyneta/transport";
7
+ function createWsClientProgram(options = {}) {
8
+ const { jitterFn = () => Math.random() * 1e3 } = options;
9
+ const reconnect = {
10
+ ...DEFAULT_RECONNECT,
11
+ ...options.reconnect
12
+ };
13
+ function tryReconnect(currentAttempt, reason, ...extraEffects) {
14
+ if (!reconnect.enabled) {
15
+ return [{ status: "disconnected", reason }, ...extraEffects];
16
+ }
17
+ if (currentAttempt >= reconnect.maxAttempts) {
18
+ return [
19
+ {
20
+ status: "disconnected",
21
+ reason: { type: "max-retries-exceeded", attempts: currentAttempt }
22
+ },
23
+ ...extraEffects
24
+ ];
25
+ }
26
+ const delay = computeBackoffDelay(
27
+ currentAttempt + 1,
28
+ reconnect.baseDelay,
29
+ reconnect.maxDelay,
30
+ jitterFn()
31
+ );
32
+ return [
33
+ {
34
+ status: "reconnecting",
35
+ attempt: currentAttempt + 1,
36
+ nextAttemptMs: delay
37
+ },
38
+ ...extraEffects,
39
+ { type: "start-reconnect-timer", delayMs: delay }
40
+ ];
44
41
  }
45
- };
42
+ return {
43
+ init: [{ status: "disconnected" }],
44
+ update(msg, model) {
45
+ switch (msg.type) {
46
+ // -----------------------------------------------------------------
47
+ // start
48
+ // -----------------------------------------------------------------
49
+ case "start": {
50
+ if (model.status !== "disconnected") return [model];
51
+ return [
52
+ { status: "connecting", attempt: 1 },
53
+ { type: "create-websocket", attempt: 1 }
54
+ ];
55
+ }
56
+ // -----------------------------------------------------------------
57
+ // socket-opened
58
+ // -----------------------------------------------------------------
59
+ case "socket-opened": {
60
+ if (model.status !== "connecting") return [model];
61
+ return [{ status: "connected" }, { type: "start-keepalive" }];
62
+ }
63
+ // -----------------------------------------------------------------
64
+ // server-ready
65
+ // -----------------------------------------------------------------
66
+ case "server-ready": {
67
+ if (model.status === "ready") return [model];
68
+ if (model.status === "connected") {
69
+ return [{ status: "ready" }, { type: "add-channel-and-establish" }];
70
+ }
71
+ if (model.status === "connecting") {
72
+ return [
73
+ { status: "ready" },
74
+ { type: "start-keepalive" },
75
+ { type: "add-channel-and-establish" }
76
+ ];
77
+ }
78
+ return [model];
79
+ }
80
+ // -----------------------------------------------------------------
81
+ // socket-closed
82
+ // -----------------------------------------------------------------
83
+ case "socket-closed": {
84
+ const reason = {
85
+ type: "closed",
86
+ code: msg.code,
87
+ reason: msg.reason
88
+ };
89
+ if (model.status === "connected") {
90
+ return tryReconnect(0, reason, { type: "stop-keepalive" });
91
+ }
92
+ if (model.status === "ready") {
93
+ return tryReconnect(
94
+ 0,
95
+ reason,
96
+ { type: "stop-keepalive" },
97
+ { type: "remove-channel" }
98
+ );
99
+ }
100
+ return [model];
101
+ }
102
+ // -----------------------------------------------------------------
103
+ // socket-error
104
+ // -----------------------------------------------------------------
105
+ case "socket-error": {
106
+ const reason = {
107
+ type: "error",
108
+ error: msg.error
109
+ };
110
+ if (model.status === "connecting") {
111
+ return tryReconnect(model.attempt, reason);
112
+ }
113
+ if (model.status === "connected") {
114
+ return tryReconnect(0, reason, { type: "stop-keepalive" });
115
+ }
116
+ if (model.status === "ready") {
117
+ return tryReconnect(
118
+ 0,
119
+ reason,
120
+ { type: "stop-keepalive" },
121
+ { type: "remove-channel" }
122
+ );
123
+ }
124
+ return [model];
125
+ }
126
+ // -----------------------------------------------------------------
127
+ // reconnect-timer-fired
128
+ // -----------------------------------------------------------------
129
+ case "reconnect-timer-fired": {
130
+ if (model.status !== "reconnecting") return [model];
131
+ return [
132
+ { status: "connecting", attempt: model.attempt },
133
+ { type: "create-websocket", attempt: model.attempt }
134
+ ];
135
+ }
136
+ // -----------------------------------------------------------------
137
+ // stop
138
+ // -----------------------------------------------------------------
139
+ case "stop": {
140
+ if (model.status === "disconnected") return [model];
141
+ const effects = [{ type: "cancel-reconnect-timer" }];
142
+ if (model.status === "connecting") {
143
+ effects.push({ type: "close-websocket" });
144
+ }
145
+ if (model.status === "connected") {
146
+ effects.push(
147
+ { type: "close-websocket" },
148
+ { type: "stop-keepalive" }
149
+ );
150
+ }
151
+ if (model.status === "ready") {
152
+ effects.push(
153
+ { type: "close-websocket" },
154
+ { type: "stop-keepalive" },
155
+ { type: "remove-channel" }
156
+ );
157
+ }
158
+ return [
159
+ { status: "disconnected", reason: { type: "intentional" } },
160
+ ...effects
161
+ ];
162
+ }
163
+ }
164
+ }
165
+ };
166
+ }
46
167
 
47
168
  // src/client-transport.ts
169
+ import { createObservableProgram } from "@kyneta/machine";
170
+ import { Transport } from "@kyneta/transport";
171
+ import {
172
+ decodeBinaryMessages,
173
+ encodeBinaryAndSend,
174
+ FragmentReassembler
175
+ } from "@kyneta/wire";
48
176
  var DEFAULT_FRAGMENT_THRESHOLD = 100 * 1024;
49
- var DEFAULT_RECONNECT = {
50
- enabled: true,
51
- maxAttempts: 10,
52
- baseDelay: 1e3,
53
- maxDelay: 3e4
54
- };
55
177
  var WebsocketClientTransport = class extends Transport {
56
178
  #peerId;
179
+ #options;
180
+ #WebSocketImpl;
181
+ // Observable program handle — created in constructor, drives all state
182
+ #handle;
183
+ // Executor-local I/O state — not in the program model
57
184
  #socket;
58
185
  #serverChannel;
59
186
  #keepaliveTimer;
60
187
  #reconnectTimer;
61
- #options;
62
- #WebSocketImpl;
63
- #shouldReconnect = true;
64
- #wasConnectedBefore = false;
65
- // State machine
66
- #stateMachine = new WebsocketClientStateMachine();
67
188
  // Fragmentation
68
189
  #fragmentThreshold;
69
190
  #reassembler;
@@ -75,120 +196,90 @@ var WebsocketClientTransport = class extends Transport {
75
196
  this.#reassembler = new FragmentReassembler({
76
197
  timeoutMs: 1e4
77
198
  });
199
+ const program = createWsClientProgram({
200
+ reconnect: options.reconnect
201
+ });
202
+ this.#handle = createObservableProgram(program, (effect, dispatch) => {
203
+ this.#executeEffect(effect, dispatch);
204
+ });
78
205
  this.#setupLifecycleEvents();
79
206
  }
80
207
  // ==========================================================================
81
- // Lifecycle event forwarding
208
+ // Effect executor — interprets data effects as I/O
82
209
  // ==========================================================================
83
- #setupLifecycleEvents() {
84
- this.#stateMachine.subscribeToTransitions((transition) => {
85
- this.#options.lifecycle?.onStateChange?.(transition);
86
- const { from, to } = transition;
87
- if (to.status === "disconnected" && to.reason) {
88
- this.#options.lifecycle?.onDisconnect?.(to.reason);
210
+ #executeEffect(effect, dispatch) {
211
+ switch (effect.type) {
212
+ case "create-websocket": {
213
+ this.#doCreateWebsocket(dispatch);
214
+ break;
89
215
  }
90
- if (to.status === "reconnecting") {
91
- this.#options.lifecycle?.onReconnecting?.(to.attempt, to.nextAttemptMs);
92
- }
93
- if (this.#wasConnectedBefore && (from.status === "reconnecting" || from.status === "connecting") && (to.status === "connected" || to.status === "ready")) {
94
- this.#options.lifecycle?.onReconnected?.();
216
+ case "close-websocket": {
217
+ if (this.#socket) {
218
+ this.#socket.close(1e3, "Client disconnecting");
219
+ this.#socket = void 0;
220
+ }
221
+ break;
95
222
  }
96
- if (to.status === "ready") {
97
- this.#options.lifecycle?.onReady?.();
223
+ case "add-channel-and-establish": {
224
+ if (this.#serverChannel) {
225
+ this.removeChannel(this.#serverChannel.channelId);
226
+ this.#serverChannel = void 0;
227
+ }
228
+ this.#serverChannel = this.addChannel();
229
+ this.establishChannel(this.#serverChannel.channelId);
230
+ break;
98
231
  }
99
- });
100
- }
101
- // ==========================================================================
102
- // State observation API
103
- // ==========================================================================
104
- /**
105
- * Get the current state of the connection.
106
- */
107
- getState() {
108
- return this.#stateMachine.getState();
109
- }
110
- /**
111
- * Subscribe to state transitions.
112
- * @returns Unsubscribe function
113
- */
114
- subscribeToTransitions(listener) {
115
- return this.#stateMachine.subscribeToTransitions(listener);
116
- }
117
- /**
118
- * Wait for a specific state.
119
- */
120
- waitForState(predicate, options) {
121
- return this.#stateMachine.waitForState(predicate, options);
122
- }
123
- /**
124
- * Wait for a specific status.
125
- */
126
- waitForStatus(status, options) {
127
- return this.#stateMachine.waitForStatus(status, options);
128
- }
129
- /**
130
- * Check if the client is ready (server ready signal received).
131
- */
132
- get isReady() {
133
- return this.#stateMachine.isReady();
134
- }
135
- // ==========================================================================
136
- // Adapter abstract method implementations
137
- // ==========================================================================
138
- generate() {
139
- return {
140
- transportType: this.transportType,
141
- send: (msg) => {
142
- if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
143
- return;
232
+ case "remove-channel": {
233
+ if (this.#serverChannel) {
234
+ this.removeChannel(this.#serverChannel.channelId);
235
+ this.#serverChannel = void 0;
144
236
  }
145
- const frame = encodeComplete(cborCodec, msg);
146
- if (this.#fragmentThreshold > 0 && frame.length > this.#fragmentThreshold) {
147
- const fragments = fragmentPayload(frame, this.#fragmentThreshold);
148
- for (const fragment of fragments) {
149
- this.#socket.send(fragment);
150
- }
151
- } else {
152
- this.#socket.send(wrapCompleteMessage(frame));
237
+ break;
238
+ }
239
+ case "start-reconnect-timer": {
240
+ this.#reconnectTimer = setTimeout(() => {
241
+ this.#reconnectTimer = void 0;
242
+ dispatch({ type: "reconnect-timer-fired" });
243
+ }, effect.delayMs);
244
+ break;
245
+ }
246
+ case "cancel-reconnect-timer": {
247
+ if (this.#reconnectTimer !== void 0) {
248
+ clearTimeout(this.#reconnectTimer);
249
+ this.#reconnectTimer = void 0;
153
250
  }
154
- },
155
- stop: () => {
251
+ break;
252
+ }
253
+ case "start-keepalive": {
254
+ this.#startKeepalive();
255
+ break;
256
+ }
257
+ case "stop-keepalive": {
258
+ this.#stopKeepalive();
259
+ break;
156
260
  }
157
- };
158
- }
159
- async onStart() {
160
- if (!this.identity) {
161
- throw new Error(
162
- "Adapter not properly initialized \u2014 identity not available"
163
- );
164
261
  }
165
- this.#peerId = this.identity.peerId;
166
- this.#shouldReconnect = true;
167
- this.#wasConnectedBefore = false;
168
- await this.#connect();
169
- }
170
- async onStop() {
171
- this.#shouldReconnect = false;
172
- this.#reassembler.dispose();
173
- this.#disconnect({ type: "intentional" });
174
262
  }
175
263
  // ==========================================================================
176
- // Connection management
264
+ // WebSocket creation — the core I/O operation
177
265
  // ==========================================================================
178
266
  /**
179
- * Connect to the Websocket server.
267
+ * Create a WebSocket and wire up event handlers to dispatch messages.
268
+ *
269
+ * The message handler is set up IMMEDIATELY after creation (before
270
+ * the open event) to handle the race condition where the server sends
271
+ * "ready" before the client's open promise resolves.
180
272
  */
181
- async #connect() {
182
- const currentState = this.#stateMachine.getState();
183
- if (currentState.status === "connecting") {
273
+ #doCreateWebsocket(dispatch) {
274
+ const peerId = this.#peerId;
275
+ if (!peerId) {
276
+ dispatch({
277
+ type: "socket-error",
278
+ error: new Error("Cannot connect: peerId not set")
279
+ });
184
280
  return;
185
281
  }
186
- if (!this.#peerId) {
187
- throw new Error("Cannot connect: peerId not set");
188
- }
189
- const attempt = currentState.status === "reconnecting" ? currentState.attempt : 1;
190
- this.#stateMachine.transition({ status: "connecting", attempt });
191
- const url = typeof this.#options.url === "function" ? this.#options.url(this.#peerId) : this.#options.url;
282
+ const url = typeof this.#options.url === "function" ? this.#options.url(peerId) : this.#options.url;
192
283
  try {
193
284
  if (this.#options.headers && Object.keys(this.#options.headers).length > 0) {
194
285
  const BunWebSocket = this.#WebSocketImpl;
@@ -199,122 +290,89 @@ var WebsocketClientTransport = class extends Transport {
199
290
  this.#socket = new this.#WebSocketImpl(url);
200
291
  }
201
292
  this.#socket.binaryType = "arraybuffer";
202
- this.#socket.addEventListener("message", (event) => {
203
- this.#handleMessage(event);
293
+ const socket = this.#socket;
294
+ socket.addEventListener("message", (event) => {
295
+ this.#handleMessage(event, dispatch);
204
296
  });
205
- await new Promise((resolve, reject) => {
206
- if (!this.#socket) {
207
- reject(new Error("Socket not created"));
208
- return;
209
- }
210
- const onOpen = () => {
211
- cleanup();
212
- resolve();
213
- };
214
- const onError = (event) => {
215
- cleanup();
216
- reject(new Error(`WebSocket connection failed: ${event}`));
217
- };
218
- const onClose = () => {
219
- cleanup();
220
- reject(new Error("WebSocket closed during connection"));
221
- };
222
- const cleanup = () => {
223
- this.#socket?.removeEventListener("open", onOpen);
224
- this.#socket?.removeEventListener("error", onError);
225
- this.#socket?.removeEventListener("close", onClose);
226
- };
227
- this.#socket.addEventListener("open", onOpen);
228
- this.#socket.addEventListener("error", onError);
229
- this.#socket.addEventListener("close", onClose);
230
- });
231
- this.#stateMachine.transition({ status: "connected" });
232
- this.#socket.addEventListener("close", (event) => {
233
- this.#handleClose(event.code, event.reason);
234
- });
235
- this.#startKeepalive();
297
+ let settled = false;
298
+ const onOpen = () => {
299
+ cleanup();
300
+ settled = true;
301
+ dispatch({ type: "socket-opened" });
302
+ socket.addEventListener("close", (event) => {
303
+ dispatch({
304
+ type: "socket-closed",
305
+ code: event.code,
306
+ reason: event.reason
307
+ });
308
+ });
309
+ };
310
+ const onError = () => {
311
+ if (settled) return;
312
+ cleanup();
313
+ settled = true;
314
+ dispatch({
315
+ type: "socket-error",
316
+ error: new Error("WebSocket connection failed")
317
+ });
318
+ };
319
+ const onClose = () => {
320
+ if (settled) return;
321
+ cleanup();
322
+ settled = true;
323
+ dispatch({
324
+ type: "socket-error",
325
+ error: new Error("WebSocket closed during connection")
326
+ });
327
+ };
328
+ const cleanup = () => {
329
+ socket.removeEventListener("open", onOpen);
330
+ socket.removeEventListener("error", onError);
331
+ socket.removeEventListener("close", onClose);
332
+ };
333
+ socket.addEventListener("open", onOpen);
334
+ socket.addEventListener("error", onError);
335
+ socket.addEventListener("close", onClose);
236
336
  } catch (error) {
237
- this.#scheduleReconnect({
238
- type: "error",
337
+ dispatch({
338
+ type: "socket-error",
239
339
  error: error instanceof Error ? error : new Error(String(error))
240
340
  });
241
341
  }
242
342
  }
243
- /**
244
- * Disconnect from the Websocket server.
245
- */
246
- #disconnect(reason) {
247
- this.#stopKeepalive();
248
- this.#clearReconnectTimer();
249
- if (this.#socket) {
250
- this.#socket.close(1e3, "Client disconnecting");
251
- this.#socket = void 0;
252
- }
253
- if (this.#serverChannel) {
254
- this.removeChannel(this.#serverChannel.channelId);
255
- this.#serverChannel = void 0;
256
- }
257
- const currentState = this.#stateMachine.getState();
258
- if (currentState.status !== "disconnected") {
259
- this.#stateMachine.transition({ status: "disconnected", reason });
260
- }
261
- }
262
343
  // ==========================================================================
263
- // Message handling
344
+ // Message handling — I/O parsing logic
264
345
  // ==========================================================================
265
346
  /**
266
347
  * Handle incoming Websocket messages.
348
+ *
349
+ * Text frames carry the "ready" handshake and keepalive pong.
350
+ * Binary frames carry CBOR-encoded ChannelMsg.
267
351
  */
268
- #handleMessage(event) {
352
+ #handleMessage(event, dispatch) {
269
353
  const data = event.data;
270
354
  if (typeof data === "string") {
271
355
  if (data === "ready") {
272
- this.#handleServerReady();
356
+ dispatch({ type: "server-ready" });
273
357
  }
274
358
  return;
275
359
  }
276
360
  if (data instanceof ArrayBuffer) {
277
- const result = this.#reassembler.receiveRaw(new Uint8Array(data));
278
- if (result.status === "complete") {
279
- try {
280
- const frame = decodeBinaryFrame(result.data);
281
- const messages = cborCodec.decode(frame.content.payload);
361
+ try {
362
+ const messages = decodeBinaryMessages(
363
+ new Uint8Array(data),
364
+ this.#reassembler
365
+ );
366
+ if (messages) {
282
367
  for (const msg of messages) {
283
368
  this.#handleChannelMessage(msg);
284
369
  }
285
- } catch (error) {
286
- console.error("Failed to decode message:", error);
287
370
  }
288
- } else if (result.status === "error") {
289
- console.error("Fragment reassembly error:", result.error);
371
+ } catch (error) {
372
+ console.error("Failed to decode message:", error);
290
373
  }
291
374
  }
292
375
  }
293
- /**
294
- * Handle the "ready" signal from the server.
295
- *
296
- * Creates the channel and starts the establishment handshake.
297
- * The "ready" signal is a transport-level indicator that the server's
298
- * Websocket handler is ready. After receiving it, we create our channel
299
- * and send a real establish-request.
300
- */
301
- #handleServerReady() {
302
- const currentState = this.#stateMachine.getState();
303
- if (currentState.status === "ready") {
304
- return;
305
- }
306
- if (currentState.status === "connecting") {
307
- this.#stateMachine.transition({ status: "connected" });
308
- }
309
- this.#stateMachine.transition({ status: "ready" });
310
- this.#wasConnectedBefore = true;
311
- if (this.#serverChannel) {
312
- this.removeChannel(this.#serverChannel.channelId);
313
- this.#serverChannel = void 0;
314
- }
315
- this.#serverChannel = this.addChannel();
316
- this.establishChannel(this.#serverChannel.channelId);
317
- }
318
376
  /**
319
377
  * Handle a decoded channel message.
320
378
  */
@@ -324,17 +382,6 @@ var WebsocketClientTransport = class extends Transport {
324
382
  }
325
383
  this.#serverChannel.onReceive(msg);
326
384
  }
327
- /**
328
- * Handle Websocket close.
329
- */
330
- #handleClose(code, reason) {
331
- this.#stopKeepalive();
332
- if (this.#serverChannel) {
333
- this.removeChannel(this.#serverChannel.channelId);
334
- this.#serverChannel = void 0;
335
- }
336
- this.#scheduleReconnect({ type: "closed", code, reason });
337
- }
338
385
  // ==========================================================================
339
386
  // Keepalive
340
387
  // ==========================================================================
@@ -354,51 +401,94 @@ var WebsocketClientTransport = class extends Transport {
354
401
  }
355
402
  }
356
403
  // ==========================================================================
357
- // Reconnection
404
+ // Lifecycle event forwarding
405
+ // ==========================================================================
406
+ #setupLifecycleEvents() {
407
+ let wasConnectedBefore = false;
408
+ this.#handle.subscribeToTransitions((transition) => {
409
+ this.#options.lifecycle?.onStateChange?.(transition);
410
+ const { from, to } = transition;
411
+ if (to.status === "disconnected" && to.reason) {
412
+ this.#options.lifecycle?.onDisconnect?.(to.reason);
413
+ }
414
+ if (to.status === "reconnecting") {
415
+ this.#options.lifecycle?.onReconnecting?.(to.attempt, to.nextAttemptMs);
416
+ }
417
+ if (wasConnectedBefore && (from.status === "reconnecting" || from.status === "connecting") && (to.status === "connected" || to.status === "ready")) {
418
+ this.#options.lifecycle?.onReconnected?.();
419
+ }
420
+ if (to.status === "ready") {
421
+ this.#options.lifecycle?.onReady?.();
422
+ wasConnectedBefore = true;
423
+ }
424
+ });
425
+ }
426
+ // ==========================================================================
427
+ // State observation — delegated to the observable handle
358
428
  // ==========================================================================
359
429
  /**
360
- * Schedule a reconnection attempt or transition to disconnected.
430
+ * Get the current connection state.
361
431
  */
362
- #scheduleReconnect(reason) {
363
- const currentState = this.#stateMachine.getState();
364
- if (currentState.status === "disconnected") {
365
- return;
366
- }
367
- const reconnectOpts = {
368
- ...DEFAULT_RECONNECT,
369
- ...this.#options.reconnect
432
+ getState() {
433
+ return this.#handle.getState();
434
+ }
435
+ /**
436
+ * Subscribe to state transitions.
437
+ */
438
+ subscribeToTransitions(listener) {
439
+ return this.#handle.subscribeToTransitions(listener);
440
+ }
441
+ /**
442
+ * Wait for a specific state.
443
+ */
444
+ waitForState(predicate, options) {
445
+ return this.#handle.waitForState(predicate, options);
446
+ }
447
+ /**
448
+ * Wait for a specific status.
449
+ */
450
+ waitForStatus(status, options) {
451
+ return this.#handle.waitForStatus(status, options);
452
+ }
453
+ /**
454
+ * Whether the client is ready (server ready signal received).
455
+ */
456
+ get isReady() {
457
+ return this.#handle.getState().status === "ready";
458
+ }
459
+ // ==========================================================================
460
+ // Transport abstract method implementations
461
+ // ==========================================================================
462
+ generate() {
463
+ return {
464
+ transportType: this.transportType,
465
+ send: (msg) => {
466
+ const socket = this.#socket;
467
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
468
+ return;
469
+ }
470
+ encodeBinaryAndSend(
471
+ msg,
472
+ this.#fragmentThreshold,
473
+ (data) => socket.send(new Uint8Array(data).buffer)
474
+ );
475
+ },
476
+ stop: () => {
477
+ }
370
478
  };
371
- if (!this.#shouldReconnect || !reconnectOpts.enabled) {
372
- this.#stateMachine.transition({ status: "disconnected", reason });
373
- return;
374
- }
375
- const currentAttempt = currentState.status === "reconnecting" ? currentState.attempt : currentState.status === "connecting" ? currentState.attempt : 0;
376
- if (currentAttempt >= reconnectOpts.maxAttempts) {
377
- this.#stateMachine.transition({
378
- status: "disconnected",
379
- reason: { type: "max-retries-exceeded", attempts: currentAttempt }
380
- });
381
- return;
382
- }
383
- const nextAttempt = currentAttempt + 1;
384
- const delay = Math.min(
385
- reconnectOpts.baseDelay * 2 ** (nextAttempt - 1) + Math.random() * 1e3,
386
- reconnectOpts.maxDelay
387
- );
388
- this.#stateMachine.transition({
389
- status: "reconnecting",
390
- attempt: nextAttempt,
391
- nextAttemptMs: delay
392
- });
393
- this.#reconnectTimer = setTimeout(() => {
394
- this.#connect();
395
- }, delay);
396
479
  }
397
- #clearReconnectTimer() {
398
- if (this.#reconnectTimer) {
399
- clearTimeout(this.#reconnectTimer);
400
- this.#reconnectTimer = void 0;
480
+ async onStart() {
481
+ if (!this.identity) {
482
+ throw new Error(
483
+ "Transport not properly initialized \u2014 identity not available"
484
+ );
401
485
  }
486
+ this.#peerId = this.identity.peerId;
487
+ this.#handle.dispatch({ type: "start" });
488
+ }
489
+ async onStop() {
490
+ this.#reassembler.dispose();
491
+ this.#handle.dispatch({ type: "stop" });
402
492
  }
403
493
  };
404
494
  function createWebsocketClient(options) {
@@ -409,10 +499,10 @@ function createServiceWebsocketClient(options) {
409
499
  }
410
500
  export {
411
501
  DEFAULT_FRAGMENT_THRESHOLD,
412
- WebsocketClientStateMachine,
413
502
  WebsocketClientTransport,
414
503
  createServiceWebsocketClient,
415
504
  createWebsocketClient,
505
+ createWsClientProgram,
416
506
  wrapStandardWebsocket
417
507
  };
418
508
  //# sourceMappingURL=client.js.map