@absolutejs/voice 0.0.22-beta.654 → 0.0.22-beta.656

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.
@@ -593,6 +593,13 @@ var RECONNECT_BASE_DELAY_MS = 500;
593
593
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
594
594
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
595
595
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
596
+
597
+ class VoiceReconnectRejectedError extends Error {
598
+ constructor(message = "Voice authorization could not be renewed.") {
599
+ super(message);
600
+ this.name = "VoiceReconnectRejectedError";
601
+ }
602
+ }
596
603
  var noop = () => {};
597
604
  var noopAsync = () => Promise.resolve();
598
605
  var noopUnsubscribe = () => noop;
@@ -672,6 +679,8 @@ var createVoiceConnection = (path, options = {}) => {
672
679
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
673
680
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
674
681
  const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
682
+ let lifecycleGeneration = 0;
683
+ let disposed = false;
675
684
  const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
676
685
  const state = {
677
686
  isConnected: false,
@@ -687,6 +696,21 @@ var createVoiceConnection = (path, options = {}) => {
687
696
  const emitConnection = (reconnect) => {
688
697
  listeners.forEach((listener) => listener(reconnect));
689
698
  };
699
+ const emitTerminalFailure = (message) => {
700
+ listeners.forEach((listener) => listener({
701
+ message,
702
+ recoverable: false,
703
+ type: "error"
704
+ }));
705
+ emitConnection({
706
+ reconnect: {
707
+ attempts: state.reconnectAttempts,
708
+ maxAttempts: maxReconnectAttempts,
709
+ status: "exhausted"
710
+ },
711
+ type: "connection"
712
+ });
713
+ };
690
714
  const clearTimers = () => {
691
715
  if (state.pingInterval) {
692
716
  clearInterval(state.pingInterval);
@@ -720,6 +744,9 @@ var createVoiceConnection = (path, options = {}) => {
720
744
  pendingCallControls.clear();
721
745
  };
722
746
  const scheduleReconnect = () => {
747
+ if (disposed)
748
+ return;
749
+ const scheduledGeneration = lifecycleGeneration;
723
750
  state.reconnectAttempts += 1;
724
751
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
725
752
  const nextAttemptAt = Date.now() + delayMs;
@@ -734,15 +761,10 @@ var createVoiceConnection = (path, options = {}) => {
734
761
  type: "connection"
735
762
  });
736
763
  state.reconnectTimeout = setTimeout(async () => {
764
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
765
+ return;
737
766
  if (state.reconnectAttempts > maxReconnectAttempts) {
738
- emitConnection({
739
- reconnect: {
740
- attempts: state.reconnectAttempts,
741
- maxAttempts: maxReconnectAttempts,
742
- status: "exhausted"
743
- },
744
- type: "connection"
745
- });
767
+ emitTerminalFailure("Voice connection could not be restored.");
746
768
  return;
747
769
  }
748
770
  try {
@@ -752,25 +774,28 @@ var createVoiceConnection = (path, options = {}) => {
752
774
  scenarioId: state.scenarioId,
753
775
  sessionId: state.sessionId
754
776
  });
755
- } catch {
777
+ } catch (error) {
778
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
779
+ return;
780
+ if (error instanceof VoiceReconnectRejectedError) {
781
+ emitTerminalFailure(error.message);
782
+ return;
783
+ }
756
784
  if (state.reconnectAttempts < maxReconnectAttempts) {
757
785
  scheduleReconnect();
758
786
  } else {
759
- emitConnection({
760
- reconnect: {
761
- attempts: state.reconnectAttempts,
762
- maxAttempts: maxReconnectAttempts,
763
- status: "exhausted"
764
- },
765
- type: "connection"
766
- });
787
+ emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
767
788
  }
768
789
  return;
769
790
  }
791
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
792
+ return;
770
793
  connect();
771
794
  }, delayMs);
772
795
  };
773
796
  const connect = () => {
797
+ if (disposed)
798
+ return;
774
799
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
775
800
  ws.binaryType = "arraybuffer";
776
801
  ws.onopen = () => {
@@ -841,6 +866,7 @@ var createVoiceConnection = (path, options = {}) => {
841
866
  state.ws = null;
842
867
  state.isConnected = false;
843
868
  clearTimers();
869
+ rejectPendingCallControls("Voice connection interrupted");
844
870
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
845
871
  const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
846
872
  if (authorizationFailure) {
@@ -853,15 +879,19 @@ var createVoiceConnection = (path, options = {}) => {
853
879
  if (reconnectable) {
854
880
  scheduleReconnect();
855
881
  } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
856
- emitConnection({
857
- reconnect: {
858
- attempts: state.reconnectAttempts,
859
- lastDisconnectAt: Date.now(),
860
- maxAttempts: maxReconnectAttempts,
861
- status: "exhausted"
862
- },
863
- type: "connection"
864
- });
882
+ if (authorizationFailure) {
883
+ emitConnection({
884
+ reconnect: {
885
+ attempts: state.reconnectAttempts,
886
+ lastDisconnectAt: Date.now(),
887
+ maxAttempts: maxReconnectAttempts,
888
+ status: "exhausted"
889
+ },
890
+ type: "connection"
891
+ });
892
+ } else {
893
+ emitTerminalFailure("Voice connection could not be restored.");
894
+ }
865
895
  }
866
896
  };
867
897
  state.ws = ws;
@@ -902,6 +932,9 @@ var createVoiceConnection = (path, options = {}) => {
902
932
  send({ type: "end_turn" });
903
933
  };
904
934
  const callControl = (message) => {
935
+ if (state.ws?.readyState !== WS_OPEN) {
936
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
937
+ }
905
938
  const requestId = crypto.randomUUID();
906
939
  const { promise, reject, resolve } = Promise.withResolvers();
907
940
  const timeout = setTimeout(() => {
@@ -909,16 +942,19 @@ var createVoiceConnection = (path, options = {}) => {
909
942
  reject(new Error(`${message.action} acknowledgement timed out`));
910
943
  }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
911
944
  pendingCallControls.set(requestId, { reject, resolve, timeout });
912
- send({
945
+ state.ws.send(JSON.stringify({
913
946
  ...message,
914
947
  requestId,
915
948
  type: "call_control"
916
- });
949
+ }));
917
950
  return promise;
918
951
  };
919
952
  const close = (reason = "client-close") => {
953
+ disposed = true;
954
+ lifecycleGeneration += 1;
920
955
  clearTimers();
921
956
  rejectPendingCallControls("Voice connection closed");
957
+ state.pendingMessages.length = 0;
922
958
  if (state.ws) {
923
959
  const ws = state.ws;
924
960
  state.ws = null;
@@ -931,8 +967,11 @@ var createVoiceConnection = (path, options = {}) => {
931
967
  listeners.clear();
932
968
  };
933
969
  const disconnect = () => {
970
+ disposed = true;
971
+ lifecycleGeneration += 1;
934
972
  clearTimers();
935
973
  rejectPendingCallControls("Voice connection disconnected");
974
+ state.pendingMessages.length = 0;
936
975
  if (state.ws) {
937
976
  const ws = state.ws;
938
977
  state.ws = null;
@@ -22,5 +22,8 @@ type VoiceConnectionHandle = {
22
22
  simulateDisconnect: () => void;
23
23
  subscribe: (callback: (message: VoiceServerMessage) => void) => () => void;
24
24
  };
25
+ export declare class VoiceReconnectRejectedError extends Error {
26
+ constructor(message?: string);
27
+ }
25
28
  export declare const createVoiceConnection: (path: string, options?: VoiceConnectionOptions) => VoiceConnectionHandle;
26
29
  export {};
@@ -510,6 +510,13 @@ var RECONNECT_BASE_DELAY_MS = 500;
510
510
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
511
511
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
512
512
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
513
+
514
+ class VoiceReconnectRejectedError extends Error {
515
+ constructor(message = "Voice authorization could not be renewed.") {
516
+ super(message);
517
+ this.name = "VoiceReconnectRejectedError";
518
+ }
519
+ }
513
520
  var noop = () => {};
514
521
  var noopAsync = () => Promise.resolve();
515
522
  var noopUnsubscribe = () => noop;
@@ -589,6 +596,8 @@ var createVoiceConnection = (path, options = {}) => {
589
596
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
590
597
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
591
598
  const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
599
+ let lifecycleGeneration = 0;
600
+ let disposed = false;
592
601
  const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
593
602
  const state = {
594
603
  isConnected: false,
@@ -604,6 +613,21 @@ var createVoiceConnection = (path, options = {}) => {
604
613
  const emitConnection = (reconnect) => {
605
614
  listeners.forEach((listener) => listener(reconnect));
606
615
  };
616
+ const emitTerminalFailure = (message) => {
617
+ listeners.forEach((listener) => listener({
618
+ message,
619
+ recoverable: false,
620
+ type: "error"
621
+ }));
622
+ emitConnection({
623
+ reconnect: {
624
+ attempts: state.reconnectAttempts,
625
+ maxAttempts: maxReconnectAttempts,
626
+ status: "exhausted"
627
+ },
628
+ type: "connection"
629
+ });
630
+ };
607
631
  const clearTimers = () => {
608
632
  if (state.pingInterval) {
609
633
  clearInterval(state.pingInterval);
@@ -637,6 +661,9 @@ var createVoiceConnection = (path, options = {}) => {
637
661
  pendingCallControls.clear();
638
662
  };
639
663
  const scheduleReconnect = () => {
664
+ if (disposed)
665
+ return;
666
+ const scheduledGeneration = lifecycleGeneration;
640
667
  state.reconnectAttempts += 1;
641
668
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
642
669
  const nextAttemptAt = Date.now() + delayMs;
@@ -651,15 +678,10 @@ var createVoiceConnection = (path, options = {}) => {
651
678
  type: "connection"
652
679
  });
653
680
  state.reconnectTimeout = setTimeout(async () => {
681
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
682
+ return;
654
683
  if (state.reconnectAttempts > maxReconnectAttempts) {
655
- emitConnection({
656
- reconnect: {
657
- attempts: state.reconnectAttempts,
658
- maxAttempts: maxReconnectAttempts,
659
- status: "exhausted"
660
- },
661
- type: "connection"
662
- });
684
+ emitTerminalFailure("Voice connection could not be restored.");
663
685
  return;
664
686
  }
665
687
  try {
@@ -669,25 +691,28 @@ var createVoiceConnection = (path, options = {}) => {
669
691
  scenarioId: state.scenarioId,
670
692
  sessionId: state.sessionId
671
693
  });
672
- } catch {
694
+ } catch (error) {
695
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
696
+ return;
697
+ if (error instanceof VoiceReconnectRejectedError) {
698
+ emitTerminalFailure(error.message);
699
+ return;
700
+ }
673
701
  if (state.reconnectAttempts < maxReconnectAttempts) {
674
702
  scheduleReconnect();
675
703
  } else {
676
- emitConnection({
677
- reconnect: {
678
- attempts: state.reconnectAttempts,
679
- maxAttempts: maxReconnectAttempts,
680
- status: "exhausted"
681
- },
682
- type: "connection"
683
- });
704
+ emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
684
705
  }
685
706
  return;
686
707
  }
708
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
709
+ return;
687
710
  connect();
688
711
  }, delayMs);
689
712
  };
690
713
  const connect = () => {
714
+ if (disposed)
715
+ return;
691
716
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
692
717
  ws.binaryType = "arraybuffer";
693
718
  ws.onopen = () => {
@@ -758,6 +783,7 @@ var createVoiceConnection = (path, options = {}) => {
758
783
  state.ws = null;
759
784
  state.isConnected = false;
760
785
  clearTimers();
786
+ rejectPendingCallControls("Voice connection interrupted");
761
787
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
762
788
  const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
763
789
  if (authorizationFailure) {
@@ -770,15 +796,19 @@ var createVoiceConnection = (path, options = {}) => {
770
796
  if (reconnectable) {
771
797
  scheduleReconnect();
772
798
  } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
773
- emitConnection({
774
- reconnect: {
775
- attempts: state.reconnectAttempts,
776
- lastDisconnectAt: Date.now(),
777
- maxAttempts: maxReconnectAttempts,
778
- status: "exhausted"
779
- },
780
- type: "connection"
781
- });
799
+ if (authorizationFailure) {
800
+ emitConnection({
801
+ reconnect: {
802
+ attempts: state.reconnectAttempts,
803
+ lastDisconnectAt: Date.now(),
804
+ maxAttempts: maxReconnectAttempts,
805
+ status: "exhausted"
806
+ },
807
+ type: "connection"
808
+ });
809
+ } else {
810
+ emitTerminalFailure("Voice connection could not be restored.");
811
+ }
782
812
  }
783
813
  };
784
814
  state.ws = ws;
@@ -819,6 +849,9 @@ var createVoiceConnection = (path, options = {}) => {
819
849
  send({ type: "end_turn" });
820
850
  };
821
851
  const callControl = (message) => {
852
+ if (state.ws?.readyState !== WS_OPEN) {
853
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
854
+ }
822
855
  const requestId = crypto.randomUUID();
823
856
  const { promise, reject, resolve } = Promise.withResolvers();
824
857
  const timeout = setTimeout(() => {
@@ -826,16 +859,19 @@ var createVoiceConnection = (path, options = {}) => {
826
859
  reject(new Error(`${message.action} acknowledgement timed out`));
827
860
  }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
828
861
  pendingCallControls.set(requestId, { reject, resolve, timeout });
829
- send({
862
+ state.ws.send(JSON.stringify({
830
863
  ...message,
831
864
  requestId,
832
865
  type: "call_control"
833
- });
866
+ }));
834
867
  return promise;
835
868
  };
836
869
  const close = (reason = "client-close") => {
870
+ disposed = true;
871
+ lifecycleGeneration += 1;
837
872
  clearTimers();
838
873
  rejectPendingCallControls("Voice connection closed");
874
+ state.pendingMessages.length = 0;
839
875
  if (state.ws) {
840
876
  const ws = state.ws;
841
877
  state.ws = null;
@@ -848,8 +884,11 @@ var createVoiceConnection = (path, options = {}) => {
848
884
  listeners.clear();
849
885
  };
850
886
  const disconnect = () => {
887
+ disposed = true;
888
+ lifecycleGeneration += 1;
851
889
  clearTimers();
852
890
  rejectPendingCallControls("Voice connection disconnected");
891
+ state.pendingMessages.length = 0;
853
892
  if (state.ws) {
854
893
  const ws = state.ws;
855
894
  state.ws = null;
@@ -1,6 +1,6 @@
1
1
  export { bindVoiceReactiveSource, voiceSseReactiveSource, } from "./reactiveSource";
2
2
  export type { VoiceReactiveSource, VoiceSseReactiveSourceOptions, } from "./reactiveSource";
3
- export { createVoiceConnection } from "./connection";
3
+ export { createVoiceConnection, VoiceReconnectRejectedError, } from "./connection";
4
4
  export { checkBrowserVoiceSupport, probeBrowserVoiceSupport, } from "./browserVoiceSupport";
5
5
  export type { BrowserVoiceSupportProbe, BrowserVoiceSupportReport, } from "./browserVoiceSupport";
6
6
  export { createVoiceAudioPlayer, decodeVoiceAudioChunk } from "./audioPlayer";
@@ -115,6 +115,13 @@ var RECONNECT_BASE_DELAY_MS = 500;
115
115
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
116
116
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
117
117
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
118
+
119
+ class VoiceReconnectRejectedError extends Error {
120
+ constructor(message = "Voice authorization could not be renewed.") {
121
+ super(message);
122
+ this.name = "VoiceReconnectRejectedError";
123
+ }
124
+ }
118
125
  var noop = () => {};
119
126
  var noopAsync = () => Promise.resolve();
120
127
  var noopUnsubscribe = () => noop;
@@ -194,6 +201,8 @@ var createVoiceConnection = (path, options = {}) => {
194
201
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
195
202
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
196
203
  const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
204
+ let lifecycleGeneration = 0;
205
+ let disposed = false;
197
206
  const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
198
207
  const state = {
199
208
  isConnected: false,
@@ -209,6 +218,21 @@ var createVoiceConnection = (path, options = {}) => {
209
218
  const emitConnection = (reconnect) => {
210
219
  listeners.forEach((listener) => listener(reconnect));
211
220
  };
221
+ const emitTerminalFailure = (message) => {
222
+ listeners.forEach((listener) => listener({
223
+ message,
224
+ recoverable: false,
225
+ type: "error"
226
+ }));
227
+ emitConnection({
228
+ reconnect: {
229
+ attempts: state.reconnectAttempts,
230
+ maxAttempts: maxReconnectAttempts,
231
+ status: "exhausted"
232
+ },
233
+ type: "connection"
234
+ });
235
+ };
212
236
  const clearTimers = () => {
213
237
  if (state.pingInterval) {
214
238
  clearInterval(state.pingInterval);
@@ -242,6 +266,9 @@ var createVoiceConnection = (path, options = {}) => {
242
266
  pendingCallControls.clear();
243
267
  };
244
268
  const scheduleReconnect = () => {
269
+ if (disposed)
270
+ return;
271
+ const scheduledGeneration = lifecycleGeneration;
245
272
  state.reconnectAttempts += 1;
246
273
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
247
274
  const nextAttemptAt = Date.now() + delayMs;
@@ -256,15 +283,10 @@ var createVoiceConnection = (path, options = {}) => {
256
283
  type: "connection"
257
284
  });
258
285
  state.reconnectTimeout = setTimeout(async () => {
286
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
287
+ return;
259
288
  if (state.reconnectAttempts > maxReconnectAttempts) {
260
- emitConnection({
261
- reconnect: {
262
- attempts: state.reconnectAttempts,
263
- maxAttempts: maxReconnectAttempts,
264
- status: "exhausted"
265
- },
266
- type: "connection"
267
- });
289
+ emitTerminalFailure("Voice connection could not be restored.");
268
290
  return;
269
291
  }
270
292
  try {
@@ -274,25 +296,28 @@ var createVoiceConnection = (path, options = {}) => {
274
296
  scenarioId: state.scenarioId,
275
297
  sessionId: state.sessionId
276
298
  });
277
- } catch {
299
+ } catch (error) {
300
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
301
+ return;
302
+ if (error instanceof VoiceReconnectRejectedError) {
303
+ emitTerminalFailure(error.message);
304
+ return;
305
+ }
278
306
  if (state.reconnectAttempts < maxReconnectAttempts) {
279
307
  scheduleReconnect();
280
308
  } else {
281
- emitConnection({
282
- reconnect: {
283
- attempts: state.reconnectAttempts,
284
- maxAttempts: maxReconnectAttempts,
285
- status: "exhausted"
286
- },
287
- type: "connection"
288
- });
309
+ emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
289
310
  }
290
311
  return;
291
312
  }
313
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
314
+ return;
292
315
  connect();
293
316
  }, delayMs);
294
317
  };
295
318
  const connect = () => {
319
+ if (disposed)
320
+ return;
296
321
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
297
322
  ws.binaryType = "arraybuffer";
298
323
  ws.onopen = () => {
@@ -363,6 +388,7 @@ var createVoiceConnection = (path, options = {}) => {
363
388
  state.ws = null;
364
389
  state.isConnected = false;
365
390
  clearTimers();
391
+ rejectPendingCallControls("Voice connection interrupted");
366
392
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
367
393
  const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
368
394
  if (authorizationFailure) {
@@ -375,15 +401,19 @@ var createVoiceConnection = (path, options = {}) => {
375
401
  if (reconnectable) {
376
402
  scheduleReconnect();
377
403
  } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
378
- emitConnection({
379
- reconnect: {
380
- attempts: state.reconnectAttempts,
381
- lastDisconnectAt: Date.now(),
382
- maxAttempts: maxReconnectAttempts,
383
- status: "exhausted"
384
- },
385
- type: "connection"
386
- });
404
+ if (authorizationFailure) {
405
+ emitConnection({
406
+ reconnect: {
407
+ attempts: state.reconnectAttempts,
408
+ lastDisconnectAt: Date.now(),
409
+ maxAttempts: maxReconnectAttempts,
410
+ status: "exhausted"
411
+ },
412
+ type: "connection"
413
+ });
414
+ } else {
415
+ emitTerminalFailure("Voice connection could not be restored.");
416
+ }
387
417
  }
388
418
  };
389
419
  state.ws = ws;
@@ -424,6 +454,9 @@ var createVoiceConnection = (path, options = {}) => {
424
454
  send({ type: "end_turn" });
425
455
  };
426
456
  const callControl = (message) => {
457
+ if (state.ws?.readyState !== WS_OPEN) {
458
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
459
+ }
427
460
  const requestId = crypto.randomUUID();
428
461
  const { promise, reject, resolve } = Promise.withResolvers();
429
462
  const timeout = setTimeout(() => {
@@ -431,16 +464,19 @@ var createVoiceConnection = (path, options = {}) => {
431
464
  reject(new Error(`${message.action} acknowledgement timed out`));
432
465
  }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
433
466
  pendingCallControls.set(requestId, { reject, resolve, timeout });
434
- send({
467
+ state.ws.send(JSON.stringify({
435
468
  ...message,
436
469
  requestId,
437
470
  type: "call_control"
438
- });
471
+ }));
439
472
  return promise;
440
473
  };
441
474
  const close = (reason = "client-close") => {
475
+ disposed = true;
476
+ lifecycleGeneration += 1;
442
477
  clearTimers();
443
478
  rejectPendingCallControls("Voice connection closed");
479
+ state.pendingMessages.length = 0;
444
480
  if (state.ws) {
445
481
  const ws = state.ws;
446
482
  state.ws = null;
@@ -453,8 +489,11 @@ var createVoiceConnection = (path, options = {}) => {
453
489
  listeners.clear();
454
490
  };
455
491
  const disconnect = () => {
492
+ disposed = true;
493
+ lifecycleGeneration += 1;
456
494
  clearTimers();
457
495
  rejectPendingCallControls("Voice connection disconnected");
496
+ state.pendingMessages.length = 0;
458
497
  if (state.ws) {
459
498
  const ws = state.ws;
460
499
  state.ws = null;
@@ -12835,5 +12874,6 @@ export {
12835
12874
  bindVoiceReactiveSource,
12836
12875
  bindVoiceProviderSimulationControls,
12837
12876
  bindVoiceHTMX,
12838
- bindVoiceBargeIn
12877
+ bindVoiceBargeIn,
12878
+ VoiceReconnectRejectedError
12839
12879
  };