@absolutejs/voice 0.0.22-beta.655 → 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,
@@ -735,6 +744,9 @@ var createVoiceConnection = (path, options = {}) => {
735
744
  pendingCallControls.clear();
736
745
  };
737
746
  const scheduleReconnect = () => {
747
+ if (disposed)
748
+ return;
749
+ const scheduledGeneration = lifecycleGeneration;
738
750
  state.reconnectAttempts += 1;
739
751
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
740
752
  const nextAttemptAt = Date.now() + delayMs;
@@ -749,6 +761,8 @@ var createVoiceConnection = (path, options = {}) => {
749
761
  type: "connection"
750
762
  });
751
763
  state.reconnectTimeout = setTimeout(async () => {
764
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
765
+ return;
752
766
  if (state.reconnectAttempts > maxReconnectAttempts) {
753
767
  emitTerminalFailure("Voice connection could not be restored.");
754
768
  return;
@@ -760,7 +774,13 @@ var createVoiceConnection = (path, options = {}) => {
760
774
  scenarioId: state.scenarioId,
761
775
  sessionId: state.sessionId
762
776
  });
763
- } catch {
777
+ } catch (error) {
778
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
779
+ return;
780
+ if (error instanceof VoiceReconnectRejectedError) {
781
+ emitTerminalFailure(error.message);
782
+ return;
783
+ }
764
784
  if (state.reconnectAttempts < maxReconnectAttempts) {
765
785
  scheduleReconnect();
766
786
  } else {
@@ -768,10 +788,14 @@ var createVoiceConnection = (path, options = {}) => {
768
788
  }
769
789
  return;
770
790
  }
791
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
792
+ return;
771
793
  connect();
772
794
  }, delayMs);
773
795
  };
774
796
  const connect = () => {
797
+ if (disposed)
798
+ return;
775
799
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
776
800
  ws.binaryType = "arraybuffer";
777
801
  ws.onopen = () => {
@@ -842,6 +866,7 @@ var createVoiceConnection = (path, options = {}) => {
842
866
  state.ws = null;
843
867
  state.isConnected = false;
844
868
  clearTimers();
869
+ rejectPendingCallControls("Voice connection interrupted");
845
870
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
846
871
  const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
847
872
  if (authorizationFailure) {
@@ -907,6 +932,9 @@ var createVoiceConnection = (path, options = {}) => {
907
932
  send({ type: "end_turn" });
908
933
  };
909
934
  const callControl = (message) => {
935
+ if (state.ws?.readyState !== WS_OPEN) {
936
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
937
+ }
910
938
  const requestId = crypto.randomUUID();
911
939
  const { promise, reject, resolve } = Promise.withResolvers();
912
940
  const timeout = setTimeout(() => {
@@ -914,16 +942,19 @@ var createVoiceConnection = (path, options = {}) => {
914
942
  reject(new Error(`${message.action} acknowledgement timed out`));
915
943
  }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
916
944
  pendingCallControls.set(requestId, { reject, resolve, timeout });
917
- send({
945
+ state.ws.send(JSON.stringify({
918
946
  ...message,
919
947
  requestId,
920
948
  type: "call_control"
921
- });
949
+ }));
922
950
  return promise;
923
951
  };
924
952
  const close = (reason = "client-close") => {
953
+ disposed = true;
954
+ lifecycleGeneration += 1;
925
955
  clearTimers();
926
956
  rejectPendingCallControls("Voice connection closed");
957
+ state.pendingMessages.length = 0;
927
958
  if (state.ws) {
928
959
  const ws = state.ws;
929
960
  state.ws = null;
@@ -936,8 +967,11 @@ var createVoiceConnection = (path, options = {}) => {
936
967
  listeners.clear();
937
968
  };
938
969
  const disconnect = () => {
970
+ disposed = true;
971
+ lifecycleGeneration += 1;
939
972
  clearTimers();
940
973
  rejectPendingCallControls("Voice connection disconnected");
974
+ state.pendingMessages.length = 0;
941
975
  if (state.ws) {
942
976
  const ws = state.ws;
943
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,
@@ -652,6 +661,9 @@ var createVoiceConnection = (path, options = {}) => {
652
661
  pendingCallControls.clear();
653
662
  };
654
663
  const scheduleReconnect = () => {
664
+ if (disposed)
665
+ return;
666
+ const scheduledGeneration = lifecycleGeneration;
655
667
  state.reconnectAttempts += 1;
656
668
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
657
669
  const nextAttemptAt = Date.now() + delayMs;
@@ -666,6 +678,8 @@ var createVoiceConnection = (path, options = {}) => {
666
678
  type: "connection"
667
679
  });
668
680
  state.reconnectTimeout = setTimeout(async () => {
681
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
682
+ return;
669
683
  if (state.reconnectAttempts > maxReconnectAttempts) {
670
684
  emitTerminalFailure("Voice connection could not be restored.");
671
685
  return;
@@ -677,7 +691,13 @@ var createVoiceConnection = (path, options = {}) => {
677
691
  scenarioId: state.scenarioId,
678
692
  sessionId: state.sessionId
679
693
  });
680
- } catch {
694
+ } catch (error) {
695
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
696
+ return;
697
+ if (error instanceof VoiceReconnectRejectedError) {
698
+ emitTerminalFailure(error.message);
699
+ return;
700
+ }
681
701
  if (state.reconnectAttempts < maxReconnectAttempts) {
682
702
  scheduleReconnect();
683
703
  } else {
@@ -685,10 +705,14 @@ var createVoiceConnection = (path, options = {}) => {
685
705
  }
686
706
  return;
687
707
  }
708
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
709
+ return;
688
710
  connect();
689
711
  }, delayMs);
690
712
  };
691
713
  const connect = () => {
714
+ if (disposed)
715
+ return;
692
716
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
693
717
  ws.binaryType = "arraybuffer";
694
718
  ws.onopen = () => {
@@ -759,6 +783,7 @@ var createVoiceConnection = (path, options = {}) => {
759
783
  state.ws = null;
760
784
  state.isConnected = false;
761
785
  clearTimers();
786
+ rejectPendingCallControls("Voice connection interrupted");
762
787
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
763
788
  const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
764
789
  if (authorizationFailure) {
@@ -824,6 +849,9 @@ var createVoiceConnection = (path, options = {}) => {
824
849
  send({ type: "end_turn" });
825
850
  };
826
851
  const callControl = (message) => {
852
+ if (state.ws?.readyState !== WS_OPEN) {
853
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
854
+ }
827
855
  const requestId = crypto.randomUUID();
828
856
  const { promise, reject, resolve } = Promise.withResolvers();
829
857
  const timeout = setTimeout(() => {
@@ -831,16 +859,19 @@ var createVoiceConnection = (path, options = {}) => {
831
859
  reject(new Error(`${message.action} acknowledgement timed out`));
832
860
  }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
833
861
  pendingCallControls.set(requestId, { reject, resolve, timeout });
834
- send({
862
+ state.ws.send(JSON.stringify({
835
863
  ...message,
836
864
  requestId,
837
865
  type: "call_control"
838
- });
866
+ }));
839
867
  return promise;
840
868
  };
841
869
  const close = (reason = "client-close") => {
870
+ disposed = true;
871
+ lifecycleGeneration += 1;
842
872
  clearTimers();
843
873
  rejectPendingCallControls("Voice connection closed");
874
+ state.pendingMessages.length = 0;
844
875
  if (state.ws) {
845
876
  const ws = state.ws;
846
877
  state.ws = null;
@@ -853,8 +884,11 @@ var createVoiceConnection = (path, options = {}) => {
853
884
  listeners.clear();
854
885
  };
855
886
  const disconnect = () => {
887
+ disposed = true;
888
+ lifecycleGeneration += 1;
856
889
  clearTimers();
857
890
  rejectPendingCallControls("Voice connection disconnected");
891
+ state.pendingMessages.length = 0;
858
892
  if (state.ws) {
859
893
  const ws = state.ws;
860
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,
@@ -257,6 +266,9 @@ var createVoiceConnection = (path, options = {}) => {
257
266
  pendingCallControls.clear();
258
267
  };
259
268
  const scheduleReconnect = () => {
269
+ if (disposed)
270
+ return;
271
+ const scheduledGeneration = lifecycleGeneration;
260
272
  state.reconnectAttempts += 1;
261
273
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
262
274
  const nextAttemptAt = Date.now() + delayMs;
@@ -271,6 +283,8 @@ var createVoiceConnection = (path, options = {}) => {
271
283
  type: "connection"
272
284
  });
273
285
  state.reconnectTimeout = setTimeout(async () => {
286
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
287
+ return;
274
288
  if (state.reconnectAttempts > maxReconnectAttempts) {
275
289
  emitTerminalFailure("Voice connection could not be restored.");
276
290
  return;
@@ -282,7 +296,13 @@ var createVoiceConnection = (path, options = {}) => {
282
296
  scenarioId: state.scenarioId,
283
297
  sessionId: state.sessionId
284
298
  });
285
- } catch {
299
+ } catch (error) {
300
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
301
+ return;
302
+ if (error instanceof VoiceReconnectRejectedError) {
303
+ emitTerminalFailure(error.message);
304
+ return;
305
+ }
286
306
  if (state.reconnectAttempts < maxReconnectAttempts) {
287
307
  scheduleReconnect();
288
308
  } else {
@@ -290,10 +310,14 @@ var createVoiceConnection = (path, options = {}) => {
290
310
  }
291
311
  return;
292
312
  }
313
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
314
+ return;
293
315
  connect();
294
316
  }, delayMs);
295
317
  };
296
318
  const connect = () => {
319
+ if (disposed)
320
+ return;
297
321
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
298
322
  ws.binaryType = "arraybuffer";
299
323
  ws.onopen = () => {
@@ -364,6 +388,7 @@ var createVoiceConnection = (path, options = {}) => {
364
388
  state.ws = null;
365
389
  state.isConnected = false;
366
390
  clearTimers();
391
+ rejectPendingCallControls("Voice connection interrupted");
367
392
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
368
393
  const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
369
394
  if (authorizationFailure) {
@@ -429,6 +454,9 @@ var createVoiceConnection = (path, options = {}) => {
429
454
  send({ type: "end_turn" });
430
455
  };
431
456
  const callControl = (message) => {
457
+ if (state.ws?.readyState !== WS_OPEN) {
458
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
459
+ }
432
460
  const requestId = crypto.randomUUID();
433
461
  const { promise, reject, resolve } = Promise.withResolvers();
434
462
  const timeout = setTimeout(() => {
@@ -436,16 +464,19 @@ var createVoiceConnection = (path, options = {}) => {
436
464
  reject(new Error(`${message.action} acknowledgement timed out`));
437
465
  }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
438
466
  pendingCallControls.set(requestId, { reject, resolve, timeout });
439
- send({
467
+ state.ws.send(JSON.stringify({
440
468
  ...message,
441
469
  requestId,
442
470
  type: "call_control"
443
- });
471
+ }));
444
472
  return promise;
445
473
  };
446
474
  const close = (reason = "client-close") => {
475
+ disposed = true;
476
+ lifecycleGeneration += 1;
447
477
  clearTimers();
448
478
  rejectPendingCallControls("Voice connection closed");
479
+ state.pendingMessages.length = 0;
449
480
  if (state.ws) {
450
481
  const ws = state.ws;
451
482
  state.ws = null;
@@ -458,8 +489,11 @@ var createVoiceConnection = (path, options = {}) => {
458
489
  listeners.clear();
459
490
  };
460
491
  const disconnect = () => {
492
+ disposed = true;
493
+ lifecycleGeneration += 1;
461
494
  clearTimers();
462
495
  rejectPendingCallControls("Voice connection disconnected");
496
+ state.pendingMessages.length = 0;
463
497
  if (state.ws) {
464
498
  const ws = state.ws;
465
499
  state.ws = null;
@@ -12840,5 +12874,6 @@ export {
12840
12874
  bindVoiceReactiveSource,
12841
12875
  bindVoiceProviderSimulationControls,
12842
12876
  bindVoiceHTMX,
12843
- bindVoiceBargeIn
12877
+ bindVoiceBargeIn,
12878
+ VoiceReconnectRejectedError
12844
12879
  };
@@ -507,6 +507,13 @@ var RECONNECT_BASE_DELAY_MS = 500;
507
507
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
508
508
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
509
509
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
510
+
511
+ class VoiceReconnectRejectedError extends Error {
512
+ constructor(message = "Voice authorization could not be renewed.") {
513
+ super(message);
514
+ this.name = "VoiceReconnectRejectedError";
515
+ }
516
+ }
510
517
  var noop = () => {};
511
518
  var noopAsync = () => Promise.resolve();
512
519
  var noopUnsubscribe = () => noop;
@@ -586,6 +593,8 @@ var createVoiceConnection = (path, options = {}) => {
586
593
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
587
594
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
588
595
  const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
596
+ let lifecycleGeneration = 0;
597
+ let disposed = false;
589
598
  const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
590
599
  const state = {
591
600
  isConnected: false,
@@ -649,6 +658,9 @@ var createVoiceConnection = (path, options = {}) => {
649
658
  pendingCallControls.clear();
650
659
  };
651
660
  const scheduleReconnect = () => {
661
+ if (disposed)
662
+ return;
663
+ const scheduledGeneration = lifecycleGeneration;
652
664
  state.reconnectAttempts += 1;
653
665
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
654
666
  const nextAttemptAt = Date.now() + delayMs;
@@ -663,6 +675,8 @@ var createVoiceConnection = (path, options = {}) => {
663
675
  type: "connection"
664
676
  });
665
677
  state.reconnectTimeout = setTimeout(async () => {
678
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
679
+ return;
666
680
  if (state.reconnectAttempts > maxReconnectAttempts) {
667
681
  emitTerminalFailure("Voice connection could not be restored.");
668
682
  return;
@@ -674,7 +688,13 @@ var createVoiceConnection = (path, options = {}) => {
674
688
  scenarioId: state.scenarioId,
675
689
  sessionId: state.sessionId
676
690
  });
677
- } catch {
691
+ } catch (error) {
692
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
693
+ return;
694
+ if (error instanceof VoiceReconnectRejectedError) {
695
+ emitTerminalFailure(error.message);
696
+ return;
697
+ }
678
698
  if (state.reconnectAttempts < maxReconnectAttempts) {
679
699
  scheduleReconnect();
680
700
  } else {
@@ -682,10 +702,14 @@ var createVoiceConnection = (path, options = {}) => {
682
702
  }
683
703
  return;
684
704
  }
705
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
706
+ return;
685
707
  connect();
686
708
  }, delayMs);
687
709
  };
688
710
  const connect = () => {
711
+ if (disposed)
712
+ return;
689
713
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
690
714
  ws.binaryType = "arraybuffer";
691
715
  ws.onopen = () => {
@@ -756,6 +780,7 @@ var createVoiceConnection = (path, options = {}) => {
756
780
  state.ws = null;
757
781
  state.isConnected = false;
758
782
  clearTimers();
783
+ rejectPendingCallControls("Voice connection interrupted");
759
784
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
760
785
  const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
761
786
  if (authorizationFailure) {
@@ -821,6 +846,9 @@ var createVoiceConnection = (path, options = {}) => {
821
846
  send({ type: "end_turn" });
822
847
  };
823
848
  const callControl = (message) => {
849
+ if (state.ws?.readyState !== WS_OPEN) {
850
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
851
+ }
824
852
  const requestId = crypto.randomUUID();
825
853
  const { promise, reject, resolve } = Promise.withResolvers();
826
854
  const timeout = setTimeout(() => {
@@ -828,16 +856,19 @@ var createVoiceConnection = (path, options = {}) => {
828
856
  reject(new Error(`${message.action} acknowledgement timed out`));
829
857
  }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
830
858
  pendingCallControls.set(requestId, { reject, resolve, timeout });
831
- send({
859
+ state.ws.send(JSON.stringify({
832
860
  ...message,
833
861
  requestId,
834
862
  type: "call_control"
835
- });
863
+ }));
836
864
  return promise;
837
865
  };
838
866
  const close = (reason = "client-close") => {
867
+ disposed = true;
868
+ lifecycleGeneration += 1;
839
869
  clearTimers();
840
870
  rejectPendingCallControls("Voice connection closed");
871
+ state.pendingMessages.length = 0;
841
872
  if (state.ws) {
842
873
  const ws = state.ws;
843
874
  state.ws = null;
@@ -850,8 +881,11 @@ var createVoiceConnection = (path, options = {}) => {
850
881
  listeners.clear();
851
882
  };
852
883
  const disconnect = () => {
884
+ disposed = true;
885
+ lifecycleGeneration += 1;
853
886
  clearTimers();
854
887
  rejectPendingCallControls("Voice connection disconnected");
888
+ state.pendingMessages.length = 0;
855
889
  if (state.ws) {
856
890
  const ws = state.ws;
857
891
  state.ws = null;
@@ -1,10 +1,10 @@
1
- (()=>{var{defineProperty:x,getOwnPropertyNames:L0,getOwnPropertyDescriptor:G0}=Object,W0=Object.prototype.hasOwnProperty;function D0(g){return this[g]}var S0=(g)=>{var A=(f??=new WeakMap).get(g),C;if(A)return A;if(A=x({},"__esModule",{value:!0}),g&&typeof g==="object"||typeof g==="function"){for(var V of L0(g))if(!W0.call(A,V))x(A,V,{get:D0.bind(g,V),enumerable:!(C=G0(g,V))||C.enumerable})}return f.set(g,A),A},f;var y0=(g)=>g;function X0(g,A){this[g]=y0.bind(null,A)}var O0=(g,A)=>{for(var C in A)x(g,C,{get:A[C],enumerable:!0,configurable:!0,set:X0.bind(A,C)})};var e0={};O0(e0,{mount:()=>c0,default:()=>gg,VOICE_EMBED_VERSION:()=>_0});var J0=(g)=>{if(typeof g!=="string")return g;return document.querySelector(g)},Y0=(g,A,C,V)=>{let T=A??g.getAttribute("hx-get")??"";if(!T)return"";let c=new URL(T,window.location.origin);if(V)c.searchParams.set(C,V);else c.searchParams.delete(C);return`${c.pathname}${c.search}${c.hash}`},d=(g,A)=>{if(typeof window>"u"||typeof document>"u")return()=>{};let C=J0(A.element);if(!C)return()=>{};let V=A.eventName??"voice-refresh",T=A.sessionQueryParam??"sessionId",c=()=>{let I=window,G=Y0(C,A.route,T,g.sessionId);if(G)C.setAttribute("hx-get",G);I.htmx?.process?.(C),I.htmx?.trigger?.(C,V)},U=g.subscribe(c);return c(),()=>{U()}};var Z0=(g)=>Math.max(-1,Math.min(1,g)),h0=(g)=>{let A=new Int16Array(g.length);for(let C=0;C<g.length;C+=1){let V=Z0(g[C]??0);A[C]=V<0?V*32768:V*32767}return new Uint8Array(A.buffer)},w0=(g)=>{let A=g instanceof Uint8Array?g:new Uint8Array(g);if(A.byteLength<2)return 0;let C=new Int16Array(A.buffer,A.byteOffset,Math.floor(A.byteLength/2));if(C.length===0)return 0;let V=0;for(let T of C){let c=T/32768;V+=c*c}return Math.min(1,Math.max(0,Math.sqrt(V/C.length)*5.5))},Q0=(g,A,C)=>{if(A===C)return g;let V=A/C,T=Math.round(g.length/V),c=new Float32Array(T),U=0,I=0;while(U<c.length){let G=Math.round((U+1)*V),W=0,_=0;for(let L=I;L<G&&L<g.length;L+=1)W+=g[L]??0,_+=1;c[U]=_>0?W/_:0,U+=1,I=G}return c},l=(g)=>{let A=null,C=null,V=null,T=null,c=async()=>{if(!g.stream&&(typeof navigator>"u"||!navigator.mediaDevices?.getUserMedia))throw Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");let I=(typeof window<"u"?window.AudioContext??window.webkitAudioContext:void 0)??AudioContext;if(!I)throw Error("Browser microphone capture requires AudioContext support.");try{if(T=g.stream??await navigator.mediaDevices.getUserMedia({audio:{autoGainControl:!0,channelCount:g.channelCount??1,echoCancellation:!0,noiseSuppression:!0}}),A=new I,A.state==="suspended")await A.resume();C=A.createMediaStreamSource(T),V=A.createScriptProcessor(4096,1,1),V.onaudioprocess=(G)=>{let W=G.inputBuffer.getChannelData(0),_=Q0(W,A?.sampleRate??48000,g.sampleRateHz??16000),L=h0(_);g.onLevel?.(w0(L)),g.onAudio(L)},C.connect(V),V.connect(A.destination)}catch(G){throw U(),G}},U=()=>{V?.disconnect(),C?.disconnect(),T?.getTracks().forEach((I)=>I.stop()),A?.close(),g.onLevel?.(0),A=null,T=null,V=null,C=null};return{start:c,stop:U}};var k=(g)=>{if(typeof g==="string"&&g.trim())return g;if(g instanceof Error&&g.message.trim())return g.message;if(g&&typeof g==="object"){let A=g;for(let C of["message","reason","description"]){let V=A[C];if(typeof V==="string"&&V.trim())return V}if("error"in A)return k(A.error);if("cause"in A)return k(A.cause);try{return JSON.stringify(g)}catch{}}return"Unexpected error"},v=(g)=>{switch(g.type){case"audio":return{chunk:Uint8Array.from(atob(g.chunkBase64),(A)=>A.charCodeAt(0)),format:g.format,receivedAt:g.receivedAt,turnId:g.turnId,type:"audio"};case"assistant":return{text:g.text,turnId:g.turnId,type:"assistant"};case"assistant_delta":return{delta:g.delta,turnId:g.turnId,type:"assistant_delta"};case"complete":return{sessionId:g.sessionId,type:"complete"};case"connection":return{reconnect:g.reconnect,type:"connection"};case"call_lifecycle":return{event:g.event,sessionId:g.sessionId,type:"call_lifecycle"};case"error":return{message:k(g.message),recoverable:g.recoverable,type:"error"};case"call_control_ack":return null;case"final":return{transcript:g.transcript,type:"final"};case"partial":return{transcript:g.transcript,type:"partial"};case"replay":return{assistantTexts:g.assistantTexts,call:g.call,partial:g.partial,scenarioId:g.scenarioId,sessionId:g.sessionId,sessionMetadata:g.sessionMetadata,status:g.status,turns:g.turns,type:"replay"};case"session":return{sessionId:g.sessionId,sessionMetadata:g.sessionMetadata,scenarioId:g.scenarioId,status:g.status,type:"session"};case"turn":return{turn:g.turn,type:"turn"};default:return null}};var _g=Math.PI*2;var P=(g,A,C,V)=>{g.push({code:C,message:V,severity:A})};var P0=(g)=>g.length===0?void 0:g.reduce((A,C)=>A+C,0)/g.length,M=(g)=>g.length===0?void 0:Math.max(...g);var X=(g,A)=>{let C=g[A];return typeof C==="number"&&Number.isFinite(C)?C:void 0},B=(g,A)=>{let C=g[A];return typeof C==="boolean"?C:void 0},J=(g,A)=>{let C=g[A];return typeof C==="string"?C:void 0},F=(g)=>String(g.id??J(g,"ssrc")??X(g,"ssrc")??J(g,"trackIdentifier")??J(g,"mid")??"unknown"),i=(g)=>g===void 0?void 0:g*1000;var K0=(g)=>{let A={};for(let[C,V]of Object.entries(g))if(V===null||typeof V==="boolean"||typeof V==="number"||typeof V==="string")A[C]=V;return A};var n=(g={})=>{let A=g.stats??[],C=[],V=A.filter((R)=>R.type==="inbound-rtp"&&J(R,"kind")!=="video"),T=A.filter((R)=>R.type==="outbound-rtp"&&J(R,"kind")!=="video"),c=A.filter((R)=>R.type==="candidate-pair"),U=A.filter((R)=>(R.type==="track"||R.type==="media-source")&&J(R,"kind")==="audio"),I=c.filter((R)=>B(R,"selected")===!0||B(R,"nominated")===!0||J(R,"state")==="succeeded").length,G=U.filter((R)=>J(R,"readyState")!=="ended"&&J(R,"trackState")!=="ended"&&B(R,"ended")!==!0).length,W=U.filter((R)=>J(R,"readyState")==="ended"||J(R,"trackState")==="ended"||B(R,"ended")===!0).length,_=V.reduce((R,O)=>R+(X(O,"packetsReceived")??0),0),L=T.reduce((R,O)=>R+(X(O,"packetsSent")??0),0),H=[...V,...T].reduce((R,O)=>R+Math.max(0,X(O,"packetsLost")??0),0),Z=_+H,E=Z===0?0:H/Z,D=V.reduce((R,O)=>R+(X(O,"bytesReceived")??0),0),h=T.reduce((R,O)=>R+(X(O,"bytesSent")??0),0),w=M(c.map((R)=>i(X(R,"currentRoundTripTime")??X(R,"roundTripTime"))).filter((R)=>R!==void 0)),Q=M([...V,...T].map((R)=>i(X(R,"jitter"))).filter((R)=>R!==void 0)),N=M(V.map((R)=>{let O=X(R,"jitterBufferDelay"),b=X(R,"jitterBufferEmittedCount");return O!==void 0&&b!==void 0&&b>0?O/b*1000:void 0}).filter((R)=>R!==void 0)),z=U.map((R)=>X(R,"audioLevel")).filter((R)=>R!==void 0);if(g.requireConnectedCandidatePair&&c.length>0&&I===0)P(C,"error","media.webrtc_candidate_pair_missing","No active WebRTC candidate pair was observed.");if(g.requireLiveAudioTrack&&G===0)P(C,"error","media.webrtc_audio_track_missing","No live WebRTC audio track was observed.");if(g.maxPacketLossRatio!==void 0&&E>g.maxPacketLossRatio)P(C,"warning","media.webrtc_packet_loss",`Observed WebRTC packet loss ratio ${String(E)} above ${String(g.maxPacketLossRatio)}.`);if(g.maxRoundTripTimeMs!==void 0&&w!==void 0&&w>g.maxRoundTripTimeMs)P(C,"warning","media.webrtc_round_trip_time",`Observed WebRTC RTT ${String(w)}ms above ${String(g.maxRoundTripTimeMs)}ms.`);if(g.maxJitterMs!==void 0&&Q!==void 0&&Q>g.maxJitterMs)P(C,"warning","media.webrtc_jitter",`Observed WebRTC jitter ${String(Q)}ms above ${String(g.maxJitterMs)}ms.`);return{activeCandidatePairs:I,audioLevelAverage:P0(z),bytesReceived:D,bytesSent:h,checkedAt:Date.now(),endedAudioTracks:W,inboundPackets:_,issues:C,jitterBufferDelayMs:N,jitterMs:Q,liveAudioTracks:G,outboundPackets:L,packetLossRatio:E,packetsLost:H,roundTripTimeMs:w,status:C.some((R)=>R.severity==="error")?"fail":C.length>0?"warn":"pass",totalStats:A.length}},o=async(g)=>{return[...(await g.peerConnection.getStats(g.selector??null)).values()].map(K0)};var m=(g={})=>{let A=g.stats??[],C=g.previousStats??[],V=[],T=new Map(C.map((H)=>[F(H),H])),U=A.filter((H)=>(H.type==="inbound-rtp"||H.type==="outbound-rtp")&&J(H,"kind")!=="video"&&J(H,"mediaType")!=="video").map((H)=>{let Z=H.type==="outbound-rtp"?"outbound":"inbound",E=Z==="outbound"?"packetsSent":"packetsReceived",D=Z==="outbound"?"bytesSent":"bytesReceived",h=T.get(F(H)),w=X(H,E),Q=h?X(h,E):void 0,N=X(H,D),z=h?X(h,D):void 0,R=H.timestamp!==void 0&&h?.timestamp!==void 0?H.timestamp-h.timestamp:void 0;return{bytesDelta:N!==void 0&&z!==void 0?N-z:void 0,currentPackets:w,direction:Z,id:F(H),packetDelta:w!==void 0&&Q!==void 0?w-Q:void 0,previousPackets:Q,timeDeltaMs:R}}),I=U.filter((H)=>H.direction==="inbound"),G=U.filter((H)=>H.direction==="outbound"),W=M(U.map((H)=>H.timeDeltaMs).filter((H)=>H!==void 0)),_=I.filter((H)=>g.maxInboundPacketStallMs!==void 0&&H.timeDeltaMs!==void 0&&H.timeDeltaMs>=g.maxInboundPacketStallMs&&H.packetDelta!==void 0&&H.packetDelta<=0).length,L=G.filter((H)=>g.maxOutboundPacketStallMs!==void 0&&H.timeDeltaMs!==void 0&&H.timeDeltaMs>=g.maxOutboundPacketStallMs&&H.packetDelta!==void 0&&H.packetDelta<=0).length;if(g.requireInboundAudio&&I.length===0)P(V,"error","media.webrtc_inbound_audio_missing","No inbound WebRTC audio RTP stream was observed.");if(g.requireOutboundAudio&&G.length===0)P(V,"error","media.webrtc_outbound_audio_missing","No outbound WebRTC audio RTP stream was observed.");if(g.maxGapMs!==void 0&&W!==void 0&&W>g.maxGapMs)P(V,"warning","media.webrtc_stream_gap",`Observed WebRTC stream sample gap ${String(W)}ms above ${String(g.maxGapMs)}ms.`);if(_>0)P(V,"error","media.webrtc_inbound_stalled",`${String(_)} inbound WebRTC audio stream(s) stopped receiving packets.`);if(L>0)P(V,"error","media.webrtc_outbound_stalled",`${String(L)} outbound WebRTC audio stream(s) stopped sending packets.`);return{checkedAt:Date.now(),inboundAudioStreams:I.length,issues:V,maxObservedGapMs:W,outboundAudioStreams:G.length,stalledInboundStreams:_,stalledOutboundStreams:L,status:V.some((H)=>H.severity==="error")?"fail":V.length>0?"warn":"pass",streams:U,totalStats:A.length}};var E0="/api/voice/browser-media",N0=5000,q0=async(g)=>g.peerConnection??await g.getPeerConnection?.()??null,z0=async(g,A)=>{let C=A.fetch??globalThis.fetch;if(!C)return;await C(A.path??E0,{body:JSON.stringify(g),headers:{"Content-Type":"application/json"},keepalive:!0,method:"POST"})},r=(g)=>{let A=null,C=[],V=async()=>{let U=await q0(g);if(!U)return;let I=await o({peerConnection:U}),G=n({...g,stats:I}),W=g.continuity===!1?void 0:m({...g.continuity,previousStats:C,stats:I}),_={at:Date.now(),continuity:W,report:G,scenarioId:g.getScenarioId?.()??null,sessionId:g.getSessionId?.()??null};return C=I,g.onReport?.(_),await z0(_,g),_},T=()=>{V().catch((U)=>{g.onError?.(U)})},c=()=>{if(A)clearInterval(A),A=null};return{close:c,reportOnce:V,stop:c,start:()=>{if(A)return;T(),A=setInterval(T,g.intervalMs??N0)}}};var b0=(g,A,C)=>Math.min(C,A*2**(Math.max(1,g)-1));var q=()=>{},B0=()=>Promise.resolve(),M0=()=>q,j0={callControl:B0,close:q,disconnect:q,endTurn:q,send:q,sendAudio:q,simulateDisconnect:q,subscribe:M0,getReadyState:()=>3,getScenarioId:()=>"",getSessionId:()=>"",start:()=>{}},x0=()=>crypto.randomUUID(),k0=(g,A,C,V)=>{let{hostname:T,port:c,protocol:U}=window.location,I=U==="https:"?"wss:":"ws:",G=c?`:${c}`:"",W=new URL(`${I}//${T}${G}${g}`);if(W.searchParams.set("sessionId",A),C)W.searchParams.set("scenarioId",C);for(let[_,L]of Object.entries(V))if(_!=="sessionId"&&_!=="scenarioId"&&L)W.searchParams.set(_,L);return W.toString()},F0=(g)=>{if(!g||typeof g!=="object"||!("type"in g))return!1;switch(g.type){case"audio":case"assistant":case"call_lifecycle":case"call_control_ack":case"complete":case"connection":case"error":case"final":case"partial":case"pong":case"replay":case"session":case"turn":return!0;default:return!1}},f0=(g)=>{if(typeof g.data!=="string")return null;try{let A=JSON.parse(g.data);return F0(A)?A:null}catch{return null}},u=(g,A={})=>{if(typeof window>"u")return j0;let C=new Set,V=new Map,T=A.reconnect!==!1,c=A.maxReconnectAttempts??15,U=A.reconnectMaxDelayMs??8000,I=A.pingInterval??30000,G=A.reconnectResetAfterMs??30000,W=($)=>b0($,500,U),_={isConnected:!1,pendingMessages:[],scenarioId:A.scenarioId??null,pingInterval:null,reconnectAttempts:0,reconnectResetTimeout:null,reconnectTimeout:null,sessionId:A.sessionId??x0(),ws:null},L=($)=>{C.forEach((S)=>S($))},H=($)=>{C.forEach((S)=>S({message:$,recoverable:!1,type:"error"})),L({reconnect:{attempts:_.reconnectAttempts,maxAttempts:c,status:"exhausted"},type:"connection"})},Z=()=>{if(_.pingInterval)clearInterval(_.pingInterval),_.pingInterval=null;if(_.reconnectTimeout)clearTimeout(_.reconnectTimeout),_.reconnectTimeout=null;if(_.reconnectResetTimeout)clearTimeout(_.reconnectResetTimeout),_.reconnectResetTimeout=null},E=()=>{if(_.ws?.readyState!==1)return;while(_.pendingMessages.length>0){let $=_.pendingMessages.shift();if($!==void 0)_.ws.send($)}},D=($)=>{for(let S of V.values())clearTimeout(S.timeout),S.reject(Error($));V.clear()},h=()=>{_.reconnectAttempts+=1;let $=W(_.reconnectAttempts),S=Date.now()+$;L({reconnect:{attempts:_.reconnectAttempts,lastDisconnectAt:Date.now(),maxAttempts:c,nextAttemptAt:S,status:"reconnecting"},type:"connection"}),_.reconnectTimeout=setTimeout(async()=>{if(_.reconnectAttempts>c){H("Voice connection could not be restored.");return}try{await A.prepareReconnect?.({attempt:_.reconnectAttempts,path:g,scenarioId:_.scenarioId,sessionId:_.sessionId})}catch{if(_.reconnectAttempts<c)h();else H("Voice authorization could not be renewed for reconnect.");return}w()},$)},w=()=>{let $=new WebSocket(k0(g,_.sessionId,_.scenarioId,A.query??{}));$.binaryType="arraybuffer",$.onopen=()=>{if(_.ws!==$){$.close(1000);return}let S=_.reconnectAttempts>0;if(_.isConnected=!0,E(),S)L({reconnect:{attempts:_.reconnectAttempts,lastResumedAt:Date.now(),maxAttempts:c,status:"resumed"},type:"connection"}),_.reconnectResetTimeout=setTimeout(()=>{if(_.ws===$&&$.readyState===1)_.reconnectAttempts=0;_.reconnectResetTimeout=null},G);C.forEach((y)=>y({scenarioId:_.scenarioId??void 0,sessionId:_.sessionId,status:"active",type:"session"})),_.pingInterval=setInterval(()=>{if($.readyState===1)$.send(JSON.stringify({type:"ping"}))},I)},$.onmessage=(S)=>{if(_.ws!==$)return;let y=f0(S);if(!y)return;if(y.type==="session")_.sessionId=y.sessionId,_.scenarioId=y.scenarioId??_.scenarioId;if(y.type==="call_control_ack"){let Y=V.get(y.requestId);if(Y)if(clearTimeout(Y.timeout),V.delete(y.requestId),y.ok)Y.resolve();else Y.reject(Error(y.message??`${y.action} was not applied`))}C.forEach((Y)=>Y(y))},$.onclose=(S)=>{if(_.ws!==$)return;_.ws=null,_.isConnected=!1,Z();let y=T&&S.code!==1000&&S.code!==4401&&S.code!==4403&&_.reconnectAttempts<c,Y=S.code===4401||S.code===4403;if(Y)C.forEach((j)=>j({message:S.code===4401?"Voice authorization expired or was rejected.":"Voice authorization was forbidden.",recoverable:!1,type:"error"}));if(y)h();else if(T&&S.code!==1000)if(Y)L({reconnect:{attempts:_.reconnectAttempts,lastDisconnectAt:Date.now(),maxAttempts:c,status:"exhausted"},type:"connection"});else H("Voice connection could not be restored.")},_.ws=$},Q=($)=>{if(_.ws?.readyState===1){_.ws.send($);return}if(typeof $!=="string")return;if(_.pendingMessages.length>=100)_.pendingMessages.shift();_.pendingMessages.push($)},N=($)=>{Q(JSON.stringify($))},z=($={})=>{if($.sessionId)_.sessionId=$.sessionId;if($.scenarioId)_.scenarioId=$.scenarioId;N({scenarioId:_.scenarioId??void 0,sessionId:_.sessionId,type:"start"})},R=($)=>{Q($)},O=()=>{N({type:"end_turn"})},b=($)=>{let S=crypto.randomUUID(),{promise:y,reject:Y,resolve:j}=Promise.withResolvers(),U0=setTimeout(()=>{V.delete(S),Y(Error(`${$.action} acknowledgement timed out`))},5000);return V.set(S,{reject:Y,resolve:j,timeout:U0}),N({...$,requestId:S,type:"call_control"}),y},R0=($="client-close")=>{if(Z(),D("Voice connection closed"),_.ws){let S=_.ws;if(_.ws=null,S.readyState===1)S.send(JSON.stringify({reason:$,type:"close"}));S.close(1000)}_.isConnected=!1,C.clear()},$0=()=>{if(Z(),D("Voice connection disconnected"),_.ws){let $=_.ws;_.ws=null,$.close(1000)}_.isConnected=!1,C.clear()},H0=()=>{if(_.ws?.readyState===1)_.ws.close(4000,"absolutejs-voice-reconnect-proof")},I0=($)=>{return C.add($),()=>{C.delete($)}};return w(),{callControl:b,close:R0,disconnect:$0,endTurn:O,send:N,sendAudio:R,simulateDisconnect:H0,start:z,subscribe:I0,getReadyState:()=>_.ws?.readyState??3,getScenarioId:()=>_.scenarioId??"",getSessionId:()=>_.sessionId}};var d0=()=>({attempts:0,maxAttempts:0,status:"idle"}),l0=(g,A)=>{let C=A.trim().replace(/\s+/g," ");if(!C)return g;if(!g)return C;if(g===C||g.endsWith(C))return g;if(C.includes(g))return C;return`${g} ${C}`},v0=(g,A)=>{let C=A.trim().replace(/\s+/g," ");if(!g)return C;if(!C||g.endsWith(C))return g;return`${g} ${C}`},i0=()=>({assistantAudio:[],assistantStreamingText:"",assistantTexts:[],call:null,error:null,isConnected:!1,partial:"",reconnect:d0(),scenarioId:null,sessionId:null,sessionMetadata:null,status:"idle",turns:[]}),s=()=>{let g=i0(),A="",C=new Set,V=()=>{C.forEach((c)=>c())};return{dispatch:(c)=>{switch(c.type){case"audio":g={...g,assistantAudio:[...g.assistantAudio,{chunk:c.chunk,format:c.format,receivedAt:c.receivedAt,turnId:c.turnId}]};break;case"assistant":g={...g,assistantStreamingText:"",assistantTexts:[...g.assistantTexts,c.text]};break;case"assistant_delta":g={...g,assistantStreamingText:`${g.assistantStreamingText}${c.delta}`};break;case"complete":g={...g,sessionId:c.sessionId,status:"completed"};break;case"call_lifecycle":g={...g,call:{...g.call,disposition:c.event.type==="end"?c.event.disposition:g.call?.disposition,endedAt:c.event.type==="end"?c.event.at:g.call?.endedAt,events:[...g.call?.events??[],c.event],lastEventAt:c.event.at,startedAt:g.call?.startedAt??c.event.at},sessionId:c.sessionId};break;case"connected":g={...g,isConnected:!0,reconnect:g.reconnect.status==="reconnecting"?{...g.reconnect,lastResumedAt:Date.now(),nextAttemptAt:void 0,status:"resumed"}:g.reconnect};break;case"connection":g={...g,reconnect:c.reconnect};break;case"disconnected":g={...g,isConnected:!1};break;case"error":g={...g,error:c.message,...c.recoverable===!1?{isConnected:!1,status:"failed"}:{}};break;case"final":A=l0(A,c.transcript.text),g={...g,partial:A};break;case"partial":g={...g,partial:v0(A,c.transcript.text)};break;case"replay":A=c.partial,g={...g,assistantStreamingText:"",assistantTexts:[...c.assistantTexts],call:c.call??null,error:null,isConnected:c.status==="active",partial:c.partial,reconnect:g.reconnect.status==="reconnecting"?{...g.reconnect,lastResumedAt:Date.now(),nextAttemptAt:void 0,status:"resumed"}:g.reconnect,scenarioId:c.scenarioId??g.scenarioId,sessionId:c.sessionId,sessionMetadata:c.sessionMetadata??g.sessionMetadata,status:c.status,turns:[...c.turns]};break;case"session":g={...g,error:null,scenarioId:c.scenarioId??g.scenarioId,isConnected:c.status==="active",sessionId:c.sessionId,sessionMetadata:c.sessionMetadata??g.sessionMetadata,status:c.status};break;case"turn":A="",g={...g,partial:"",turns:[...g.turns,c.turn]};break}V()},getServerSnapshot:()=>g,getSnapshot:()=>g,subscribe:(c)=>{return C.add(c),()=>{C.delete(c)}}}};var a=(g,A={})=>{let C=u(g,A),V=s(),T=A.browserMedia&&typeof window<"u"?r({...A.browserMedia,getScenarioId:()=>A.browserMedia?A.browserMedia.getScenarioId?.()??C.getScenarioId():C.getScenarioId(),getSessionId:()=>A.browserMedia?A.browserMedia.getSessionId?.()??C.getSessionId():C.getSessionId()}):null,c=new Set,U=(_)=>Promise.resolve().then(()=>{if(!_?.sessionId&&!_?.scenarioId)return;C.start(_),T?.start()}),I=()=>{c.forEach((_)=>_())},G=()=>{if(!A.reconnectReportPath||typeof fetch>"u")return;let _=V.getSnapshot(),L=JSON.stringify({at:Date.now(),reconnect:_.reconnect,scenarioId:_.scenarioId,sessionId:C.getSessionId(),turnIds:_.turns.map((H)=>H.id)});fetch(A.reconnectReportPath,{body:L,headers:{"Content-Type":"application/json"},keepalive:!0,method:"POST"}).catch(()=>{})},W=C.subscribe((_)=>{let L=v(_);if(L){if(V.dispatch(L),_.type==="connection")G();I()}});return{start:U,get assistantAudio(){return V.getSnapshot().assistantAudio},get assistantTexts(){return V.getSnapshot().assistantTexts},get assistantStreamingText(){return V.getSnapshot().assistantStreamingText},get call(){return V.getSnapshot().call},callControl(_){return C.callControl(_)},close(_){W(),T?.close(),C.close(_),V.dispatch({type:"disconnected"}),I()},disconnect(){W(),T?.close(),C.disconnect(),V.dispatch({type:"disconnected"}),I()},endTurn(){C.endTurn()},get error(){return V.getSnapshot().error},getServerSnapshot(){return V.getServerSnapshot()},getSnapshot(){return V.getSnapshot()},get isConnected(){return V.getSnapshot().isConnected},get partial(){return V.getSnapshot().partial},get reconnect(){return V.getSnapshot().reconnect},get scenarioId(){return V.getSnapshot().scenarioId},sendAudio(_){C.sendAudio(_)},get sessionId(){return C.getSessionId()},get sessionMetadata(){return V.getSnapshot().sessionMetadata},simulateDisconnect(){C.simulateDisconnect()},get status(){return V.getSnapshot().status},subscribe(_){return c.add(_),()=>{c.delete(_)}},get turns(){return V.getSnapshot().turns}}};var p=(g)=>{if(!g||g.enabled===!1)return;return{enabled:!0,maxGain:g.maxGain??3,noiseGateAttenuation:g.noiseGateAttenuation??0.15,noiseGateThreshold:g.noiseGateThreshold??0.006,targetLevel:g.targetLevel??0.08}};var n0={balanced:{qualityProfile:"general",minSilenceMs:400,silenceMs:1400,speechThreshold:0.012,transcriptStabilityMs:1000},fast:{qualityProfile:"general",minSilenceMs:300,silenceMs:700,speechThreshold:0.015,transcriptStabilityMs:450},"long-form":{qualityProfile:"general",minSilenceMs:600,silenceMs:2200,speechThreshold:0.01,transcriptStabilityMs:1500}},o0={"accent-heavy":{silenceMs:1200,speechThreshold:0.01,transcriptStabilityMs:1200},general:{},"noisy-room":{silenceMs:2000,speechThreshold:0.02,transcriptStabilityMs:1600},"short-command":{silenceMs:500,speechThreshold:0.016,transcriptStabilityMs:420}};var t=(g)=>{let A=g?.profile??"fast",C=g?.qualityProfile??"general",V=n0[A],T=o0[C],c=g?.silenceMs??T.silenceMs??V.silenceMs;return{profile:A,qualityProfile:C,minSilenceMs:Math.min(c,g?.minSilenceMs??T.minSilenceMs??V.minSilenceMs),silenceMs:c,speechThreshold:g?.speechThreshold??T.speechThreshold??V.speechThreshold,transcriptStabilityMs:g?.transcriptStabilityMs??T.transcriptStabilityMs??V.transcriptStabilityMs}};var m0={chat:{audioConditioning:{enabled:!0,maxGain:2.5,noiseGateAttenuation:0,noiseGateThreshold:0.004,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:10,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"balanced",qualityProfile:"short-command"}},default:{capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:10,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"fast",qualityProfile:"general"}},dictation:{audioConditioning:{enabled:!0,maxGain:2.25,noiseGateAttenuation:0.05,noiseGateThreshold:0.003,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:12,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"accent-heavy"}},"guided-intake":{audioConditioning:{enabled:!0,maxGain:2.5,noiseGateAttenuation:0,noiseGateThreshold:0.004,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:12,pingInterval:30000,reconnect:!0},sttLifecycle:"turn-scoped",turnDetection:{profile:"long-form",qualityProfile:"accent-heavy"}},"noisy-room":{audioConditioning:{enabled:!0,maxGain:3,noiseGateAttenuation:0.12,noiseGateThreshold:0.006,targetLevel:0.085},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:2100,speechThreshold:0.02,transcriptStabilityMs:1650}},"pstn-balanced":{audioConditioning:{enabled:!0,maxGain:2.8,noiseGateAttenuation:0.07,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:660,speechThreshold:0.012,transcriptStabilityMs:300}},"pstn-fast":{audioConditioning:{enabled:!0,maxGain:2.75,noiseGateAttenuation:0.06,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:620,speechThreshold:0.012,transcriptStabilityMs:280}},reliability:{audioConditioning:{enabled:!0,maxGain:2.9,noiseGateAttenuation:0.08,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room"}}},e=(g="default")=>{let A=m0[g];return{audioConditioning:p(A.audioConditioning),capture:{channelCount:A.capture?.channelCount??1,sampleRateHz:A.capture?.sampleRateHz??16000},connection:{...A.connection},name:g,sttLifecycle:A.sttLifecycle??"continuous",turnDetection:t(A.turnDetection)}};var r0=(g)=>({assistantAudio:[...g.assistantAudio],assistantStreamingText:g.assistantStreamingText,assistantTexts:[...g.assistantTexts],call:g.call,error:g.error,isConnected:g.isConnected,isRecording:!1,partial:g.partial,reconnect:g.reconnect,recordingError:null,sessionId:g.sessionId,sessionMetadata:g.sessionMetadata,scenarioId:g.scenarioId,status:g.status,turns:[...g.turns]}),g0=(g,A={})=>{let C=e(A.preset),V=a(g,{...C.connection,...A.connection}),T=null,c=r0(V),U=new Set,I=()=>{for(let D of U)D()},G=()=>{if(c={...c,assistantAudio:[...V.assistantAudio],assistantStreamingText:V.assistantStreamingText,assistantTexts:[...V.assistantTexts],call:V.call,error:V.error,isConnected:V.isConnected,partial:V.partial,reconnect:V.reconnect,sessionId:V.sessionId,sessionMetadata:V.sessionMetadata,scenarioId:V.scenarioId,status:V.status,turns:[...V.turns]},A.autoStopOnComplete!==!1&&c.status==="completed"&&c.isRecording)T?.stop(),T=null,c={...c,isRecording:!1};I()},W=V.subscribe(G);G();let _=()=>{if(T)return T;return T=l({channelCount:A.capture?.channelCount??C.capture.channelCount,onLevel:A.capture?.onLevel,onAudio:(D)=>{if(A.capture?.onAudio){A.capture.onAudio(D,V.sendAudio);return}V.sendAudio(D)},sampleRateHz:A.capture?.sampleRateHz??C.capture.sampleRateHz,...A.capture?.stream?{stream:A.capture.stream}:{}}),T},L=()=>{T?.stop(),T=null,c={...c,isRecording:!1},I()},H=async()=>{if(c.isRecording)return;try{c={...c,recordingError:null},I(),await _().start(),c={...c,isRecording:!0},I()}catch(D){throw T?.stop(),T=null,c={...c,isRecording:!1,recordingError:D instanceof Error?D.message:String(D)},I(),D}};return{close:(D)=>{W(),L(),V.close(D)},disconnect:()=>{W(),L(),V.disconnect()},startRecording:H,stopRecording:L,get assistantAudio(){return c.assistantAudio},get assistantTexts(){return c.assistantTexts},get assistantStreamingText(){return c.assistantStreamingText},bindHTMX(D){return d(V,D)},get call(){return c.call},callControl:(D)=>V.callControl(D),endTurn:()=>V.endTurn(),get error(){return c.error},getServerSnapshot:()=>c,getSnapshot:()=>c,get isConnected(){return c.isConnected},get isRecording(){return c.isRecording},get partial(){return c.partial},get reconnect(){return c.reconnect},get recordingError(){return c.recordingError},get scenarioId(){return c.scenarioId},sendAudio:(D)=>V.sendAudio(D),get sessionId(){return c.sessionId},get sessionMetadata(){return c.sessionMetadata},simulateDisconnect:()=>V.simulateDisconnect(),get status(){return c.status},subscribe:(D)=>{return U.add(D),()=>{U.delete(D)}},toggleRecording:async()=>{if(c.isRecording){L();return}await H()},get turns(){return c.turns}}};var K=(g)=>String(g).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#39;");var A0=(g)=>{if(!g.isConnected)return"idle";if(g.isPlaying)return"speaking";if(g.isRecording&&g.hasActivePartial)return"listening";if(g.isRecording)return"listening";if(g.lastTranscriptAt&&!g.lastAssistantAt)return"thinking";if(g.lastTranscriptAt&&g.lastAssistantAt&&g.lastTranscriptAt>g.lastAssistantAt)return"thinking";return"idle"};var u0={accent:"#3b82f6",background:"#0f172a",errorAccent:"#ef4444",fontFamily:'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',foreground:"#f8fafc",radius:16},s0={callEnded:"Call ended",connecting:"Connecting…",endCall:"End call",idle:"Idle",listening:"Listening",mute:"Mute",speaking:"Speaking",startCall:"Start call",thinking:"Thinking",unmute:"Unmute"},a0=(g,A)=>{switch(g){case"listening":return A.listening;case"speaking":return A.speaking;case"thinking":return A.thinking;case"idle":return A.idle}},V0=(g)=>{let A={...u0,...g.theme},C={...s0,...g.labels},V=g.state.assistantAudio.at(-1)?.receivedAt,T=g.state.turns.at(-1)?.committedAt,c=A0({hasActivePartial:g.state.partial.length>0,isConnected:g.state.isConnected,isPlaying:!1,isRecording:g.state.isRecording,lastAssistantAt:V,lastTranscriptAt:T}),U=!g.state.isConnected&&g.state.status!=="idle"&&!g.state.error,I=g.state.error?"Error":U?C.connecting:g.state.status==="completed"?C.callEnded:a0(c,C);return{agentState:c,classes:{container:`absolute-voice-widget absolute-voice-widget--${c}`,dot:`absolute-voice-widget__dot${g.state.error?" absolute-voice-widget__dot--error":""}`},controls:{canEnd:g.state.isConnected,canMute:g.state.isRecording,canStart:!g.state.isRecording&&g.state.status!=="completed"},errorMessage:g.state.error??void 0,labels:C,partial:g.state.partial||void 0,statusLabel:I,theme:A,title:g.title??"Voice"}},p0=(g)=>typeof g==="number"?`${g}px`:g,C0=(g)=>{let A=g.theme,C=`background:${A.background};border-radius:${p0(A.radius)};color:${A.foreground};font-family:${A.fontFamily};min-width:240px;padding:20px 22px;`,V=`background:${g.errorMessage?A.errorAccent:g.agentState==="idle"?"rgba(148,163,184,0.6)":A.accent};border-radius:50%;height:10px;width:10px;`,T=[];if(g.controls.canStart)T.push(`<button type="button" data-action="start" style="background:${A.accent};border:none;border-radius:12px;color:${A.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${K(g.labels.startCall)}</button>`);if(g.controls.canMute)T.push(`<button type="button" data-action="mute" style="background:transparent;border:1px solid rgba(255,255,255,0.18);border-radius:12px;color:${A.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${K(g.labels.mute)}</button>`);if(g.controls.canEnd)T.push(`<button type="button" data-action="end" style="background:${A.errorAccent};border:none;border-radius:12px;color:${A.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${K(g.labels.endCall)}</button>`);return`<div role="region" aria-live="polite" data-agent-state="${g.agentState}" class="${K(g.classes.container)}" style="${C}">
1
+ (()=>{var{defineProperty:x,getOwnPropertyNames:G0,getOwnPropertyDescriptor:S0}=Object,X0=Object.prototype.hasOwnProperty;function O0(g){return this[g]}var y0=(g)=>{var A=(f??=new WeakMap).get(g),C;if(A)return A;if(A=x({},"__esModule",{value:!0}),g&&typeof g==="object"||typeof g==="function"){for(var V of G0(g))if(!X0.call(A,V))x(A,V,{get:O0.bind(g,V),enumerable:!(C=S0(g,V))||C.enumerable})}return f.set(g,A),A},f;var J0=(g)=>g;function Y0(g,A){this[g]=J0.bind(null,A)}var Z0=(g,A)=>{for(var C in A)x(g,C,{get:A[C],enumerable:!0,configurable:!0,set:Y0.bind(A,C)})};var Vg={};Z0(Vg,{mount:()=>T0,default:()=>Cg,VOICE_EMBED_VERSION:()=>$0});var w0=(g)=>{if(typeof g!=="string")return g;return document.querySelector(g)},h0=(g,A,C,V)=>{let $=A??g.getAttribute("hx-get")??"";if(!$)return"";let _=new URL($,window.location.origin);if(V)_.searchParams.set(C,V);else _.searchParams.delete(C);return`${_.pathname}${_.search}${_.hash}`},d=(g,A)=>{if(typeof window>"u"||typeof document>"u")return()=>{};let C=w0(A.element);if(!C)return()=>{};let V=A.eventName??"voice-refresh",$=A.sessionQueryParam??"sessionId",_=()=>{let U=window,c=h0(C,A.route,$,g.sessionId);if(c)C.setAttribute("hx-get",c);U.htmx?.process?.(C),U.htmx?.trigger?.(C,V)},L=g.subscribe(_);return _(),()=>{L()}};var Q0=(g)=>Math.max(-1,Math.min(1,g)),K0=(g)=>{let A=new Int16Array(g.length);for(let C=0;C<g.length;C+=1){let V=Q0(g[C]??0);A[C]=V<0?V*32768:V*32767}return new Uint8Array(A.buffer)},P0=(g)=>{let A=g instanceof Uint8Array?g:new Uint8Array(g);if(A.byteLength<2)return 0;let C=new Int16Array(A.buffer,A.byteOffset,Math.floor(A.byteLength/2));if(C.length===0)return 0;let V=0;for(let $ of C){let _=$/32768;V+=_*_}return Math.min(1,Math.max(0,Math.sqrt(V/C.length)*5.5))},N0=(g,A,C)=>{if(A===C)return g;let V=A/C,$=Math.round(g.length/V),_=new Float32Array($),L=0,U=0;while(L<_.length){let c=Math.round((L+1)*V),W=0,R=0;for(let S=U;S<c&&S<g.length;S+=1)W+=g[S]??0,R+=1;_[L]=R>0?W/R:0,L+=1,U=c}return _},l=(g)=>{let A=null,C=null,V=null,$=null,_=async()=>{if(!g.stream&&(typeof navigator>"u"||!navigator.mediaDevices?.getUserMedia))throw Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");let U=(typeof window<"u"?window.AudioContext??window.webkitAudioContext:void 0)??AudioContext;if(!U)throw Error("Browser microphone capture requires AudioContext support.");try{if($=g.stream??await navigator.mediaDevices.getUserMedia({audio:{autoGainControl:!0,channelCount:g.channelCount??1,echoCancellation:!0,noiseSuppression:!0}}),A=new U,A.state==="suspended")await A.resume();C=A.createMediaStreamSource($),V=A.createScriptProcessor(4096,1,1),V.onaudioprocess=(c)=>{let W=c.inputBuffer.getChannelData(0),R=N0(W,A?.sampleRate??48000,g.sampleRateHz??16000),S=K0(R);g.onLevel?.(P0(S)),g.onAudio(S)},C.connect(V),V.connect(A.destination)}catch(c){throw L(),c}},L=()=>{V?.disconnect(),C?.disconnect(),$?.getTracks().forEach((U)=>U.stop()),A?.close(),g.onLevel?.(0),A=null,$=null,V=null,C=null};return{start:_,stop:L}};var k=(g)=>{if(typeof g==="string"&&g.trim())return g;if(g instanceof Error&&g.message.trim())return g.message;if(g&&typeof g==="object"){let A=g;for(let C of["message","reason","description"]){let V=A[C];if(typeof V==="string"&&V.trim())return V}if("error"in A)return k(A.error);if("cause"in A)return k(A.cause);try{return JSON.stringify(g)}catch{}}return"Unexpected error"},v=(g)=>{switch(g.type){case"audio":return{chunk:Uint8Array.from(atob(g.chunkBase64),(A)=>A.charCodeAt(0)),format:g.format,receivedAt:g.receivedAt,turnId:g.turnId,type:"audio"};case"assistant":return{text:g.text,turnId:g.turnId,type:"assistant"};case"assistant_delta":return{delta:g.delta,turnId:g.turnId,type:"assistant_delta"};case"complete":return{sessionId:g.sessionId,type:"complete"};case"connection":return{reconnect:g.reconnect,type:"connection"};case"call_lifecycle":return{event:g.event,sessionId:g.sessionId,type:"call_lifecycle"};case"error":return{message:k(g.message),recoverable:g.recoverable,type:"error"};case"call_control_ack":return null;case"final":return{transcript:g.transcript,type:"final"};case"partial":return{transcript:g.transcript,type:"partial"};case"replay":return{assistantTexts:g.assistantTexts,call:g.call,partial:g.partial,scenarioId:g.scenarioId,sessionId:g.sessionId,sessionMetadata:g.sessionMetadata,status:g.status,turns:g.turns,type:"replay"};case"session":return{sessionId:g.sessionId,sessionMetadata:g.sessionMetadata,scenarioId:g.scenarioId,status:g.status,type:"session"};case"turn":return{turn:g.turn,type:"turn"};default:return null}};var Rg=Math.PI*2;var P=(g,A,C,V)=>{g.push({code:C,message:V,severity:A})};var E0=(g)=>g.length===0?void 0:g.reduce((A,C)=>A+C,0)/g.length,M=(g)=>g.length===0?void 0:Math.max(...g);var O=(g,A)=>{let C=g[A];return typeof C==="number"&&Number.isFinite(C)?C:void 0},B=(g,A)=>{let C=g[A];return typeof C==="boolean"?C:void 0},Y=(g,A)=>{let C=g[A];return typeof C==="string"?C:void 0},F=(g)=>String(g.id??Y(g,"ssrc")??O(g,"ssrc")??Y(g,"trackIdentifier")??Y(g,"mid")??"unknown"),i=(g)=>g===void 0?void 0:g*1000;var q0=(g)=>{let A={};for(let[C,V]of Object.entries(g))if(V===null||typeof V==="boolean"||typeof V==="number"||typeof V==="string")A[C]=V;return A};var n=(g={})=>{let A=g.stats??[],C=[],V=A.filter((H)=>H.type==="inbound-rtp"&&Y(H,"kind")!=="video"),$=A.filter((H)=>H.type==="outbound-rtp"&&Y(H,"kind")!=="video"),_=A.filter((H)=>H.type==="candidate-pair"),L=A.filter((H)=>(H.type==="track"||H.type==="media-source")&&Y(H,"kind")==="audio"),U=_.filter((H)=>B(H,"selected")===!0||B(H,"nominated")===!0||Y(H,"state")==="succeeded").length,c=L.filter((H)=>Y(H,"readyState")!=="ended"&&Y(H,"trackState")!=="ended"&&B(H,"ended")!==!0).length,W=L.filter((H)=>Y(H,"readyState")==="ended"||Y(H,"trackState")==="ended"||B(H,"ended")===!0).length,R=V.reduce((H,J)=>H+(O(J,"packetsReceived")??0),0),S=$.reduce((H,J)=>H+(O(J,"packetsSent")??0),0),T=[...V,...$].reduce((H,J)=>H+Math.max(0,O(J,"packetsLost")??0),0),Z=R+T,w=Z===0?0:T/Z,D=V.reduce((H,J)=>H+(O(J,"bytesReceived")??0),0),K=$.reduce((H,J)=>H+(O(J,"bytesSent")??0),0),h=M(_.map((H)=>i(O(H,"currentRoundTripTime")??O(H,"roundTripTime"))).filter((H)=>H!==void 0)),Q=M([...V,...$].map((H)=>i(O(H,"jitter"))).filter((H)=>H!==void 0)),E=M(V.map((H)=>{let J=O(H,"jitterBufferDelay"),b=O(H,"jitterBufferEmittedCount");return J!==void 0&&b!==void 0&&b>0?J/b*1000:void 0}).filter((H)=>H!==void 0)),q=L.map((H)=>O(H,"audioLevel")).filter((H)=>H!==void 0);if(g.requireConnectedCandidatePair&&_.length>0&&U===0)P(C,"error","media.webrtc_candidate_pair_missing","No active WebRTC candidate pair was observed.");if(g.requireLiveAudioTrack&&c===0)P(C,"error","media.webrtc_audio_track_missing","No live WebRTC audio track was observed.");if(g.maxPacketLossRatio!==void 0&&w>g.maxPacketLossRatio)P(C,"warning","media.webrtc_packet_loss",`Observed WebRTC packet loss ratio ${String(w)} above ${String(g.maxPacketLossRatio)}.`);if(g.maxRoundTripTimeMs!==void 0&&h!==void 0&&h>g.maxRoundTripTimeMs)P(C,"warning","media.webrtc_round_trip_time",`Observed WebRTC RTT ${String(h)}ms above ${String(g.maxRoundTripTimeMs)}ms.`);if(g.maxJitterMs!==void 0&&Q!==void 0&&Q>g.maxJitterMs)P(C,"warning","media.webrtc_jitter",`Observed WebRTC jitter ${String(Q)}ms above ${String(g.maxJitterMs)}ms.`);return{activeCandidatePairs:U,audioLevelAverage:E0(q),bytesReceived:D,bytesSent:K,checkedAt:Date.now(),endedAudioTracks:W,inboundPackets:R,issues:C,jitterBufferDelayMs:E,jitterMs:Q,liveAudioTracks:c,outboundPackets:S,packetLossRatio:w,packetsLost:T,roundTripTimeMs:h,status:C.some((H)=>H.severity==="error")?"fail":C.length>0?"warn":"pass",totalStats:A.length}},o=async(g)=>{return[...(await g.peerConnection.getStats(g.selector??null)).values()].map(q0)};var m=(g={})=>{let A=g.stats??[],C=g.previousStats??[],V=[],$=new Map(C.map((T)=>[F(T),T])),L=A.filter((T)=>(T.type==="inbound-rtp"||T.type==="outbound-rtp")&&Y(T,"kind")!=="video"&&Y(T,"mediaType")!=="video").map((T)=>{let Z=T.type==="outbound-rtp"?"outbound":"inbound",w=Z==="outbound"?"packetsSent":"packetsReceived",D=Z==="outbound"?"bytesSent":"bytesReceived",K=$.get(F(T)),h=O(T,w),Q=K?O(K,w):void 0,E=O(T,D),q=K?O(K,D):void 0,H=T.timestamp!==void 0&&K?.timestamp!==void 0?T.timestamp-K.timestamp:void 0;return{bytesDelta:E!==void 0&&q!==void 0?E-q:void 0,currentPackets:h,direction:Z,id:F(T),packetDelta:h!==void 0&&Q!==void 0?h-Q:void 0,previousPackets:Q,timeDeltaMs:H}}),U=L.filter((T)=>T.direction==="inbound"),c=L.filter((T)=>T.direction==="outbound"),W=M(L.map((T)=>T.timeDeltaMs).filter((T)=>T!==void 0)),R=U.filter((T)=>g.maxInboundPacketStallMs!==void 0&&T.timeDeltaMs!==void 0&&T.timeDeltaMs>=g.maxInboundPacketStallMs&&T.packetDelta!==void 0&&T.packetDelta<=0).length,S=c.filter((T)=>g.maxOutboundPacketStallMs!==void 0&&T.timeDeltaMs!==void 0&&T.timeDeltaMs>=g.maxOutboundPacketStallMs&&T.packetDelta!==void 0&&T.packetDelta<=0).length;if(g.requireInboundAudio&&U.length===0)P(V,"error","media.webrtc_inbound_audio_missing","No inbound WebRTC audio RTP stream was observed.");if(g.requireOutboundAudio&&c.length===0)P(V,"error","media.webrtc_outbound_audio_missing","No outbound WebRTC audio RTP stream was observed.");if(g.maxGapMs!==void 0&&W!==void 0&&W>g.maxGapMs)P(V,"warning","media.webrtc_stream_gap",`Observed WebRTC stream sample gap ${String(W)}ms above ${String(g.maxGapMs)}ms.`);if(R>0)P(V,"error","media.webrtc_inbound_stalled",`${String(R)} inbound WebRTC audio stream(s) stopped receiving packets.`);if(S>0)P(V,"error","media.webrtc_outbound_stalled",`${String(S)} outbound WebRTC audio stream(s) stopped sending packets.`);return{checkedAt:Date.now(),inboundAudioStreams:U.length,issues:V,maxObservedGapMs:W,outboundAudioStreams:c.length,stalledInboundStreams:R,stalledOutboundStreams:S,status:V.some((T)=>T.severity==="error")?"fail":V.length>0?"warn":"pass",streams:L,totalStats:A.length}};var z0="/api/voice/browser-media",b0=5000,B0=async(g)=>g.peerConnection??await g.getPeerConnection?.()??null,M0=async(g,A)=>{let C=A.fetch??globalThis.fetch;if(!C)return;await C(A.path??z0,{body:JSON.stringify(g),headers:{"Content-Type":"application/json"},keepalive:!0,method:"POST"})},r=(g)=>{let A=null,C=[],V=async()=>{let L=await B0(g);if(!L)return;let U=await o({peerConnection:L}),c=n({...g,stats:U}),W=g.continuity===!1?void 0:m({...g.continuity,previousStats:C,stats:U}),R={at:Date.now(),continuity:W,report:c,scenarioId:g.getScenarioId?.()??null,sessionId:g.getSessionId?.()??null};return C=U,g.onReport?.(R),await M0(R,g),R},$=()=>{V().catch((L)=>{g.onError?.(L)})},_=()=>{if(A)clearInterval(A),A=null};return{close:_,reportOnce:V,stop:_,start:()=>{if(A)return;$(),A=setInterval($,g.intervalMs??b0)}}};var j0=(g,A,C)=>Math.min(C,A*2**(Math.max(1,g)-1));class u extends Error{constructor(g="Voice authorization could not be renewed."){super(g);this.name="VoiceReconnectRejectedError"}}var z=()=>{},x0=()=>Promise.resolve(),k0=()=>z,F0={callControl:x0,close:z,disconnect:z,endTurn:z,send:z,sendAudio:z,simulateDisconnect:z,subscribe:k0,getReadyState:()=>3,getScenarioId:()=>"",getSessionId:()=>"",start:()=>{}},f0=()=>crypto.randomUUID(),d0=(g,A,C,V)=>{let{hostname:$,port:_,protocol:L}=window.location,U=L==="https:"?"wss:":"ws:",c=_?`:${_}`:"",W=new URL(`${U}//${$}${c}${g}`);if(W.searchParams.set("sessionId",A),C)W.searchParams.set("scenarioId",C);for(let[R,S]of Object.entries(V))if(R!=="sessionId"&&R!=="scenarioId"&&S)W.searchParams.set(R,S);return W.toString()},l0=(g)=>{if(!g||typeof g!=="object"||!("type"in g))return!1;switch(g.type){case"audio":case"assistant":case"call_lifecycle":case"call_control_ack":case"complete":case"connection":case"error":case"final":case"partial":case"pong":case"replay":case"session":case"turn":return!0;default:return!1}},v0=(g)=>{if(typeof g.data!=="string")return null;try{let A=JSON.parse(g.data);return l0(A)?A:null}catch{return null}},s=(g,A={})=>{if(typeof window>"u")return F0;let C=new Set,V=new Map,$=A.reconnect!==!1,_=A.maxReconnectAttempts??15,L=A.reconnectMaxDelayMs??8000,U=A.pingInterval??30000,c=A.reconnectResetAfterMs??30000,W=0,R=!1,S=(I)=>j0(I,500,L),T={isConnected:!1,pendingMessages:[],scenarioId:A.scenarioId??null,pingInterval:null,reconnectAttempts:0,reconnectResetTimeout:null,reconnectTimeout:null,sessionId:A.sessionId??f0(),ws:null},Z=(I)=>{C.forEach((G)=>G(I))},w=(I)=>{C.forEach((G)=>G({message:I,recoverable:!1,type:"error"})),Z({reconnect:{attempts:T.reconnectAttempts,maxAttempts:_,status:"exhausted"},type:"connection"})},D=()=>{if(T.pingInterval)clearInterval(T.pingInterval),T.pingInterval=null;if(T.reconnectTimeout)clearTimeout(T.reconnectTimeout),T.reconnectTimeout=null;if(T.reconnectResetTimeout)clearTimeout(T.reconnectResetTimeout),T.reconnectResetTimeout=null},K=()=>{if(T.ws?.readyState!==1)return;while(T.pendingMessages.length>0){let I=T.pendingMessages.shift();if(I!==void 0)T.ws.send(I)}},h=(I)=>{for(let G of V.values())clearTimeout(G.timeout),G.reject(Error(I));V.clear()},Q=()=>{if(R)return;let I=W;T.reconnectAttempts+=1;let G=S(T.reconnectAttempts),X=Date.now()+G;Z({reconnect:{attempts:T.reconnectAttempts,lastDisconnectAt:Date.now(),maxAttempts:_,nextAttemptAt:X,status:"reconnecting"},type:"connection"}),T.reconnectTimeout=setTimeout(async()=>{if(R||W!==I)return;if(T.reconnectAttempts>_){w("Voice connection could not be restored.");return}try{await A.prepareReconnect?.({attempt:T.reconnectAttempts,path:g,scenarioId:T.scenarioId,sessionId:T.sessionId})}catch(y){if(R||W!==I)return;if(y instanceof u){w(y.message);return}if(T.reconnectAttempts<_)Q();else w("Voice authorization could not be renewed for reconnect.");return}if(R||W!==I)return;E()},G)},E=()=>{if(R)return;let I=new WebSocket(d0(g,T.sessionId,T.scenarioId,A.query??{}));I.binaryType="arraybuffer",I.onopen=()=>{if(T.ws!==I){I.close(1000);return}let G=T.reconnectAttempts>0;if(T.isConnected=!0,K(),G)Z({reconnect:{attempts:T.reconnectAttempts,lastResumedAt:Date.now(),maxAttempts:_,status:"resumed"},type:"connection"}),T.reconnectResetTimeout=setTimeout(()=>{if(T.ws===I&&I.readyState===1)T.reconnectAttempts=0;T.reconnectResetTimeout=null},c);C.forEach((X)=>X({scenarioId:T.scenarioId??void 0,sessionId:T.sessionId,status:"active",type:"session"})),T.pingInterval=setInterval(()=>{if(I.readyState===1)I.send(JSON.stringify({type:"ping"}))},U)},I.onmessage=(G)=>{if(T.ws!==I)return;let X=v0(G);if(!X)return;if(X.type==="session")T.sessionId=X.sessionId,T.scenarioId=X.scenarioId??T.scenarioId;if(X.type==="call_control_ack"){let y=V.get(X.requestId);if(y)if(clearTimeout(y.timeout),V.delete(X.requestId),X.ok)y.resolve();else y.reject(Error(X.message??`${X.action} was not applied`))}C.forEach((y)=>y(X))},I.onclose=(G)=>{if(T.ws!==I)return;T.ws=null,T.isConnected=!1,D(),h("Voice connection interrupted");let X=$&&G.code!==1000&&G.code!==4401&&G.code!==4403&&T.reconnectAttempts<_,y=G.code===4401||G.code===4403;if(y)C.forEach((j)=>j({message:G.code===4401?"Voice authorization expired or was rejected.":"Voice authorization was forbidden.",recoverable:!1,type:"error"}));if(X)Q();else if($&&G.code!==1000)if(y)Z({reconnect:{attempts:T.reconnectAttempts,lastDisconnectAt:Date.now(),maxAttempts:_,status:"exhausted"},type:"connection"});else w("Voice connection could not be restored.")},T.ws=I},q=(I)=>{if(T.ws?.readyState===1){T.ws.send(I);return}if(typeof I!=="string")return;if(T.pendingMessages.length>=100)T.pendingMessages.shift();T.pendingMessages.push(I)},H=(I)=>{q(JSON.stringify(I))},J=(I={})=>{if(I.sessionId)T.sessionId=I.sessionId;if(I.scenarioId)T.scenarioId=I.scenarioId;H({scenarioId:T.scenarioId??void 0,sessionId:T.sessionId,type:"start"})},b=(I)=>{q(I)},R0=()=>{H({type:"end_turn"})},I0=(I)=>{if(T.ws?.readyState!==1)return Promise.reject(Error(`${I.action} is unavailable while disconnected`));let G=crypto.randomUUID(),{promise:X,reject:y,resolve:j}=Promise.withResolvers(),D0=setTimeout(()=>{V.delete(G),y(Error(`${I.action} acknowledgement timed out`))},5000);return V.set(G,{reject:y,resolve:j,timeout:D0}),T.ws.send(JSON.stringify({...I,requestId:G,type:"call_control"})),X},U0=(I="client-close")=>{if(R=!0,W+=1,D(),h("Voice connection closed"),T.pendingMessages.length=0,T.ws){let G=T.ws;if(T.ws=null,G.readyState===1)G.send(JSON.stringify({reason:I,type:"close"}));G.close(1000)}T.isConnected=!1,C.clear()},L0=()=>{if(R=!0,W+=1,D(),h("Voice connection disconnected"),T.pendingMessages.length=0,T.ws){let I=T.ws;T.ws=null,I.close(1000)}T.isConnected=!1,C.clear()},W0=()=>{if(T.ws?.readyState===1)T.ws.close(4000,"absolutejs-voice-reconnect-proof")},c0=(I)=>{return C.add(I),()=>{C.delete(I)}};return E(),{callControl:I0,close:U0,disconnect:L0,endTurn:R0,send:H,sendAudio:b,simulateDisconnect:W0,start:J,subscribe:c0,getReadyState:()=>T.ws?.readyState??3,getScenarioId:()=>T.scenarioId??"",getSessionId:()=>T.sessionId}};var i0=()=>({attempts:0,maxAttempts:0,status:"idle"}),n0=(g,A)=>{let C=A.trim().replace(/\s+/g," ");if(!C)return g;if(!g)return C;if(g===C||g.endsWith(C))return g;if(C.includes(g))return C;return`${g} ${C}`},o0=(g,A)=>{let C=A.trim().replace(/\s+/g," ");if(!g)return C;if(!C||g.endsWith(C))return g;return`${g} ${C}`},m0=()=>({assistantAudio:[],assistantStreamingText:"",assistantTexts:[],call:null,error:null,isConnected:!1,partial:"",reconnect:i0(),scenarioId:null,sessionId:null,sessionMetadata:null,status:"idle",turns:[]}),a=()=>{let g=m0(),A="",C=new Set,V=()=>{C.forEach((_)=>_())};return{dispatch:(_)=>{switch(_.type){case"audio":g={...g,assistantAudio:[...g.assistantAudio,{chunk:_.chunk,format:_.format,receivedAt:_.receivedAt,turnId:_.turnId}]};break;case"assistant":g={...g,assistantStreamingText:"",assistantTexts:[...g.assistantTexts,_.text]};break;case"assistant_delta":g={...g,assistantStreamingText:`${g.assistantStreamingText}${_.delta}`};break;case"complete":g={...g,sessionId:_.sessionId,status:"completed"};break;case"call_lifecycle":g={...g,call:{...g.call,disposition:_.event.type==="end"?_.event.disposition:g.call?.disposition,endedAt:_.event.type==="end"?_.event.at:g.call?.endedAt,events:[...g.call?.events??[],_.event],lastEventAt:_.event.at,startedAt:g.call?.startedAt??_.event.at},sessionId:_.sessionId};break;case"connected":g={...g,isConnected:!0,reconnect:g.reconnect.status==="reconnecting"?{...g.reconnect,lastResumedAt:Date.now(),nextAttemptAt:void 0,status:"resumed"}:g.reconnect};break;case"connection":g={...g,reconnect:_.reconnect};break;case"disconnected":g={...g,isConnected:!1};break;case"error":g={...g,error:_.message,..._.recoverable===!1?{isConnected:!1,status:"failed"}:{}};break;case"final":A=n0(A,_.transcript.text),g={...g,partial:A};break;case"partial":g={...g,partial:o0(A,_.transcript.text)};break;case"replay":A=_.partial,g={...g,assistantStreamingText:"",assistantTexts:[..._.assistantTexts],call:_.call??null,error:null,isConnected:_.status==="active",partial:_.partial,reconnect:g.reconnect.status==="reconnecting"?{...g.reconnect,lastResumedAt:Date.now(),nextAttemptAt:void 0,status:"resumed"}:g.reconnect,scenarioId:_.scenarioId??g.scenarioId,sessionId:_.sessionId,sessionMetadata:_.sessionMetadata??g.sessionMetadata,status:_.status,turns:[..._.turns]};break;case"session":g={...g,error:null,scenarioId:_.scenarioId??g.scenarioId,isConnected:_.status==="active",sessionId:_.sessionId,sessionMetadata:_.sessionMetadata??g.sessionMetadata,status:_.status};break;case"turn":A="",g={...g,partial:"",turns:[...g.turns,_.turn]};break}V()},getServerSnapshot:()=>g,getSnapshot:()=>g,subscribe:(_)=>{return C.add(_),()=>{C.delete(_)}}}};var p=(g,A={})=>{let C=s(g,A),V=a(),$=A.browserMedia&&typeof window<"u"?r({...A.browserMedia,getScenarioId:()=>A.browserMedia?A.browserMedia.getScenarioId?.()??C.getScenarioId():C.getScenarioId(),getSessionId:()=>A.browserMedia?A.browserMedia.getSessionId?.()??C.getSessionId():C.getSessionId()}):null,_=new Set,L=(R)=>Promise.resolve().then(()=>{if(!R?.sessionId&&!R?.scenarioId)return;C.start(R),$?.start()}),U=()=>{_.forEach((R)=>R())},c=()=>{if(!A.reconnectReportPath||typeof fetch>"u")return;let R=V.getSnapshot(),S=JSON.stringify({at:Date.now(),reconnect:R.reconnect,scenarioId:R.scenarioId,sessionId:C.getSessionId(),turnIds:R.turns.map((T)=>T.id)});fetch(A.reconnectReportPath,{body:S,headers:{"Content-Type":"application/json"},keepalive:!0,method:"POST"}).catch(()=>{})},W=C.subscribe((R)=>{let S=v(R);if(S){if(V.dispatch(S),R.type==="connection")c();U()}});return{start:L,get assistantAudio(){return V.getSnapshot().assistantAudio},get assistantTexts(){return V.getSnapshot().assistantTexts},get assistantStreamingText(){return V.getSnapshot().assistantStreamingText},get call(){return V.getSnapshot().call},callControl(R){return C.callControl(R)},close(R){W(),$?.close(),C.close(R),V.dispatch({type:"disconnected"}),U()},disconnect(){W(),$?.close(),C.disconnect(),V.dispatch({type:"disconnected"}),U()},endTurn(){C.endTurn()},get error(){return V.getSnapshot().error},getServerSnapshot(){return V.getServerSnapshot()},getSnapshot(){return V.getSnapshot()},get isConnected(){return V.getSnapshot().isConnected},get partial(){return V.getSnapshot().partial},get reconnect(){return V.getSnapshot().reconnect},get scenarioId(){return V.getSnapshot().scenarioId},sendAudio(R){C.sendAudio(R)},get sessionId(){return C.getSessionId()},get sessionMetadata(){return V.getSnapshot().sessionMetadata},simulateDisconnect(){C.simulateDisconnect()},get status(){return V.getSnapshot().status},subscribe(R){return _.add(R),()=>{_.delete(R)}},get turns(){return V.getSnapshot().turns}}};var t=(g)=>{if(!g||g.enabled===!1)return;return{enabled:!0,maxGain:g.maxGain??3,noiseGateAttenuation:g.noiseGateAttenuation??0.15,noiseGateThreshold:g.noiseGateThreshold??0.006,targetLevel:g.targetLevel??0.08}};var r0={balanced:{qualityProfile:"general",minSilenceMs:400,silenceMs:1400,speechThreshold:0.012,transcriptStabilityMs:1000},fast:{qualityProfile:"general",minSilenceMs:300,silenceMs:700,speechThreshold:0.015,transcriptStabilityMs:450},"long-form":{qualityProfile:"general",minSilenceMs:600,silenceMs:2200,speechThreshold:0.01,transcriptStabilityMs:1500}},u0={"accent-heavy":{silenceMs:1200,speechThreshold:0.01,transcriptStabilityMs:1200},general:{},"noisy-room":{silenceMs:2000,speechThreshold:0.02,transcriptStabilityMs:1600},"short-command":{silenceMs:500,speechThreshold:0.016,transcriptStabilityMs:420}};var e=(g)=>{let A=g?.profile??"fast",C=g?.qualityProfile??"general",V=r0[A],$=u0[C],_=g?.silenceMs??$.silenceMs??V.silenceMs;return{profile:A,qualityProfile:C,minSilenceMs:Math.min(_,g?.minSilenceMs??$.minSilenceMs??V.minSilenceMs),silenceMs:_,speechThreshold:g?.speechThreshold??$.speechThreshold??V.speechThreshold,transcriptStabilityMs:g?.transcriptStabilityMs??$.transcriptStabilityMs??V.transcriptStabilityMs}};var s0={chat:{audioConditioning:{enabled:!0,maxGain:2.5,noiseGateAttenuation:0,noiseGateThreshold:0.004,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:10,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"balanced",qualityProfile:"short-command"}},default:{capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:10,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"fast",qualityProfile:"general"}},dictation:{audioConditioning:{enabled:!0,maxGain:2.25,noiseGateAttenuation:0.05,noiseGateThreshold:0.003,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:12,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"accent-heavy"}},"guided-intake":{audioConditioning:{enabled:!0,maxGain:2.5,noiseGateAttenuation:0,noiseGateThreshold:0.004,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:12,pingInterval:30000,reconnect:!0},sttLifecycle:"turn-scoped",turnDetection:{profile:"long-form",qualityProfile:"accent-heavy"}},"noisy-room":{audioConditioning:{enabled:!0,maxGain:3,noiseGateAttenuation:0.12,noiseGateThreshold:0.006,targetLevel:0.085},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:2100,speechThreshold:0.02,transcriptStabilityMs:1650}},"pstn-balanced":{audioConditioning:{enabled:!0,maxGain:2.8,noiseGateAttenuation:0.07,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:660,speechThreshold:0.012,transcriptStabilityMs:300}},"pstn-fast":{audioConditioning:{enabled:!0,maxGain:2.75,noiseGateAttenuation:0.06,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:620,speechThreshold:0.012,transcriptStabilityMs:280}},reliability:{audioConditioning:{enabled:!0,maxGain:2.9,noiseGateAttenuation:0.08,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room"}}},g0=(g="default")=>{let A=s0[g];return{audioConditioning:t(A.audioConditioning),capture:{channelCount:A.capture?.channelCount??1,sampleRateHz:A.capture?.sampleRateHz??16000},connection:{...A.connection},name:g,sttLifecycle:A.sttLifecycle??"continuous",turnDetection:e(A.turnDetection)}};var a0=(g)=>({assistantAudio:[...g.assistantAudio],assistantStreamingText:g.assistantStreamingText,assistantTexts:[...g.assistantTexts],call:g.call,error:g.error,isConnected:g.isConnected,isRecording:!1,partial:g.partial,reconnect:g.reconnect,recordingError:null,sessionId:g.sessionId,sessionMetadata:g.sessionMetadata,scenarioId:g.scenarioId,status:g.status,turns:[...g.turns]}),A0=(g,A={})=>{let C=g0(A.preset),V=p(g,{...C.connection,...A.connection}),$=null,_=a0(V),L=new Set,U=()=>{for(let D of L)D()},c=()=>{if(_={..._,assistantAudio:[...V.assistantAudio],assistantStreamingText:V.assistantStreamingText,assistantTexts:[...V.assistantTexts],call:V.call,error:V.error,isConnected:V.isConnected,partial:V.partial,reconnect:V.reconnect,sessionId:V.sessionId,sessionMetadata:V.sessionMetadata,scenarioId:V.scenarioId,status:V.status,turns:[...V.turns]},A.autoStopOnComplete!==!1&&_.status==="completed"&&_.isRecording)$?.stop(),$=null,_={..._,isRecording:!1};U()},W=V.subscribe(c);c();let R=()=>{if($)return $;return $=l({channelCount:A.capture?.channelCount??C.capture.channelCount,onLevel:A.capture?.onLevel,onAudio:(D)=>{if(A.capture?.onAudio){A.capture.onAudio(D,V.sendAudio);return}V.sendAudio(D)},sampleRateHz:A.capture?.sampleRateHz??C.capture.sampleRateHz,...A.capture?.stream?{stream:A.capture.stream}:{}}),$},S=()=>{$?.stop(),$=null,_={..._,isRecording:!1},U()},T=async()=>{if(_.isRecording)return;try{_={..._,recordingError:null},U(),await R().start(),_={..._,isRecording:!0},U()}catch(D){throw $?.stop(),$=null,_={..._,isRecording:!1,recordingError:D instanceof Error?D.message:String(D)},U(),D}};return{close:(D)=>{W(),S(),V.close(D)},disconnect:()=>{W(),S(),V.disconnect()},startRecording:T,stopRecording:S,get assistantAudio(){return _.assistantAudio},get assistantTexts(){return _.assistantTexts},get assistantStreamingText(){return _.assistantStreamingText},bindHTMX(D){return d(V,D)},get call(){return _.call},callControl:(D)=>V.callControl(D),endTurn:()=>V.endTurn(),get error(){return _.error},getServerSnapshot:()=>_,getSnapshot:()=>_,get isConnected(){return _.isConnected},get isRecording(){return _.isRecording},get partial(){return _.partial},get reconnect(){return _.reconnect},get recordingError(){return _.recordingError},get scenarioId(){return _.scenarioId},sendAudio:(D)=>V.sendAudio(D),get sessionId(){return _.sessionId},get sessionMetadata(){return _.sessionMetadata},simulateDisconnect:()=>V.simulateDisconnect(),get status(){return _.status},subscribe:(D)=>{return L.add(D),()=>{L.delete(D)}},toggleRecording:async()=>{if(_.isRecording){S();return}await T()},get turns(){return _.turns}}};var N=(g)=>String(g).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#39;");var V0=(g)=>{if(!g.isConnected)return"idle";if(g.isPlaying)return"speaking";if(g.isRecording&&g.hasActivePartial)return"listening";if(g.isRecording)return"listening";if(g.lastTranscriptAt&&!g.lastAssistantAt)return"thinking";if(g.lastTranscriptAt&&g.lastAssistantAt&&g.lastTranscriptAt>g.lastAssistantAt)return"thinking";return"idle"};var p0={accent:"#3b82f6",background:"#0f172a",errorAccent:"#ef4444",fontFamily:'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',foreground:"#f8fafc",radius:16},t0={callEnded:"Call ended",connecting:"Connecting…",endCall:"End call",idle:"Idle",listening:"Listening",mute:"Mute",speaking:"Speaking",startCall:"Start call",thinking:"Thinking",unmute:"Unmute"},e0=(g,A)=>{switch(g){case"listening":return A.listening;case"speaking":return A.speaking;case"thinking":return A.thinking;case"idle":return A.idle}},C0=(g)=>{let A={...p0,...g.theme},C={...t0,...g.labels},V=g.state.assistantAudio.at(-1)?.receivedAt,$=g.state.turns.at(-1)?.committedAt,_=V0({hasActivePartial:g.state.partial.length>0,isConnected:g.state.isConnected,isPlaying:!1,isRecording:g.state.isRecording,lastAssistantAt:V,lastTranscriptAt:$}),L=!g.state.isConnected&&g.state.status!=="idle"&&!g.state.error,U=g.state.error?"Error":L?C.connecting:g.state.status==="completed"?C.callEnded:e0(_,C);return{agentState:_,classes:{container:`absolute-voice-widget absolute-voice-widget--${_}`,dot:`absolute-voice-widget__dot${g.state.error?" absolute-voice-widget__dot--error":""}`},controls:{canEnd:g.state.isConnected,canMute:g.state.isRecording,canStart:!g.state.isRecording&&g.state.status!=="completed"},errorMessage:g.state.error??void 0,labels:C,partial:g.state.partial||void 0,statusLabel:U,theme:A,title:g.title??"Voice"}},gg=(g)=>typeof g==="number"?`${g}px`:g,_0=(g)=>{let A=g.theme,C=`background:${A.background};border-radius:${gg(A.radius)};color:${A.foreground};font-family:${A.fontFamily};min-width:240px;padding:20px 22px;`,V=`background:${g.errorMessage?A.errorAccent:g.agentState==="idle"?"rgba(148,163,184,0.6)":A.accent};border-radius:50%;height:10px;width:10px;`,$=[];if(g.controls.canStart)$.push(`<button type="button" data-action="start" style="background:${A.accent};border:none;border-radius:12px;color:${A.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${N(g.labels.startCall)}</button>`);if(g.controls.canMute)$.push(`<button type="button" data-action="mute" style="background:transparent;border:1px solid rgba(255,255,255,0.18);border-radius:12px;color:${A.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${N(g.labels.mute)}</button>`);if(g.controls.canEnd)$.push(`<button type="button" data-action="end" style="background:${A.errorAccent};border:none;border-radius:12px;color:${A.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${N(g.labels.endCall)}</button>`);return`<div role="region" aria-live="polite" data-agent-state="${g.agentState}" class="${N(g.classes.container)}" style="${C}">
2
2
  <div style="align-items:center;display:flex;gap:10px;margin-bottom:12px;">
3
- <span aria-hidden="true" class="${K(g.classes.dot)}" style="${V}"></span>
4
- <strong style="font-size:15px;">${K(g.title)}</strong>
5
- <span style="font-size:13px;margin-left:auto;opacity:0.7;">${K(g.statusLabel)}</span>
3
+ <span aria-hidden="true" class="${N(g.classes.dot)}" style="${V}"></span>
4
+ <strong style="font-size:15px;">${N(g.title)}</strong>
5
+ <span style="font-size:13px;margin-left:auto;opacity:0.7;">${N(g.statusLabel)}</span>
6
6
  </div>
7
- ${g.partial?`<p style="font-size:13px;margin:8px 0 12px;opacity:0.85;word-break:break-word;">“${K(g.partial)}”</p>`:""}
8
- <div style="display:flex;gap:10px;">${T.join("")}</div>
9
- ${g.errorMessage?`<p style="color:${A.errorAccent};font-size:12px;margin-top:12px;">${K(g.errorMessage)}</p>`:""}
10
- </div>`};var t0=(g)=>{if(typeof g!=="string")return g;let A=document.querySelector(g);if(!A)throw Error(`AbsoluteVoice.mount: no element matches "${g}"`);return A},c0=(g,A={})=>{let C=t0(g),V=g0(A.path??"/voice",A.controllerOptions),T=null,c=null,U=()=>{let G=V0({...A.labels!==void 0?{labels:A.labels}:{},state:{assistantAudio:V.assistantAudio,error:V.error,isConnected:V.isConnected,isRecording:V.isRecording,partial:V.partial,status:V.status,turns:V.turns},...A.theme!==void 0?{theme:A.theme}:{},...A.title!==void 0?{title:A.title}:{}});C.innerHTML=C0(G);for(let W of C.querySelectorAll("button[data-action]")){let{action:_}=W.dataset;W.addEventListener("click",()=>{if(_==="start")V.startRecording();else if(_==="mute")V.stopRecording();else if(_==="end")V.close()})}if(V.error&&V.error!==T)T=V.error,A.onError?.(V.error);if(V.status!==c)c=V.status,A.onStatusChange?.(V.status)},I=V.subscribe(U);if(U(),A.autoStart)V.startRecording();return{controller:V,async end(){await V.close()},mute(){V.stopRecording()},async start(){await V.startRecording()},unmount(){I(),V.close(),C.innerHTML=""}}},_0="0.0.22-beta.516",T0={mount:c0,version:_0};if(typeof globalThis<"u")globalThis.AbsoluteVoice=T0;var gg=T0;})();
7
+ ${g.partial?`<p style="font-size:13px;margin:8px 0 12px;opacity:0.85;word-break:break-word;">“${N(g.partial)}”</p>`:""}
8
+ <div style="display:flex;gap:10px;">${$.join("")}</div>
9
+ ${g.errorMessage?`<p style="color:${A.errorAccent};font-size:12px;margin-top:12px;">${N(g.errorMessage)}</p>`:""}
10
+ </div>`};var Ag=(g)=>{if(typeof g!=="string")return g;let A=document.querySelector(g);if(!A)throw Error(`AbsoluteVoice.mount: no element matches "${g}"`);return A},T0=(g,A={})=>{let C=Ag(g),V=A0(A.path??"/voice",A.controllerOptions),$=null,_=null,L=()=>{let c=C0({...A.labels!==void 0?{labels:A.labels}:{},state:{assistantAudio:V.assistantAudio,error:V.error,isConnected:V.isConnected,isRecording:V.isRecording,partial:V.partial,status:V.status,turns:V.turns},...A.theme!==void 0?{theme:A.theme}:{},...A.title!==void 0?{title:A.title}:{}});C.innerHTML=_0(c);for(let W of C.querySelectorAll("button[data-action]")){let{action:R}=W.dataset;W.addEventListener("click",()=>{if(R==="start")V.startRecording();else if(R==="mute")V.stopRecording();else if(R==="end")V.close()})}if(V.error&&V.error!==$)$=V.error,A.onError?.(V.error);if(V.status!==_)_=V.status,A.onStatusChange?.(V.status)},U=V.subscribe(L);if(L(),A.autoStart)V.startRecording();return{controller:V,async end(){await V.close()},mute(){V.stopRecording()},async start(){await V.startRecording()},unmount(){U(),V.close(),C.innerHTML=""}}},$0="0.0.22-beta.516",H0={mount:T0,version:$0};if(typeof globalThis<"u")globalThis.AbsoluteVoice=H0;var Cg=H0;})();
@@ -11447,6 +11447,13 @@ var RECONNECT_BASE_DELAY_MS = 500;
11447
11447
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
11448
11448
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
11449
11449
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
11450
+
11451
+ class VoiceReconnectRejectedError extends Error {
11452
+ constructor(message = "Voice authorization could not be renewed.") {
11453
+ super(message);
11454
+ this.name = "VoiceReconnectRejectedError";
11455
+ }
11456
+ }
11450
11457
  var noop = () => {};
11451
11458
  var noopAsync = () => Promise.resolve();
11452
11459
  var noopUnsubscribe = () => noop;
@@ -11526,6 +11533,8 @@ var createVoiceConnection = (path, options = {}) => {
11526
11533
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
11527
11534
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
11528
11535
  const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
11536
+ let lifecycleGeneration = 0;
11537
+ let disposed = false;
11529
11538
  const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
11530
11539
  const state = {
11531
11540
  isConnected: false,
@@ -11589,6 +11598,9 @@ var createVoiceConnection = (path, options = {}) => {
11589
11598
  pendingCallControls.clear();
11590
11599
  };
11591
11600
  const scheduleReconnect = () => {
11601
+ if (disposed)
11602
+ return;
11603
+ const scheduledGeneration = lifecycleGeneration;
11592
11604
  state.reconnectAttempts += 1;
11593
11605
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
11594
11606
  const nextAttemptAt = Date.now() + delayMs;
@@ -11603,6 +11615,8 @@ var createVoiceConnection = (path, options = {}) => {
11603
11615
  type: "connection"
11604
11616
  });
11605
11617
  state.reconnectTimeout = setTimeout(async () => {
11618
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
11619
+ return;
11606
11620
  if (state.reconnectAttempts > maxReconnectAttempts) {
11607
11621
  emitTerminalFailure("Voice connection could not be restored.");
11608
11622
  return;
@@ -11614,7 +11628,13 @@ var createVoiceConnection = (path, options = {}) => {
11614
11628
  scenarioId: state.scenarioId,
11615
11629
  sessionId: state.sessionId
11616
11630
  });
11617
- } catch {
11631
+ } catch (error) {
11632
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
11633
+ return;
11634
+ if (error instanceof VoiceReconnectRejectedError) {
11635
+ emitTerminalFailure(error.message);
11636
+ return;
11637
+ }
11618
11638
  if (state.reconnectAttempts < maxReconnectAttempts) {
11619
11639
  scheduleReconnect();
11620
11640
  } else {
@@ -11622,10 +11642,14 @@ var createVoiceConnection = (path, options = {}) => {
11622
11642
  }
11623
11643
  return;
11624
11644
  }
11645
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
11646
+ return;
11625
11647
  connect();
11626
11648
  }, delayMs);
11627
11649
  };
11628
11650
  const connect = () => {
11651
+ if (disposed)
11652
+ return;
11629
11653
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
11630
11654
  ws.binaryType = "arraybuffer";
11631
11655
  ws.onopen = () => {
@@ -11696,6 +11720,7 @@ var createVoiceConnection = (path, options = {}) => {
11696
11720
  state.ws = null;
11697
11721
  state.isConnected = false;
11698
11722
  clearTimers();
11723
+ rejectPendingCallControls("Voice connection interrupted");
11699
11724
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
11700
11725
  const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
11701
11726
  if (authorizationFailure) {
@@ -11761,6 +11786,9 @@ var createVoiceConnection = (path, options = {}) => {
11761
11786
  send({ type: "end_turn" });
11762
11787
  };
11763
11788
  const callControl = (message) => {
11789
+ if (state.ws?.readyState !== WS_OPEN) {
11790
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
11791
+ }
11764
11792
  const requestId = crypto.randomUUID();
11765
11793
  const { promise, reject, resolve } = Promise.withResolvers();
11766
11794
  const timeout = setTimeout(() => {
@@ -11768,16 +11796,19 @@ var createVoiceConnection = (path, options = {}) => {
11768
11796
  reject(new Error(`${message.action} acknowledgement timed out`));
11769
11797
  }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
11770
11798
  pendingCallControls.set(requestId, { reject, resolve, timeout });
11771
- send({
11799
+ state.ws.send(JSON.stringify({
11772
11800
  ...message,
11773
11801
  requestId,
11774
11802
  type: "call_control"
11775
- });
11803
+ }));
11776
11804
  return promise;
11777
11805
  };
11778
11806
  const close = (reason = "client-close") => {
11807
+ disposed = true;
11808
+ lifecycleGeneration += 1;
11779
11809
  clearTimers();
11780
11810
  rejectPendingCallControls("Voice connection closed");
11811
+ state.pendingMessages.length = 0;
11781
11812
  if (state.ws) {
11782
11813
  const ws = state.ws;
11783
11814
  state.ws = null;
@@ -11790,8 +11821,11 @@ var createVoiceConnection = (path, options = {}) => {
11790
11821
  listeners.clear();
11791
11822
  };
11792
11823
  const disconnect = () => {
11824
+ disposed = true;
11825
+ lifecycleGeneration += 1;
11793
11826
  clearTimers();
11794
11827
  rejectPendingCallControls("Voice connection disconnected");
11828
+ state.pendingMessages.length = 0;
11795
11829
  if (state.ws) {
11796
11830
  const ws = state.ws;
11797
11831
  state.ws = null;
@@ -775,6 +775,13 @@ var RECONNECT_BASE_DELAY_MS = 500;
775
775
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
776
776
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
777
777
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
778
+
779
+ class VoiceReconnectRejectedError extends Error {
780
+ constructor(message = "Voice authorization could not be renewed.") {
781
+ super(message);
782
+ this.name = "VoiceReconnectRejectedError";
783
+ }
784
+ }
778
785
  var noop = () => {};
779
786
  var noopAsync = () => Promise.resolve();
780
787
  var noopUnsubscribe = () => noop;
@@ -854,6 +861,8 @@ var createVoiceConnection = (path, options = {}) => {
854
861
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
855
862
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
856
863
  const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
864
+ let lifecycleGeneration = 0;
865
+ let disposed = false;
857
866
  const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
858
867
  const state = {
859
868
  isConnected: false,
@@ -917,6 +926,9 @@ var createVoiceConnection = (path, options = {}) => {
917
926
  pendingCallControls.clear();
918
927
  };
919
928
  const scheduleReconnect = () => {
929
+ if (disposed)
930
+ return;
931
+ const scheduledGeneration = lifecycleGeneration;
920
932
  state.reconnectAttempts += 1;
921
933
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
922
934
  const nextAttemptAt = Date.now() + delayMs;
@@ -931,6 +943,8 @@ var createVoiceConnection = (path, options = {}) => {
931
943
  type: "connection"
932
944
  });
933
945
  state.reconnectTimeout = setTimeout(async () => {
946
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
947
+ return;
934
948
  if (state.reconnectAttempts > maxReconnectAttempts) {
935
949
  emitTerminalFailure("Voice connection could not be restored.");
936
950
  return;
@@ -942,7 +956,13 @@ var createVoiceConnection = (path, options = {}) => {
942
956
  scenarioId: state.scenarioId,
943
957
  sessionId: state.sessionId
944
958
  });
945
- } catch {
959
+ } catch (error) {
960
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
961
+ return;
962
+ if (error instanceof VoiceReconnectRejectedError) {
963
+ emitTerminalFailure(error.message);
964
+ return;
965
+ }
946
966
  if (state.reconnectAttempts < maxReconnectAttempts) {
947
967
  scheduleReconnect();
948
968
  } else {
@@ -950,10 +970,14 @@ var createVoiceConnection = (path, options = {}) => {
950
970
  }
951
971
  return;
952
972
  }
973
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
974
+ return;
953
975
  connect();
954
976
  }, delayMs);
955
977
  };
956
978
  const connect = () => {
979
+ if (disposed)
980
+ return;
957
981
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
958
982
  ws.binaryType = "arraybuffer";
959
983
  ws.onopen = () => {
@@ -1024,6 +1048,7 @@ var createVoiceConnection = (path, options = {}) => {
1024
1048
  state.ws = null;
1025
1049
  state.isConnected = false;
1026
1050
  clearTimers();
1051
+ rejectPendingCallControls("Voice connection interrupted");
1027
1052
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
1028
1053
  const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
1029
1054
  if (authorizationFailure) {
@@ -1089,6 +1114,9 @@ var createVoiceConnection = (path, options = {}) => {
1089
1114
  send({ type: "end_turn" });
1090
1115
  };
1091
1116
  const callControl = (message) => {
1117
+ if (state.ws?.readyState !== WS_OPEN) {
1118
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
1119
+ }
1092
1120
  const requestId = crypto.randomUUID();
1093
1121
  const { promise, reject, resolve } = Promise.withResolvers();
1094
1122
  const timeout = setTimeout(() => {
@@ -1096,16 +1124,19 @@ var createVoiceConnection = (path, options = {}) => {
1096
1124
  reject(new Error(`${message.action} acknowledgement timed out`));
1097
1125
  }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
1098
1126
  pendingCallControls.set(requestId, { reject, resolve, timeout });
1099
- send({
1127
+ state.ws.send(JSON.stringify({
1100
1128
  ...message,
1101
1129
  requestId,
1102
1130
  type: "call_control"
1103
- });
1131
+ }));
1104
1132
  return promise;
1105
1133
  };
1106
1134
  const close = (reason = "client-close") => {
1135
+ disposed = true;
1136
+ lifecycleGeneration += 1;
1107
1137
  clearTimers();
1108
1138
  rejectPendingCallControls("Voice connection closed");
1139
+ state.pendingMessages.length = 0;
1109
1140
  if (state.ws) {
1110
1141
  const ws = state.ws;
1111
1142
  state.ws = null;
@@ -1118,8 +1149,11 @@ var createVoiceConnection = (path, options = {}) => {
1118
1149
  listeners.clear();
1119
1150
  };
1120
1151
  const disconnect = () => {
1152
+ disposed = true;
1153
+ lifecycleGeneration += 1;
1121
1154
  clearTimers();
1122
1155
  rejectPendingCallControls("Voice connection disconnected");
1156
+ state.pendingMessages.length = 0;
1123
1157
  if (state.ws) {
1124
1158
  const ws = state.ws;
1125
1159
  state.ws = null;
@@ -2996,6 +2996,13 @@ var RECONNECT_BASE_DELAY_MS = 500;
2996
2996
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
2997
2997
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
2998
2998
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
2999
+
3000
+ class VoiceReconnectRejectedError extends Error {
3001
+ constructor(message = "Voice authorization could not be renewed.") {
3002
+ super(message);
3003
+ this.name = "VoiceReconnectRejectedError";
3004
+ }
3005
+ }
2999
3006
  var noop = () => {};
3000
3007
  var noopAsync = () => Promise.resolve();
3001
3008
  var noopUnsubscribe = () => noop;
@@ -3075,6 +3082,8 @@ var createVoiceConnection = (path, options = {}) => {
3075
3082
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
3076
3083
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
3077
3084
  const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
3085
+ let lifecycleGeneration = 0;
3086
+ let disposed = false;
3078
3087
  const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
3079
3088
  const state = {
3080
3089
  isConnected: false,
@@ -3138,6 +3147,9 @@ var createVoiceConnection = (path, options = {}) => {
3138
3147
  pendingCallControls.clear();
3139
3148
  };
3140
3149
  const scheduleReconnect = () => {
3150
+ if (disposed)
3151
+ return;
3152
+ const scheduledGeneration = lifecycleGeneration;
3141
3153
  state.reconnectAttempts += 1;
3142
3154
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
3143
3155
  const nextAttemptAt = Date.now() + delayMs;
@@ -3152,6 +3164,8 @@ var createVoiceConnection = (path, options = {}) => {
3152
3164
  type: "connection"
3153
3165
  });
3154
3166
  state.reconnectTimeout = setTimeout(async () => {
3167
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
3168
+ return;
3155
3169
  if (state.reconnectAttempts > maxReconnectAttempts) {
3156
3170
  emitTerminalFailure("Voice connection could not be restored.");
3157
3171
  return;
@@ -3163,7 +3177,13 @@ var createVoiceConnection = (path, options = {}) => {
3163
3177
  scenarioId: state.scenarioId,
3164
3178
  sessionId: state.sessionId
3165
3179
  });
3166
- } catch {
3180
+ } catch (error) {
3181
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
3182
+ return;
3183
+ if (error instanceof VoiceReconnectRejectedError) {
3184
+ emitTerminalFailure(error.message);
3185
+ return;
3186
+ }
3167
3187
  if (state.reconnectAttempts < maxReconnectAttempts) {
3168
3188
  scheduleReconnect();
3169
3189
  } else {
@@ -3171,10 +3191,14 @@ var createVoiceConnection = (path, options = {}) => {
3171
3191
  }
3172
3192
  return;
3173
3193
  }
3194
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
3195
+ return;
3174
3196
  connect();
3175
3197
  }, delayMs);
3176
3198
  };
3177
3199
  const connect = () => {
3200
+ if (disposed)
3201
+ return;
3178
3202
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
3179
3203
  ws.binaryType = "arraybuffer";
3180
3204
  ws.onopen = () => {
@@ -3245,6 +3269,7 @@ var createVoiceConnection = (path, options = {}) => {
3245
3269
  state.ws = null;
3246
3270
  state.isConnected = false;
3247
3271
  clearTimers();
3272
+ rejectPendingCallControls("Voice connection interrupted");
3248
3273
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
3249
3274
  const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
3250
3275
  if (authorizationFailure) {
@@ -3310,6 +3335,9 @@ var createVoiceConnection = (path, options = {}) => {
3310
3335
  send({ type: "end_turn" });
3311
3336
  };
3312
3337
  const callControl = (message) => {
3338
+ if (state.ws?.readyState !== WS_OPEN) {
3339
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
3340
+ }
3313
3341
  const requestId = crypto.randomUUID();
3314
3342
  const { promise, reject, resolve } = Promise.withResolvers();
3315
3343
  const timeout = setTimeout(() => {
@@ -3317,16 +3345,19 @@ var createVoiceConnection = (path, options = {}) => {
3317
3345
  reject(new Error(`${message.action} acknowledgement timed out`));
3318
3346
  }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
3319
3347
  pendingCallControls.set(requestId, { reject, resolve, timeout });
3320
- send({
3348
+ state.ws.send(JSON.stringify({
3321
3349
  ...message,
3322
3350
  requestId,
3323
3351
  type: "call_control"
3324
- });
3352
+ }));
3325
3353
  return promise;
3326
3354
  };
3327
3355
  const close = (reason = "client-close") => {
3356
+ disposed = true;
3357
+ lifecycleGeneration += 1;
3328
3358
  clearTimers();
3329
3359
  rejectPendingCallControls("Voice connection closed");
3360
+ state.pendingMessages.length = 0;
3330
3361
  if (state.ws) {
3331
3362
  const ws = state.ws;
3332
3363
  state.ws = null;
@@ -3339,8 +3370,11 @@ var createVoiceConnection = (path, options = {}) => {
3339
3370
  listeners.clear();
3340
3371
  };
3341
3372
  const disconnect = () => {
3373
+ disposed = true;
3374
+ lifecycleGeneration += 1;
3342
3375
  clearTimers();
3343
3376
  rejectPendingCallControls("Voice connection disconnected");
3377
+ state.pendingMessages.length = 0;
3344
3378
  if (state.ws) {
3345
3379
  const ws = state.ws;
3346
3380
  state.ws = null;
package/dist/vue/index.js CHANGED
@@ -10843,6 +10843,13 @@ var RECONNECT_BASE_DELAY_MS = 500;
10843
10843
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
10844
10844
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
10845
10845
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
10846
+
10847
+ class VoiceReconnectRejectedError extends Error {
10848
+ constructor(message = "Voice authorization could not be renewed.") {
10849
+ super(message);
10850
+ this.name = "VoiceReconnectRejectedError";
10851
+ }
10852
+ }
10846
10853
  var noop = () => {};
10847
10854
  var noopAsync = () => Promise.resolve();
10848
10855
  var noopUnsubscribe = () => noop;
@@ -10922,6 +10929,8 @@ var createVoiceConnection = (path, options = {}) => {
10922
10929
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
10923
10930
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
10924
10931
  const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
10932
+ let lifecycleGeneration = 0;
10933
+ let disposed = false;
10925
10934
  const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
10926
10935
  const state = {
10927
10936
  isConnected: false,
@@ -10985,6 +10994,9 @@ var createVoiceConnection = (path, options = {}) => {
10985
10994
  pendingCallControls.clear();
10986
10995
  };
10987
10996
  const scheduleReconnect = () => {
10997
+ if (disposed)
10998
+ return;
10999
+ const scheduledGeneration = lifecycleGeneration;
10988
11000
  state.reconnectAttempts += 1;
10989
11001
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
10990
11002
  const nextAttemptAt = Date.now() + delayMs;
@@ -10999,6 +11011,8 @@ var createVoiceConnection = (path, options = {}) => {
10999
11011
  type: "connection"
11000
11012
  });
11001
11013
  state.reconnectTimeout = setTimeout(async () => {
11014
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
11015
+ return;
11002
11016
  if (state.reconnectAttempts > maxReconnectAttempts) {
11003
11017
  emitTerminalFailure("Voice connection could not be restored.");
11004
11018
  return;
@@ -11010,7 +11024,13 @@ var createVoiceConnection = (path, options = {}) => {
11010
11024
  scenarioId: state.scenarioId,
11011
11025
  sessionId: state.sessionId
11012
11026
  });
11013
- } catch {
11027
+ } catch (error) {
11028
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
11029
+ return;
11030
+ if (error instanceof VoiceReconnectRejectedError) {
11031
+ emitTerminalFailure(error.message);
11032
+ return;
11033
+ }
11014
11034
  if (state.reconnectAttempts < maxReconnectAttempts) {
11015
11035
  scheduleReconnect();
11016
11036
  } else {
@@ -11018,10 +11038,14 @@ var createVoiceConnection = (path, options = {}) => {
11018
11038
  }
11019
11039
  return;
11020
11040
  }
11041
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
11042
+ return;
11021
11043
  connect();
11022
11044
  }, delayMs);
11023
11045
  };
11024
11046
  const connect = () => {
11047
+ if (disposed)
11048
+ return;
11025
11049
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
11026
11050
  ws.binaryType = "arraybuffer";
11027
11051
  ws.onopen = () => {
@@ -11092,6 +11116,7 @@ var createVoiceConnection = (path, options = {}) => {
11092
11116
  state.ws = null;
11093
11117
  state.isConnected = false;
11094
11118
  clearTimers();
11119
+ rejectPendingCallControls("Voice connection interrupted");
11095
11120
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
11096
11121
  const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
11097
11122
  if (authorizationFailure) {
@@ -11157,6 +11182,9 @@ var createVoiceConnection = (path, options = {}) => {
11157
11182
  send({ type: "end_turn" });
11158
11183
  };
11159
11184
  const callControl = (message) => {
11185
+ if (state.ws?.readyState !== WS_OPEN) {
11186
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
11187
+ }
11160
11188
  const requestId = crypto.randomUUID();
11161
11189
  const { promise, reject, resolve } = Promise.withResolvers();
11162
11190
  const timeout = setTimeout(() => {
@@ -11164,16 +11192,19 @@ var createVoiceConnection = (path, options = {}) => {
11164
11192
  reject(new Error(`${message.action} acknowledgement timed out`));
11165
11193
  }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
11166
11194
  pendingCallControls.set(requestId, { reject, resolve, timeout });
11167
- send({
11195
+ state.ws.send(JSON.stringify({
11168
11196
  ...message,
11169
11197
  requestId,
11170
11198
  type: "call_control"
11171
- });
11199
+ }));
11172
11200
  return promise;
11173
11201
  };
11174
11202
  const close = (reason = "client-close") => {
11203
+ disposed = true;
11204
+ lifecycleGeneration += 1;
11175
11205
  clearTimers();
11176
11206
  rejectPendingCallControls("Voice connection closed");
11207
+ state.pendingMessages.length = 0;
11177
11208
  if (state.ws) {
11178
11209
  const ws = state.ws;
11179
11210
  state.ws = null;
@@ -11186,8 +11217,11 @@ var createVoiceConnection = (path, options = {}) => {
11186
11217
  listeners.clear();
11187
11218
  };
11188
11219
  const disconnect = () => {
11220
+ disposed = true;
11221
+ lifecycleGeneration += 1;
11189
11222
  clearTimers();
11190
11223
  rejectPendingCallControls("Voice connection disconnected");
11224
+ state.pendingMessages.length = 0;
11191
11225
  if (state.ws) {
11192
11226
  const ws = state.ws;
11193
11227
  state.ws = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.655",
3
+ "version": "0.0.22-beta.656",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",