@absolutejs/voice 0.0.22-beta.653 → 0.0.22-beta.655

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.
@@ -455,8 +455,11 @@ var serverMessageToAction = (message) => {
455
455
  case "error":
456
456
  return {
457
457
  message: normalizeErrorMessage(message.message),
458
+ recoverable: message.recoverable,
458
459
  type: "error"
459
460
  };
461
+ case "call_control_ack":
462
+ return null;
460
463
  case "final":
461
464
  return {
462
465
  transcript: message.transcript,
@@ -584,14 +587,17 @@ var WS_FORBIDDEN = 4403;
584
587
  var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
585
588
  var DEFAULT_PING_INTERVAL = 30000;
586
589
  var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
590
+ var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
591
+ var MAX_PENDING_CONTROL_MESSAGES = 100;
587
592
  var RECONNECT_BASE_DELAY_MS = 500;
588
593
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
589
594
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
590
595
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
591
596
  var noop = () => {};
597
+ var noopAsync = () => Promise.resolve();
592
598
  var noopUnsubscribe = () => noop;
593
599
  var NOOP_CONNECTION = {
594
- callControl: noop,
600
+ callControl: noopAsync,
595
601
  close: noop,
596
602
  disconnect: noop,
597
603
  endTurn: noop,
@@ -629,6 +635,7 @@ var isVoiceServerMessage = (value) => {
629
635
  case "audio":
630
636
  case "assistant":
631
637
  case "call_lifecycle":
638
+ case "call_control_ack":
632
639
  case "complete":
633
640
  case "connection":
634
641
  case "error":
@@ -659,6 +666,7 @@ var createVoiceConnection = (path, options = {}) => {
659
666
  return NOOP_CONNECTION;
660
667
  }
661
668
  const listeners = new Set;
669
+ const pendingCallControls = new Map;
662
670
  const shouldReconnect = options.reconnect !== false;
663
671
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
664
672
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
@@ -679,6 +687,21 @@ var createVoiceConnection = (path, options = {}) => {
679
687
  const emitConnection = (reconnect) => {
680
688
  listeners.forEach((listener) => listener(reconnect));
681
689
  };
690
+ const emitTerminalFailure = (message) => {
691
+ listeners.forEach((listener) => listener({
692
+ message,
693
+ recoverable: false,
694
+ type: "error"
695
+ }));
696
+ emitConnection({
697
+ reconnect: {
698
+ attempts: state.reconnectAttempts,
699
+ maxAttempts: maxReconnectAttempts,
700
+ status: "exhausted"
701
+ },
702
+ type: "connection"
703
+ });
704
+ };
682
705
  const clearTimers = () => {
683
706
  if (state.pingInterval) {
684
707
  clearInterval(state.pingInterval);
@@ -704,6 +727,13 @@ var createVoiceConnection = (path, options = {}) => {
704
727
  }
705
728
  }
706
729
  };
730
+ const rejectPendingCallControls = (message) => {
731
+ for (const pending of pendingCallControls.values()) {
732
+ clearTimeout(pending.timeout);
733
+ pending.reject(new Error(message));
734
+ }
735
+ pendingCallControls.clear();
736
+ };
707
737
  const scheduleReconnect = () => {
708
738
  state.reconnectAttempts += 1;
709
739
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
@@ -718,16 +748,24 @@ var createVoiceConnection = (path, options = {}) => {
718
748
  },
719
749
  type: "connection"
720
750
  });
721
- state.reconnectTimeout = setTimeout(() => {
751
+ state.reconnectTimeout = setTimeout(async () => {
722
752
  if (state.reconnectAttempts > maxReconnectAttempts) {
723
- emitConnection({
724
- reconnect: {
725
- attempts: state.reconnectAttempts,
726
- maxAttempts: maxReconnectAttempts,
727
- status: "exhausted"
728
- },
729
- type: "connection"
753
+ emitTerminalFailure("Voice connection could not be restored.");
754
+ return;
755
+ }
756
+ try {
757
+ await options.prepareReconnect?.({
758
+ attempt: state.reconnectAttempts,
759
+ path,
760
+ scenarioId: state.scenarioId,
761
+ sessionId: state.sessionId
730
762
  });
763
+ } catch {
764
+ if (state.reconnectAttempts < maxReconnectAttempts) {
765
+ scheduleReconnect();
766
+ } else {
767
+ emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
768
+ }
731
769
  return;
732
770
  }
733
771
  connect();
@@ -784,6 +822,18 @@ var createVoiceConnection = (path, options = {}) => {
784
822
  state.sessionId = parsed.sessionId;
785
823
  state.scenarioId = parsed.scenarioId ?? state.scenarioId;
786
824
  }
825
+ if (parsed.type === "call_control_ack") {
826
+ const pending = pendingCallControls.get(parsed.requestId);
827
+ if (pending) {
828
+ clearTimeout(pending.timeout);
829
+ pendingCallControls.delete(parsed.requestId);
830
+ if (parsed.ok) {
831
+ pending.resolve();
832
+ } else {
833
+ pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
834
+ }
835
+ }
836
+ }
787
837
  listeners.forEach((listener) => listener(parsed));
788
838
  };
789
839
  ws.onclose = (event) => {
@@ -793,18 +843,30 @@ var createVoiceConnection = (path, options = {}) => {
793
843
  state.isConnected = false;
794
844
  clearTimers();
795
845
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
846
+ const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
847
+ if (authorizationFailure) {
848
+ listeners.forEach((listener) => listener({
849
+ message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
850
+ recoverable: false,
851
+ type: "error"
852
+ }));
853
+ }
796
854
  if (reconnectable) {
797
855
  scheduleReconnect();
798
856
  } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
799
- emitConnection({
800
- reconnect: {
801
- attempts: state.reconnectAttempts,
802
- lastDisconnectAt: Date.now(),
803
- maxAttempts: maxReconnectAttempts,
804
- status: "exhausted"
805
- },
806
- type: "connection"
807
- });
857
+ if (authorizationFailure) {
858
+ emitConnection({
859
+ reconnect: {
860
+ attempts: state.reconnectAttempts,
861
+ lastDisconnectAt: Date.now(),
862
+ maxAttempts: maxReconnectAttempts,
863
+ status: "exhausted"
864
+ },
865
+ type: "connection"
866
+ });
867
+ } else {
868
+ emitTerminalFailure("Voice connection could not be restored.");
869
+ }
808
870
  }
809
871
  };
810
872
  state.ws = ws;
@@ -814,6 +876,12 @@ var createVoiceConnection = (path, options = {}) => {
814
876
  state.ws.send(value);
815
877
  return;
816
878
  }
879
+ if (typeof value !== "string") {
880
+ return;
881
+ }
882
+ if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
883
+ state.pendingMessages.shift();
884
+ }
817
885
  state.pendingMessages.push(value);
818
886
  };
819
887
  const send = (message) => {
@@ -839,13 +907,23 @@ var createVoiceConnection = (path, options = {}) => {
839
907
  send({ type: "end_turn" });
840
908
  };
841
909
  const callControl = (message) => {
910
+ const requestId = crypto.randomUUID();
911
+ const { promise, reject, resolve } = Promise.withResolvers();
912
+ const timeout = setTimeout(() => {
913
+ pendingCallControls.delete(requestId);
914
+ reject(new Error(`${message.action} acknowledgement timed out`));
915
+ }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
916
+ pendingCallControls.set(requestId, { reject, resolve, timeout });
842
917
  send({
843
918
  ...message,
919
+ requestId,
844
920
  type: "call_control"
845
921
  });
922
+ return promise;
846
923
  };
847
924
  const close = (reason = "client-close") => {
848
925
  clearTimers();
926
+ rejectPendingCallControls("Voice connection closed");
849
927
  if (state.ws) {
850
928
  const ws = state.ws;
851
929
  state.ws = null;
@@ -859,6 +937,7 @@ var createVoiceConnection = (path, options = {}) => {
859
937
  };
860
938
  const disconnect = () => {
861
939
  clearTimers();
940
+ rejectPendingCallControls("Voice connection disconnected");
862
941
  if (state.ws) {
863
942
  const ws = state.ws;
864
943
  state.ws = null;
@@ -1021,7 +1100,8 @@ var createVoiceStreamStore = () => {
1021
1100
  case "error":
1022
1101
  state = {
1023
1102
  ...state,
1024
- error: action.message
1103
+ error: action.message,
1104
+ ...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
1025
1105
  };
1026
1106
  break;
1027
1107
  case "final":
@@ -1161,7 +1241,7 @@ var createVoiceStream = (path, options = {}) => {
1161
1241
  return store.getSnapshot().call;
1162
1242
  },
1163
1243
  callControl(message) {
1164
- connection.callControl(message);
1244
+ return connection.callControl(message);
1165
1245
  },
1166
1246
  close(reason) {
1167
1247
  unsubscribeConnection();
@@ -10,7 +10,7 @@ export declare class VoiceStreamService {
10
10
  assistantStreamingText: import("@angular/core").Signal<string>;
11
11
  assistantTexts: import("@angular/core").Signal<string[]>;
12
12
  call: import("@angular/core").Signal<import("..").VoiceCallLifecycleState | null>;
13
- callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "type">) => void>[0]) => void;
13
+ callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "requestId" | "type">) => Promise<void>>[0]) => Promise<void>;
14
14
  close: () => void;
15
15
  endTurn: () => void;
16
16
  error: import("@angular/core").Signal<string | null>;
@@ -11,6 +11,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
11
11
  reconnect?: undefined;
12
12
  event?: undefined;
13
13
  message?: undefined;
14
+ recoverable?: undefined;
14
15
  transcript?: undefined;
15
16
  assistantTexts?: undefined;
16
17
  call?: undefined;
@@ -32,6 +33,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
32
33
  reconnect?: undefined;
33
34
  event?: undefined;
34
35
  message?: undefined;
36
+ recoverable?: undefined;
35
37
  transcript?: undefined;
36
38
  assistantTexts?: undefined;
37
39
  call?: undefined;
@@ -53,6 +55,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
53
55
  reconnect?: undefined;
54
56
  event?: undefined;
55
57
  message?: undefined;
58
+ recoverable?: undefined;
56
59
  transcript?: undefined;
57
60
  assistantTexts?: undefined;
58
61
  call?: undefined;
@@ -74,6 +77,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
74
77
  reconnect?: undefined;
75
78
  event?: undefined;
76
79
  message?: undefined;
80
+ recoverable?: undefined;
77
81
  transcript?: undefined;
78
82
  assistantTexts?: undefined;
79
83
  call?: undefined;
@@ -95,6 +99,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
95
99
  sessionId?: undefined;
96
100
  event?: undefined;
97
101
  message?: undefined;
102
+ recoverable?: undefined;
98
103
  transcript?: undefined;
99
104
  assistantTexts?: undefined;
100
105
  call?: undefined;
@@ -116,6 +121,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
116
121
  delta?: undefined;
117
122
  reconnect?: undefined;
118
123
  message?: undefined;
124
+ recoverable?: undefined;
119
125
  transcript?: undefined;
120
126
  assistantTexts?: undefined;
121
127
  call?: undefined;
@@ -127,6 +133,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
127
133
  turn?: undefined;
128
134
  } | {
129
135
  message: string;
136
+ recoverable: boolean | undefined;
130
137
  type: "error";
131
138
  chunk?: undefined;
132
139
  format?: undefined;
@@ -159,6 +166,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
159
166
  reconnect?: undefined;
160
167
  event?: undefined;
161
168
  message?: undefined;
169
+ recoverable?: undefined;
162
170
  assistantTexts?: undefined;
163
171
  call?: undefined;
164
172
  partial?: undefined;
@@ -180,6 +188,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
180
188
  reconnect?: undefined;
181
189
  event?: undefined;
182
190
  message?: undefined;
191
+ recoverable?: undefined;
183
192
  assistantTexts?: undefined;
184
193
  call?: undefined;
185
194
  partial?: undefined;
@@ -207,6 +216,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
207
216
  reconnect?: undefined;
208
217
  event?: undefined;
209
218
  message?: undefined;
219
+ recoverable?: undefined;
210
220
  transcript?: undefined;
211
221
  turn?: undefined;
212
222
  } | {
@@ -224,6 +234,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
224
234
  reconnect?: undefined;
225
235
  event?: undefined;
226
236
  message?: undefined;
237
+ recoverable?: undefined;
227
238
  transcript?: undefined;
228
239
  assistantTexts?: undefined;
229
240
  call?: undefined;
@@ -243,6 +254,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
243
254
  reconnect?: undefined;
244
255
  event?: undefined;
245
256
  message?: undefined;
257
+ recoverable?: undefined;
246
258
  transcript?: undefined;
247
259
  assistantTexts?: undefined;
248
260
  call?: undefined;
@@ -6,7 +6,7 @@ export declare const computeVoiceReconnectDelayMs: (attempt: number, baseMs: num
6
6
  type VoiceConnectionHandle = {
7
7
  callControl: (message: Omit<VoiceClientMessage & {
8
8
  type: "call_control";
9
- }, "type">) => void;
9
+ }, "requestId" | "type">) => Promise<void>;
10
10
  start: (input?: {
11
11
  sessionId?: string;
12
12
  scenarioId?: string;
@@ -223,8 +223,11 @@ var serverMessageToAction = (message) => {
223
223
  case "error":
224
224
  return {
225
225
  message: normalizeErrorMessage(message.message),
226
+ recoverable: message.recoverable,
226
227
  type: "error"
227
228
  };
229
+ case "call_control_ack":
230
+ return null;
228
231
  case "final":
229
232
  return {
230
233
  transcript: message.transcript,
@@ -501,14 +504,17 @@ var WS_FORBIDDEN = 4403;
501
504
  var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
502
505
  var DEFAULT_PING_INTERVAL = 30000;
503
506
  var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
507
+ var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
508
+ var MAX_PENDING_CONTROL_MESSAGES = 100;
504
509
  var RECONNECT_BASE_DELAY_MS = 500;
505
510
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
506
511
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
507
512
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
508
513
  var noop = () => {};
514
+ var noopAsync = () => Promise.resolve();
509
515
  var noopUnsubscribe = () => noop;
510
516
  var NOOP_CONNECTION = {
511
- callControl: noop,
517
+ callControl: noopAsync,
512
518
  close: noop,
513
519
  disconnect: noop,
514
520
  endTurn: noop,
@@ -546,6 +552,7 @@ var isVoiceServerMessage = (value) => {
546
552
  case "audio":
547
553
  case "assistant":
548
554
  case "call_lifecycle":
555
+ case "call_control_ack":
549
556
  case "complete":
550
557
  case "connection":
551
558
  case "error":
@@ -576,6 +583,7 @@ var createVoiceConnection = (path, options = {}) => {
576
583
  return NOOP_CONNECTION;
577
584
  }
578
585
  const listeners = new Set;
586
+ const pendingCallControls = new Map;
579
587
  const shouldReconnect = options.reconnect !== false;
580
588
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
581
589
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
@@ -596,6 +604,21 @@ var createVoiceConnection = (path, options = {}) => {
596
604
  const emitConnection = (reconnect) => {
597
605
  listeners.forEach((listener) => listener(reconnect));
598
606
  };
607
+ const emitTerminalFailure = (message) => {
608
+ listeners.forEach((listener) => listener({
609
+ message,
610
+ recoverable: false,
611
+ type: "error"
612
+ }));
613
+ emitConnection({
614
+ reconnect: {
615
+ attempts: state.reconnectAttempts,
616
+ maxAttempts: maxReconnectAttempts,
617
+ status: "exhausted"
618
+ },
619
+ type: "connection"
620
+ });
621
+ };
599
622
  const clearTimers = () => {
600
623
  if (state.pingInterval) {
601
624
  clearInterval(state.pingInterval);
@@ -621,6 +644,13 @@ var createVoiceConnection = (path, options = {}) => {
621
644
  }
622
645
  }
623
646
  };
647
+ const rejectPendingCallControls = (message) => {
648
+ for (const pending of pendingCallControls.values()) {
649
+ clearTimeout(pending.timeout);
650
+ pending.reject(new Error(message));
651
+ }
652
+ pendingCallControls.clear();
653
+ };
624
654
  const scheduleReconnect = () => {
625
655
  state.reconnectAttempts += 1;
626
656
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
@@ -635,16 +665,24 @@ var createVoiceConnection = (path, options = {}) => {
635
665
  },
636
666
  type: "connection"
637
667
  });
638
- state.reconnectTimeout = setTimeout(() => {
668
+ state.reconnectTimeout = setTimeout(async () => {
639
669
  if (state.reconnectAttempts > maxReconnectAttempts) {
640
- emitConnection({
641
- reconnect: {
642
- attempts: state.reconnectAttempts,
643
- maxAttempts: maxReconnectAttempts,
644
- status: "exhausted"
645
- },
646
- type: "connection"
670
+ emitTerminalFailure("Voice connection could not be restored.");
671
+ return;
672
+ }
673
+ try {
674
+ await options.prepareReconnect?.({
675
+ attempt: state.reconnectAttempts,
676
+ path,
677
+ scenarioId: state.scenarioId,
678
+ sessionId: state.sessionId
647
679
  });
680
+ } catch {
681
+ if (state.reconnectAttempts < maxReconnectAttempts) {
682
+ scheduleReconnect();
683
+ } else {
684
+ emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
685
+ }
648
686
  return;
649
687
  }
650
688
  connect();
@@ -701,6 +739,18 @@ var createVoiceConnection = (path, options = {}) => {
701
739
  state.sessionId = parsed.sessionId;
702
740
  state.scenarioId = parsed.scenarioId ?? state.scenarioId;
703
741
  }
742
+ if (parsed.type === "call_control_ack") {
743
+ const pending = pendingCallControls.get(parsed.requestId);
744
+ if (pending) {
745
+ clearTimeout(pending.timeout);
746
+ pendingCallControls.delete(parsed.requestId);
747
+ if (parsed.ok) {
748
+ pending.resolve();
749
+ } else {
750
+ pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
751
+ }
752
+ }
753
+ }
704
754
  listeners.forEach((listener) => listener(parsed));
705
755
  };
706
756
  ws.onclose = (event) => {
@@ -710,18 +760,30 @@ var createVoiceConnection = (path, options = {}) => {
710
760
  state.isConnected = false;
711
761
  clearTimers();
712
762
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
763
+ const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
764
+ if (authorizationFailure) {
765
+ listeners.forEach((listener) => listener({
766
+ message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
767
+ recoverable: false,
768
+ type: "error"
769
+ }));
770
+ }
713
771
  if (reconnectable) {
714
772
  scheduleReconnect();
715
773
  } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
716
- emitConnection({
717
- reconnect: {
718
- attempts: state.reconnectAttempts,
719
- lastDisconnectAt: Date.now(),
720
- maxAttempts: maxReconnectAttempts,
721
- status: "exhausted"
722
- },
723
- type: "connection"
724
- });
774
+ if (authorizationFailure) {
775
+ emitConnection({
776
+ reconnect: {
777
+ attempts: state.reconnectAttempts,
778
+ lastDisconnectAt: Date.now(),
779
+ maxAttempts: maxReconnectAttempts,
780
+ status: "exhausted"
781
+ },
782
+ type: "connection"
783
+ });
784
+ } else {
785
+ emitTerminalFailure("Voice connection could not be restored.");
786
+ }
725
787
  }
726
788
  };
727
789
  state.ws = ws;
@@ -731,6 +793,12 @@ var createVoiceConnection = (path, options = {}) => {
731
793
  state.ws.send(value);
732
794
  return;
733
795
  }
796
+ if (typeof value !== "string") {
797
+ return;
798
+ }
799
+ if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
800
+ state.pendingMessages.shift();
801
+ }
734
802
  state.pendingMessages.push(value);
735
803
  };
736
804
  const send = (message) => {
@@ -756,13 +824,23 @@ var createVoiceConnection = (path, options = {}) => {
756
824
  send({ type: "end_turn" });
757
825
  };
758
826
  const callControl = (message) => {
827
+ const requestId = crypto.randomUUID();
828
+ const { promise, reject, resolve } = Promise.withResolvers();
829
+ const timeout = setTimeout(() => {
830
+ pendingCallControls.delete(requestId);
831
+ reject(new Error(`${message.action} acknowledgement timed out`));
832
+ }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
833
+ pendingCallControls.set(requestId, { reject, resolve, timeout });
759
834
  send({
760
835
  ...message,
836
+ requestId,
761
837
  type: "call_control"
762
838
  });
839
+ return promise;
763
840
  };
764
841
  const close = (reason = "client-close") => {
765
842
  clearTimers();
843
+ rejectPendingCallControls("Voice connection closed");
766
844
  if (state.ws) {
767
845
  const ws = state.ws;
768
846
  state.ws = null;
@@ -776,6 +854,7 @@ var createVoiceConnection = (path, options = {}) => {
776
854
  };
777
855
  const disconnect = () => {
778
856
  clearTimers();
857
+ rejectPendingCallControls("Voice connection disconnected");
779
858
  if (state.ws) {
780
859
  const ws = state.ws;
781
860
  state.ws = null;
@@ -938,7 +1017,8 @@ var createVoiceStreamStore = () => {
938
1017
  case "error":
939
1018
  state = {
940
1019
  ...state,
941
- error: action.message
1020
+ error: action.message,
1021
+ ...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
942
1022
  };
943
1023
  break;
944
1024
  case "final":
@@ -1078,7 +1158,7 @@ var createVoiceStream = (path, options = {}) => {
1078
1158
  return store.getSnapshot().call;
1079
1159
  },
1080
1160
  callControl(message) {
1081
- connection.callControl(message);
1161
+ return connection.callControl(message);
1082
1162
  },
1083
1163
  close(reason) {
1084
1164
  unsubscribeConnection();