@rlanz/socket 0.0.1-9 → 0.1.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 (43) hide show
  1. package/README.md +556 -556
  2. package/build/{base_channel-DPOnqned.d.ts → base_channel-CasBQV5I.d.ts} +7 -25
  3. package/build/{chunk-6PJ4ALJD.js → chunk-DM3UQMWW.js} +5 -3
  4. package/build/chunk-DM3UQMWW.js.map +1 -0
  5. package/build/{chunk-QNOOQXYY.js → chunk-DR6GWQUW.js} +242 -199
  6. package/build/chunk-DR6GWQUW.js.map +1 -0
  7. package/build/chunk-NUPBNOP3.js +61 -0
  8. package/build/chunk-NUPBNOP3.js.map +1 -0
  9. package/build/{chunk-YVC7IMDN.js → chunk-UE75J3JE.js} +2 -2
  10. package/build/chunk-YSW4MNFR.js +7 -0
  11. package/build/chunk-YSW4MNFR.js.map +1 -0
  12. package/build/{framework-B8pJ4cCA.d.ts → framework-Cb71JA5f.d.ts} +1 -1
  13. package/build/{index-DjBYUhFR.d.ts → index-DCPtfkJL.d.ts} +1 -1
  14. package/build/index.d.ts +3 -2
  15. package/build/providers/socket_provider.d.ts +3 -2
  16. package/build/providers/socket_provider.js +312 -130
  17. package/build/providers/socket_provider.js.map +1 -1
  18. package/build/services/socket.d.ts +3 -2
  19. package/build/shared_types-B11CaOr5.d.ts +26 -0
  20. package/build/{socket_service-BcC9ASCE.d.ts → socket_service-C2YRbhrO.d.ts} +1 -1
  21. package/build/src/assembler_hook.js +54 -30
  22. package/build/src/assembler_hook.js.map +1 -1
  23. package/build/src/client/index.d.ts +2 -7
  24. package/build/src/client/index.js +3 -2
  25. package/build/src/client/react.d.ts +3 -8
  26. package/build/src/client/react.js +4 -3
  27. package/build/src/client/react.js.map +1 -1
  28. package/build/src/client/types.d.ts +9 -13
  29. package/build/src/client/vue.d.ts +3 -8
  30. package/build/src/client/vue.js +4 -3
  31. package/build/src/client/vue.js.map +1 -1
  32. package/build/src/decorators.d.ts +2 -1
  33. package/build/src/health_check.d.ts +3 -2
  34. package/build/src/otel.d.ts +1 -0
  35. package/build/src/otel.js +211 -27
  36. package/build/src/otel.js.map +1 -1
  37. package/build/src/types.d.ts +2 -1
  38. package/package.json +31 -7
  39. package/build/chunk-6PJ4ALJD.js.map +0 -1
  40. package/build/chunk-D3HUBCBW.js +0 -18
  41. package/build/chunk-D3HUBCBW.js.map +0 -1
  42. package/build/chunk-QNOOQXYY.js.map +0 -1
  43. /package/build/{chunk-YVC7IMDN.js.map → chunk-UE75J3JE.js.map} +0 -0
@@ -1,6 +1,9 @@
1
+ import {
2
+ SERVER_DISCONNECT_CODE
3
+ } from "./chunk-YSW4MNFR.js";
1
4
  import {
2
5
  BindableChannelPattern
3
- } from "./chunk-6PJ4ALJD.js";
6
+ } from "./chunk-DM3UQMWW.js";
4
7
 
5
8
  // src/client/callback.ts
6
9
  function reportClientError(error) {
@@ -86,7 +89,7 @@ var Channel = class {
86
89
  * Whether the channel should stay subscribed.
87
90
  */
88
91
  #subscribed = false;
89
- #active = false;
92
+ #remoteSynchronization = { status: "idle" };
90
93
  /**
91
94
  * Event handlers grouped by event type.
92
95
  */
@@ -114,11 +117,7 @@ var Channel = class {
114
117
  #boundPresenceHandler = null;
115
118
  #boundEventHandler = null;
116
119
  #boundDisconnectHandler = null;
117
- #listenersAttached = false;
118
- #pendingSubscribe = null;
119
- #subscribeToken = 0;
120
120
  #managedSubscriptions = 0;
121
- #pendingUnsubscribe = null;
122
121
  constructor(name, transport) {
123
122
  this.name = name;
124
123
  this.#transport = transport;
@@ -137,7 +136,7 @@ var Channel = class {
137
136
  * Whether the channel is active on the server.
138
137
  */
139
138
  get active() {
140
- return this.#active;
139
+ return this.#remoteSynchronization.status === "active";
141
140
  }
142
141
  /**
143
142
  * List of present users.
@@ -237,7 +236,9 @@ var Channel = class {
237
236
  return {
238
237
  ready,
239
238
  release: () => {
240
- if (released) return this.#pendingUnsubscribe ?? Promise.resolve();
239
+ if (released) {
240
+ return this.#remoteSynchronization.status === "unsubscribing" ? this.#remoteSynchronization.promise : Promise.resolve();
241
+ }
241
242
  released = true;
242
243
  this.#managedSubscriptions--;
243
244
  return this.#unsubscribeIfUnused();
@@ -249,43 +250,41 @@ var Channel = class {
249
250
  return this.#ensureSubscribed(options);
250
251
  }
251
252
  async #ensureSubscribed(options) {
252
- if (this.#pendingUnsubscribe) {
253
- await this.#pendingUnsubscribe;
253
+ if (this.#remoteSynchronization.status === "unsubscribing") {
254
+ await this.#remoteSynchronization.promise;
254
255
  }
255
256
  if (!this.subscribed) return this;
256
- if (this.#pendingSubscribe) {
257
- await this.#pendingSubscribe.promise;
257
+ if (this.#remoteSynchronization.status === "subscribing") {
258
+ await this.#remoteSynchronization.pending.promise;
258
259
  return this;
259
260
  }
260
- if (this.#active) {
261
+ if (this.#remoteSynchronization.status === "active") {
261
262
  return this;
262
263
  }
263
264
  const timeout = options.timeout ?? 5e3;
264
- if (!this.#listenersAttached) {
265
+ if (this.#remoteSynchronization.status === "idle" || this.#remoteSynchronization.status === "remoteSubscriptionUncertain") {
265
266
  this.#setupSocketListeners();
266
267
  }
267
- const token = ++this.#subscribeToken;
268
268
  const deferred = Promise.withResolvers();
269
- this.#pendingSubscribe = {
270
- promise: deferred.promise,
271
- resolve: deferred.resolve,
272
- reject: deferred.reject,
273
- token
269
+ const synchronization = {
270
+ status: "subscribing",
271
+ pending: deferred,
272
+ connected: true
274
273
  };
275
- const { resolve, reject } = this.#pendingSubscribe;
274
+ this.#remoteSynchronization = synchronization;
276
275
  const timer = setTimeout(() => {
277
- if (token !== this.#subscribeToken) return;
278
- this.#subscribeToken++;
279
- this.#pendingSubscribe = null;
276
+ if (this.#remoteSynchronization !== synchronization) return;
277
+ this.#remoteSynchronization = { status: "waitingForReconnect" };
280
278
  this.#resetConnectionState();
281
- reject(new Error(`Subscribe timeout for channel: ${this.name}`));
279
+ synchronization.pending.reject(new Error(`Subscribe timeout for channel: ${this.name}`));
280
+ void this.#transport.sendRequest({ type: "unsubscribe", channel: this.name }, timeout).catch(() => {
281
+ });
282
282
  }, timeout);
283
283
  this.#transport.sendRequest({ type: "subscribe", channel: this.name }, timeout).then((response) => {
284
- if (token !== this.#subscribeToken) return;
284
+ if (this.#remoteSynchronization !== synchronization) return;
285
285
  clearTimeout(timer);
286
- this.#pendingSubscribe = null;
287
286
  if (response.ok) {
288
- this.#active = true;
287
+ this.#remoteSynchronization = { status: "active" };
289
288
  const presenceData = ClientPresenceSnapshot.fromTransport(
290
289
  response.data?.presenceData,
291
290
  this.name
@@ -293,17 +292,20 @@ var Channel = class {
293
292
  if (presenceData) {
294
293
  this.#handleInitialPresence(presenceData);
295
294
  }
296
- resolve(this);
295
+ synchronization.pending.resolve(this);
297
296
  } else {
297
+ this.#remoteSynchronization = { status: "waitingForReconnect" };
298
298
  this.#resetConnectionState();
299
- reject(new Error(response.error ?? "Subscribe failed"));
299
+ synchronization.pending.reject(new Error(response.error));
300
300
  }
301
301
  }).catch((error) => {
302
- if (token !== this.#subscribeToken) return;
302
+ if (this.#remoteSynchronization !== synchronization) return;
303
303
  clearTimeout(timer);
304
- this.#pendingSubscribe = null;
304
+ this.#remoteSynchronization = synchronization.connected ? { status: "subscriptionUncertain" } : { status: "waitingForReconnect" };
305
305
  this.#resetConnectionState();
306
- reject(error instanceof Error ? error : new Error("Subscribe failed"));
306
+ synchronization.pending.reject(
307
+ error instanceof Error ? error : new Error("Subscribe failed")
308
+ );
307
309
  });
308
310
  await deferred.promise;
309
311
  return this;
@@ -318,35 +320,45 @@ var Channel = class {
318
320
  }
319
321
  #unsubscribeIfUnused(options = {}) {
320
322
  if (this.subscribed) return Promise.resolve();
321
- if (this.#pendingUnsubscribe) return this.#pendingUnsubscribe;
322
- const hadPendingSubscribe = this.#pendingSubscribe !== null;
323
- this.#subscribeToken++;
324
- if (this.#pendingSubscribe) {
325
- this.#pendingSubscribe.reject(new Error(`Unsubscribed from channel: ${this.name}`));
326
- this.#pendingSubscribe = null;
327
- }
328
- if (!hadPendingSubscribe && !this.#active) {
323
+ if (this.#remoteSynchronization.status === "unsubscribing") {
324
+ return this.#remoteSynchronization.promise;
325
+ }
326
+ const previousSynchronization = this.#remoteSynchronization;
327
+ const hadPendingSubscribe = previousSynchronization.status === "subscribing";
328
+ if (previousSynchronization.status === "subscribing") {
329
+ previousSynchronization.pending.reject(new Error(`Unsubscribed from channel: ${this.name}`));
330
+ }
331
+ const remoteSubscriptionPossible = previousSynchronization.status === "active" || previousSynchronization.status === "subscriptionUncertain" || previousSynchronization.status === "remoteSubscriptionUncertain";
332
+ if (!hadPendingSubscribe && !remoteSubscriptionPossible) {
329
333
  this.#cleanupTerminal();
330
334
  return Promise.resolve();
331
335
  }
332
- this.#active = false;
333
- const pending = new Promise((resolve) => {
334
- const timeout = options.timeout ?? 5e3;
335
- const timer = setTimeout(() => {
336
- resolve();
337
- }, timeout);
338
- this.#transport.sendRequest({ type: "unsubscribe", channel: this.name }, timeout).then(() => {
339
- clearTimeout(timer);
340
- resolve();
341
- }).catch(() => {
342
- clearTimeout(timer);
343
- resolve();
344
- });
336
+ const timeout = options.timeout ?? 5e3;
337
+ const deferred = Promise.withResolvers();
338
+ const synchronization = {
339
+ status: "unsubscribing",
340
+ promise: deferred.promise
341
+ };
342
+ this.#remoteSynchronization = synchronization;
343
+ let remoteMayRemain = true;
344
+ let terminalSynchronization = null;
345
+ const timer = setTimeout(() => deferred.resolve(), timeout);
346
+ this.#transport.sendRequest({ type: "unsubscribe", channel: this.name }, timeout).then(() => {
347
+ remoteMayRemain = false;
348
+ clearTimeout(timer);
349
+ deferred.resolve();
350
+ if (this.#remoteSynchronization === terminalSynchronization) {
351
+ this.#remoteSynchronization = { status: "idle" };
352
+ }
353
+ }).catch(() => {
354
+ clearTimeout(timer);
355
+ deferred.resolve();
345
356
  });
346
- this.#pendingUnsubscribe = pending;
357
+ const pending = deferred.promise;
347
358
  void pending.finally(() => {
348
- if (this.#pendingUnsubscribe !== pending) return;
349
- this.#pendingUnsubscribe = null;
359
+ if (this.#remoteSynchronization !== synchronization) return;
360
+ terminalSynchronization = remoteMayRemain ? { status: "remoteSubscriptionUncertain" } : { status: "idle" };
361
+ this.#remoteSynchronization = terminalSynchronization;
350
362
  this.#resetConnectionState();
351
363
  this.#detachTransportListeners();
352
364
  if (!this.subscribed) this.#clearCallbacks();
@@ -354,7 +366,7 @@ var Channel = class {
354
366
  return pending;
355
367
  }
356
368
  send(event, data) {
357
- if (!this.#active) {
369
+ if (!this.active) {
358
370
  console.warn(`Cannot send to channel ${this.name}: not subscribed`);
359
371
  return this;
360
372
  }
@@ -370,7 +382,7 @@ var Channel = class {
370
382
  * Relays a client event to other members of this channel.
371
383
  */
372
384
  whisper(event, data) {
373
- if (!this.#active) {
385
+ if (!this.active) {
374
386
  console.warn(`Cannot whisper to channel ${this.name}: not subscribed`);
375
387
  return this;
376
388
  }
@@ -383,7 +395,7 @@ var Channel = class {
383
395
  return this;
384
396
  }
385
397
  async sendWithAck(event, data) {
386
- if (!this.#active) {
398
+ if (!this.active) {
387
399
  throw new Error(`Cannot send to channel ${this.name}: not subscribed`);
388
400
  }
389
401
  const payload = {
@@ -421,10 +433,16 @@ var Channel = class {
421
433
  };
422
434
  this.#transport.on(`channel:${this.name}:event`, this.#boundEventHandler);
423
435
  this.#boundDisconnectHandler = () => {
436
+ if (this.#remoteSynchronization.status === "subscribing") {
437
+ this.#remoteSynchronization.connected = false;
438
+ } else if (this.#remoteSynchronization.status === "active" || this.#remoteSynchronization.status === "subscriptionUncertain") {
439
+ this.#remoteSynchronization = { status: "waitingForReconnect" };
440
+ } else if (this.#remoteSynchronization.status === "remoteSubscriptionUncertain") {
441
+ this.#remoteSynchronization = { status: "idle" };
442
+ }
424
443
  this.#resetConnectionState();
425
444
  };
426
445
  this.#transport.on("disconnect", this.#boundDisconnectHandler);
427
- this.#listenersAttached = true;
428
446
  }
429
447
  /**
430
448
  * Handles the initial presence snapshot.
@@ -460,7 +478,6 @@ var Channel = class {
460
478
  }
461
479
  }
462
480
  #resetConnectionState() {
463
- this.#active = false;
464
481
  this.#presence = null;
465
482
  }
466
483
  #detachTransportListeners() {
@@ -476,7 +493,6 @@ var Channel = class {
476
493
  this.#transport.off("disconnect", this.#boundDisconnectHandler);
477
494
  this.#boundDisconnectHandler = null;
478
495
  }
479
- this.#listenersAttached = false;
480
496
  }
481
497
  #clearUserCallbacks() {
482
498
  this.#eventHandlers.clear();
@@ -492,6 +508,7 @@ var Channel = class {
492
508
  * Performs terminal cleanup after the user leaves or the socket is disposed.
493
509
  */
494
510
  #cleanupTerminal() {
511
+ this.#remoteSynchronization = { status: "idle" };
495
512
  this.#resetConnectionState();
496
513
  this.#detachTransportListeners();
497
514
  this.#clearCallbacks();
@@ -500,22 +517,17 @@ var Channel = class {
500
517
 
501
518
  // src/client/socket_session.ts
502
519
  var ClientSocketSession = class {
503
- #ws = null;
504
- #state = "disconnected";
505
- #connectPromise = null;
506
- #cancelConnect = null;
520
+ #lifecycle = { kind: "disconnected", reconnectAttempt: 0 };
507
521
  #stateHandlers = /* @__PURE__ */ new Set();
508
522
  #eventHandlers = /* @__PURE__ */ new Map();
509
523
  #pendingRequests = /* @__PURE__ */ new Map();
510
524
  #requestId = 0;
511
- #reconnectAttempts = 0;
512
- #reconnectTimer = null;
513
- #manualDisconnect = false;
514
525
  #buildUrl;
515
526
  #createWebSocket;
516
527
  #autoReconnect;
517
528
  #reconnectDelay;
518
529
  #reconnectMaxDelay;
530
+ #shouldReconnect;
519
531
  #onConnected;
520
532
  constructor(options) {
521
533
  this.#buildUrl = options.buildUrl;
@@ -523,115 +535,109 @@ var ClientSocketSession = class {
523
535
  this.#autoReconnect = options.autoReconnect ?? true;
524
536
  this.#reconnectDelay = Math.max(0, options.reconnectDelay ?? 250);
525
537
  this.#reconnectMaxDelay = Math.max(this.#reconnectDelay, options.reconnectMaxDelay ?? 5e3);
538
+ this.#shouldReconnect = options.shouldReconnect ?? ((event) => event.code !== SERVER_DISCONNECT_CODE);
526
539
  this.#onConnected = options.onConnected;
527
540
  }
528
541
  get state() {
529
- return this.#state;
542
+ switch (this.#lifecycle.kind) {
543
+ case "opening":
544
+ case "connecting":
545
+ return "connecting";
546
+ case "opened":
547
+ case "connected":
548
+ return "connected";
549
+ case "disconnected":
550
+ case "waiting":
551
+ case "failed":
552
+ return "disconnected";
553
+ }
530
554
  }
531
555
  get connected() {
532
- return this.#state === "connected";
556
+ return this.state === "connected";
533
557
  }
534
558
  connect() {
535
- if (this.#state === "connected") {
536
- return Promise.resolve();
537
- }
538
- if (this.#connectPromise) {
539
- return this.#connectPromise;
540
- }
541
- this.#manualDisconnect = false;
542
- this.#setState("connecting");
543
- let setupFailed = false;
544
- const connectPromise = new Promise((resolve, reject) => {
545
- let ws;
546
- try {
547
- ws = this.#createWebSocket(this.#buildUrl());
548
- } catch (error) {
549
- setupFailed = true;
550
- reject(error);
551
- return;
552
- }
553
- this.#ws = ws;
554
- let settled = false;
555
- const cleanup = () => {
556
- ws.removeEventListener("open", handleOpen);
557
- ws.removeEventListener("error", handleError);
558
- if (this.#connectPromise === connectPromise) {
559
- this.#connectPromise = null;
560
- }
561
- if (this.#cancelConnect === cancelConnect) {
562
- this.#cancelConnect = null;
563
- }
564
- };
565
- const cancelConnect = (error) => {
566
- if (settled) {
567
- return;
568
- }
569
- settled = true;
570
- cleanup();
571
- reject(error);
572
- };
573
- const handleOpen = () => {
574
- if (settled || this.#ws !== ws) {
575
- return;
576
- }
577
- this.#setState("connected");
578
- if (settled || this.#ws !== ws || !this.connected) {
579
- return;
580
- }
581
- settled = true;
582
- cleanup();
583
- this.#reconnectAttempts = 0;
584
- this.#emitLocal("connect");
585
- if (this.#onConnected) {
586
- this.#callHandler(this.#onConnected);
587
- }
588
- resolve();
589
- };
590
- const handleError = () => {
591
- cancelConnect(new Error("WebSocket connection failed"));
592
- if (this.#ws === ws) {
593
- this.#setState("disconnected");
594
- }
595
- };
596
- const handleClose = () => {
597
- cancelConnect(new Error("WebSocket connection failed"));
598
- if (this.#ws === ws) {
599
- this.#handleClose();
600
- }
559
+ if (this.#lifecycle.kind === "connected") return Promise.resolve();
560
+ if (this.#lifecycle.kind === "opening" || this.#lifecycle.kind === "connecting") {
561
+ return this.#lifecycle.deferred.promise;
562
+ }
563
+ if (this.#lifecycle.kind === "opened") return this.#lifecycle.deferred.promise;
564
+ const reconnectAttempt = this.#reconnectAttempt;
565
+ const failedSocket = this.#lifecycle.kind === "failed" ? this.#lifecycle.connection.socket : null;
566
+ if (this.#lifecycle.kind === "waiting") clearTimeout(this.#lifecycle.timer);
567
+ const connection = this.#startConnection(reconnectAttempt);
568
+ failedSocket?.close();
569
+ return connection;
570
+ }
571
+ #startConnection(reconnectAttempt) {
572
+ const deferred = Promise.withResolvers();
573
+ const opening = { kind: "opening", deferred, reconnectAttempt };
574
+ this.#setLifecycle(opening);
575
+ if (this.#lifecycle !== opening) return deferred.promise;
576
+ let socket;
577
+ try {
578
+ socket = this.#createWebSocket(this.#buildUrl());
579
+ } catch (error) {
580
+ this.#setLifecycle({ kind: "disconnected", reconnectAttempt });
581
+ deferred.reject(error);
582
+ return deferred.promise;
583
+ }
584
+ const connection = { socket };
585
+ const connecting = {
586
+ kind: "connecting",
587
+ connection,
588
+ deferred,
589
+ reconnectAttempt
590
+ };
591
+ this.#lifecycle = connecting;
592
+ const cleanup = () => {
593
+ socket.removeEventListener("open", handleOpen);
594
+ socket.removeEventListener("error", handleError);
595
+ };
596
+ const handleOpen = () => {
597
+ if (this.#lifecycle !== connecting) return;
598
+ cleanup();
599
+ const opened = {
600
+ kind: "opened",
601
+ connection,
602
+ deferred,
603
+ reconnectAttempt
601
604
  };
602
- this.#cancelConnect = cancelConnect;
603
- ws.addEventListener("open", handleOpen);
604
- ws.addEventListener("error", handleError);
605
- ws.addEventListener("message", (event) => {
606
- if (this.#ws === ws) {
607
- this.#handleIncoming(event.data);
608
- }
609
- });
610
- ws.addEventListener("close", handleClose);
605
+ this.#setLifecycle(opened);
606
+ if (!this.#isCurrent(opened)) return;
607
+ this.#lifecycle = { kind: "connected", connection };
608
+ this.#emitLocal("connect");
609
+ if (this.#onConnected) this.#callHandler(this.#onConnected);
610
+ deferred.resolve();
611
+ };
612
+ const handleError = () => {
613
+ if (this.#lifecycle !== connecting) return;
614
+ cleanup();
615
+ deferred.reject(new Error("WebSocket connection failed"));
616
+ this.#setLifecycle({ kind: "failed", connection, reconnectAttempt });
617
+ };
618
+ socket.addEventListener("open", handleOpen);
619
+ socket.addEventListener("error", handleError);
620
+ socket.addEventListener("message", (event) => {
621
+ if (this.#owns(connection)) this.#handleIncoming(event.data);
611
622
  });
612
- this.#connectPromise = connectPromise;
613
- if (setupFailed) {
614
- this.#connectPromise = null;
615
- this.#setState("disconnected");
616
- }
617
- return connectPromise;
623
+ socket.addEventListener("close", (event) => {
624
+ cleanup();
625
+ if (this.#owns(connection)) this.#handleClose(event);
626
+ });
627
+ return deferred.promise;
618
628
  }
619
629
  disconnect() {
620
- this.#manualDisconnect = true;
621
- if (this.#reconnectTimer) {
622
- clearTimeout(this.#reconnectTimer);
623
- this.#reconnectTimer = null;
624
- }
625
- this.#cancelConnect?.(new Error("Socket disconnected"));
626
- this.#reconnectAttempts = 0;
627
- const ws = this.#ws;
628
- const shouldEmitDisconnect = this.#state !== "disconnected";
629
- this.#ws = null;
630
- this.#setState("disconnected");
630
+ const lifecycle = this.#lifecycle;
631
+ const shouldEmitDisconnect = this.state !== "disconnected";
632
+ if (lifecycle.kind === "waiting") clearTimeout(lifecycle.timer);
633
+ if (lifecycle.kind === "opening" || lifecycle.kind === "connecting" || lifecycle.kind === "opened") {
634
+ lifecycle.deferred.reject(new Error("Socket disconnected"));
635
+ }
636
+ this.#setLifecycle({ kind: "disconnected", reconnectAttempt: 0 });
631
637
  if (shouldEmitDisconnect) {
632
638
  this.#emitLocal("disconnect");
633
639
  }
634
- ws?.close();
640
+ if ("connection" in lifecycle) lifecycle.connection.socket.close();
635
641
  this.#rejectPending(new Error("Socket disconnected"));
636
642
  }
637
643
  onStateChange(handler) {
@@ -653,15 +659,15 @@ var ClientSocketSession = class {
653
659
  this.#eventHandlers.get(event)?.delete(handler);
654
660
  }
655
661
  send(message) {
656
- const ws = this.#ws;
657
- if (ws?.readyState !== WebSocket.OPEN) {
662
+ const socket = this.#connection?.socket;
663
+ if (socket?.readyState !== WebSocket.OPEN) {
658
664
  return;
659
665
  }
660
- ws.send(JSON.stringify(message));
666
+ socket.send(JSON.stringify(message));
661
667
  }
662
668
  sendRequest(message, timeout = 5e3) {
663
- const ws = this.#ws;
664
- if (ws?.readyState !== WebSocket.OPEN) {
669
+ const socket = this.#connection?.socket;
670
+ if (socket?.readyState !== WebSocket.OPEN) {
665
671
  return Promise.reject(new Error("Socket is not connected"));
666
672
  }
667
673
  const id = String(++this.#requestId);
@@ -676,7 +682,7 @@ var ClientSocketSession = class {
676
682
  reject,
677
683
  timer
678
684
  });
679
- ws.send(JSON.stringify(payload));
685
+ socket.send(JSON.stringify(payload));
680
686
  });
681
687
  }
682
688
  #handleIncoming(raw) {
@@ -710,7 +716,7 @@ var ClientSocketSession = class {
710
716
  if (!message || typeof message !== "object" || Array.isArray(message)) {
711
717
  return null;
712
718
  }
713
- if (message.type === "ack" && typeof message.id === "string" && typeof message.ok === "boolean" && (message.error === void 0 || typeof message.error === "string")) {
719
+ if (message.type === "ack" && typeof message.id === "string" && (message.ok === true && message.error === void 0 || message.ok === false && typeof message.error === "string" && message.data === void 0)) {
714
720
  return message;
715
721
  }
716
722
  if (message.type === "event" && typeof message.event === "string" && (message.channel === void 0 || typeof message.channel === "string")) {
@@ -737,33 +743,75 @@ var ClientSocketSession = class {
737
743
  pending.resolve({ ok: false, error: message.error });
738
744
  }
739
745
  }
740
- #handleClose() {
741
- const shouldEmitDisconnect = this.#state !== "disconnected";
742
- this.#ws = null;
743
- this.#setState("disconnected");
746
+ #handleClose(event) {
747
+ const lifecycle = this.#lifecycle;
748
+ const reconnectAttempt = "reconnectAttempt" in lifecycle ? lifecycle.reconnectAttempt : 0;
749
+ const shouldEmitDisconnect = this.state !== "disconnected";
750
+ if (lifecycle.kind === "connecting") {
751
+ lifecycle.deferred.reject(new Error("WebSocket connection failed"));
752
+ }
753
+ const disconnected = { kind: "disconnected", reconnectAttempt };
754
+ this.#setLifecycle(disconnected);
744
755
  if (shouldEmitDisconnect) {
745
756
  this.#emitLocal("disconnect");
746
757
  }
747
758
  this.#rejectPending(new Error("Socket disconnected"));
748
- this.#scheduleReconnect();
749
- }
750
- #scheduleReconnect() {
751
- if (this.#manualDisconnect || !this.#autoReconnect || this.#reconnectTimer) {
752
- return;
759
+ if (this.#shouldReconnect(event) && this.#lifecycle === disconnected) {
760
+ this.#scheduleReconnect(disconnected);
753
761
  }
762
+ }
763
+ #scheduleReconnect(disconnected) {
764
+ if (!this.#autoReconnect || this.#lifecycle !== disconnected) return;
754
765
  const delay = Math.min(
755
- this.#reconnectDelay * 2 ** this.#reconnectAttempts,
766
+ this.#reconnectDelay * 2 ** disconnected.reconnectAttempt,
756
767
  this.#reconnectMaxDelay
757
768
  );
758
- this.#reconnectAttempts += 1;
759
- this.#reconnectTimer = setTimeout(() => {
760
- this.#reconnectTimer = null;
761
- this.connect().catch(() => {
762
- if (this.#state === "disconnected") {
763
- this.#scheduleReconnect();
764
- }
765
- });
766
- }, delay);
769
+ const waiting = {
770
+ kind: "waiting",
771
+ reconnectAttempt: disconnected.reconnectAttempt + 1,
772
+ timer: setTimeout(() => {
773
+ if (this.#lifecycle !== waiting) return;
774
+ this.#startConnection(waiting.reconnectAttempt).catch(() => {
775
+ if (this.#lifecycle.kind === "failed") {
776
+ const failed = this.#lifecycle;
777
+ const disconnected2 = {
778
+ kind: "disconnected",
779
+ reconnectAttempt: failed.reconnectAttempt
780
+ };
781
+ this.#setLifecycle(disconnected2);
782
+ failed.connection.socket.close();
783
+ if (this.#isCurrent(disconnected2)) this.#scheduleReconnect(disconnected2);
784
+ return;
785
+ }
786
+ if (this.#lifecycle.kind === "disconnected") {
787
+ this.#scheduleReconnect(this.#lifecycle);
788
+ }
789
+ });
790
+ }, delay)
791
+ };
792
+ this.#setLifecycle(waiting);
793
+ }
794
+ get #connection() {
795
+ const lifecycle = this.#lifecycle;
796
+ return "connection" in lifecycle ? lifecycle.connection : void 0;
797
+ }
798
+ get #reconnectAttempt() {
799
+ const lifecycle = this.#lifecycle;
800
+ return "reconnectAttempt" in lifecycle ? lifecycle.reconnectAttempt : 0;
801
+ }
802
+ #owns(connection) {
803
+ return this.#connection === connection;
804
+ }
805
+ #isCurrent(lifecycle) {
806
+ return this.#lifecycle === lifecycle;
807
+ }
808
+ #setLifecycle(lifecycle) {
809
+ const previousState = this.state;
810
+ this.#lifecycle = lifecycle;
811
+ const state = this.state;
812
+ if (previousState !== state) {
813
+ this.#stateHandlers.forEach((handler) => this.#callHandler(handler, state));
814
+ }
767
815
  }
768
816
  #rejectPending(error) {
769
817
  for (const [id, pending] of this.#pendingRequests) {
@@ -775,13 +823,6 @@ var ClientSocketSession = class {
775
823
  #emitLocal(event, data) {
776
824
  this.#eventHandlers.get(event)?.forEach((handler) => this.#callHandler(handler, data));
777
825
  }
778
- #setState(state) {
779
- if (this.#state === state) {
780
- return;
781
- }
782
- this.#state = state;
783
- this.#stateHandlers.forEach((handler) => this.#callHandler(handler, state));
784
- }
785
826
  #callHandler(handler, data) {
786
827
  callClientHandler(handler, data);
787
828
  }
@@ -811,6 +852,7 @@ var Socket = class {
811
852
  autoReconnect: options.autoReconnect,
812
853
  reconnectDelay: options.reconnectDelay,
813
854
  reconnectMaxDelay: options.reconnectMaxDelay,
855
+ shouldReconnect: options.shouldReconnect,
814
856
  onConnected: () => {
815
857
  void this.#resubscribeChannels();
816
858
  }
@@ -879,6 +921,7 @@ var Socket = class {
879
921
  throw new Error(`Unsupported socket URL protocol: ${url.protocol}`);
880
922
  }
881
923
  url.pathname = this.#path;
924
+ url.hash = "";
882
925
  return url.toString();
883
926
  }
884
927
  async #resubscribeChannels() {
@@ -896,4 +939,4 @@ export {
896
939
  buildChannelName,
897
940
  Socket
898
941
  };
899
- //# sourceMappingURL=chunk-QNOOQXYY.js.map
942
+ //# sourceMappingURL=chunk-DR6GWQUW.js.map