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

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.
@@ -11309,8 +11309,11 @@ var serverMessageToAction = (message) => {
11309
11309
  case "error":
11310
11310
  return {
11311
11311
  message: normalizeErrorMessage(message.message),
11312
+ recoverable: message.recoverable,
11312
11313
  type: "error"
11313
11314
  };
11315
+ case "call_control_ack":
11316
+ return null;
11314
11317
  case "final":
11315
11318
  return {
11316
11319
  transcript: message.transcript,
@@ -11438,14 +11441,17 @@ var WS_FORBIDDEN = 4403;
11438
11441
  var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
11439
11442
  var DEFAULT_PING_INTERVAL = 30000;
11440
11443
  var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
11444
+ var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
11445
+ var MAX_PENDING_CONTROL_MESSAGES = 100;
11441
11446
  var RECONNECT_BASE_DELAY_MS = 500;
11442
11447
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
11443
11448
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
11444
11449
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
11445
11450
  var noop = () => {};
11451
+ var noopAsync = () => Promise.resolve();
11446
11452
  var noopUnsubscribe = () => noop;
11447
11453
  var NOOP_CONNECTION = {
11448
- callControl: noop,
11454
+ callControl: noopAsync,
11449
11455
  close: noop,
11450
11456
  disconnect: noop,
11451
11457
  endTurn: noop,
@@ -11483,6 +11489,7 @@ var isVoiceServerMessage = (value) => {
11483
11489
  case "audio":
11484
11490
  case "assistant":
11485
11491
  case "call_lifecycle":
11492
+ case "call_control_ack":
11486
11493
  case "complete":
11487
11494
  case "connection":
11488
11495
  case "error":
@@ -11513,6 +11520,7 @@ var createVoiceConnection = (path, options = {}) => {
11513
11520
  return NOOP_CONNECTION;
11514
11521
  }
11515
11522
  const listeners = new Set;
11523
+ const pendingCallControls = new Map;
11516
11524
  const shouldReconnect = options.reconnect !== false;
11517
11525
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
11518
11526
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
@@ -11558,6 +11566,13 @@ var createVoiceConnection = (path, options = {}) => {
11558
11566
  }
11559
11567
  }
11560
11568
  };
11569
+ const rejectPendingCallControls = (message) => {
11570
+ for (const pending of pendingCallControls.values()) {
11571
+ clearTimeout(pending.timeout);
11572
+ pending.reject(new Error(message));
11573
+ }
11574
+ pendingCallControls.clear();
11575
+ };
11561
11576
  const scheduleReconnect = () => {
11562
11577
  state.reconnectAttempts += 1;
11563
11578
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
@@ -11572,7 +11587,7 @@ var createVoiceConnection = (path, options = {}) => {
11572
11587
  },
11573
11588
  type: "connection"
11574
11589
  });
11575
- state.reconnectTimeout = setTimeout(() => {
11590
+ state.reconnectTimeout = setTimeout(async () => {
11576
11591
  if (state.reconnectAttempts > maxReconnectAttempts) {
11577
11592
  emitConnection({
11578
11593
  reconnect: {
@@ -11584,6 +11599,28 @@ var createVoiceConnection = (path, options = {}) => {
11584
11599
  });
11585
11600
  return;
11586
11601
  }
11602
+ try {
11603
+ await options.prepareReconnect?.({
11604
+ attempt: state.reconnectAttempts,
11605
+ path,
11606
+ scenarioId: state.scenarioId,
11607
+ sessionId: state.sessionId
11608
+ });
11609
+ } catch {
11610
+ if (state.reconnectAttempts < maxReconnectAttempts) {
11611
+ scheduleReconnect();
11612
+ } else {
11613
+ emitConnection({
11614
+ reconnect: {
11615
+ attempts: state.reconnectAttempts,
11616
+ maxAttempts: maxReconnectAttempts,
11617
+ status: "exhausted"
11618
+ },
11619
+ type: "connection"
11620
+ });
11621
+ }
11622
+ return;
11623
+ }
11587
11624
  connect();
11588
11625
  }, delayMs);
11589
11626
  };
@@ -11638,6 +11675,18 @@ var createVoiceConnection = (path, options = {}) => {
11638
11675
  state.sessionId = parsed.sessionId;
11639
11676
  state.scenarioId = parsed.scenarioId ?? state.scenarioId;
11640
11677
  }
11678
+ if (parsed.type === "call_control_ack") {
11679
+ const pending = pendingCallControls.get(parsed.requestId);
11680
+ if (pending) {
11681
+ clearTimeout(pending.timeout);
11682
+ pendingCallControls.delete(parsed.requestId);
11683
+ if (parsed.ok) {
11684
+ pending.resolve();
11685
+ } else {
11686
+ pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
11687
+ }
11688
+ }
11689
+ }
11641
11690
  listeners.forEach((listener) => listener(parsed));
11642
11691
  };
11643
11692
  ws.onclose = (event) => {
@@ -11647,6 +11696,14 @@ var createVoiceConnection = (path, options = {}) => {
11647
11696
  state.isConnected = false;
11648
11697
  clearTimers();
11649
11698
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
11699
+ const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
11700
+ if (authorizationFailure) {
11701
+ listeners.forEach((listener) => listener({
11702
+ message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
11703
+ recoverable: false,
11704
+ type: "error"
11705
+ }));
11706
+ }
11650
11707
  if (reconnectable) {
11651
11708
  scheduleReconnect();
11652
11709
  } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
@@ -11668,6 +11725,12 @@ var createVoiceConnection = (path, options = {}) => {
11668
11725
  state.ws.send(value);
11669
11726
  return;
11670
11727
  }
11728
+ if (typeof value !== "string") {
11729
+ return;
11730
+ }
11731
+ if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
11732
+ state.pendingMessages.shift();
11733
+ }
11671
11734
  state.pendingMessages.push(value);
11672
11735
  };
11673
11736
  const send = (message) => {
@@ -11693,13 +11756,23 @@ var createVoiceConnection = (path, options = {}) => {
11693
11756
  send({ type: "end_turn" });
11694
11757
  };
11695
11758
  const callControl = (message) => {
11759
+ const requestId = crypto.randomUUID();
11760
+ const { promise, reject, resolve } = Promise.withResolvers();
11761
+ const timeout = setTimeout(() => {
11762
+ pendingCallControls.delete(requestId);
11763
+ reject(new Error(`${message.action} acknowledgement timed out`));
11764
+ }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
11765
+ pendingCallControls.set(requestId, { reject, resolve, timeout });
11696
11766
  send({
11697
11767
  ...message,
11768
+ requestId,
11698
11769
  type: "call_control"
11699
11770
  });
11771
+ return promise;
11700
11772
  };
11701
11773
  const close = (reason = "client-close") => {
11702
11774
  clearTimers();
11775
+ rejectPendingCallControls("Voice connection closed");
11703
11776
  if (state.ws) {
11704
11777
  const ws = state.ws;
11705
11778
  state.ws = null;
@@ -11713,6 +11786,7 @@ var createVoiceConnection = (path, options = {}) => {
11713
11786
  };
11714
11787
  const disconnect = () => {
11715
11788
  clearTimers();
11789
+ rejectPendingCallControls("Voice connection disconnected");
11716
11790
  if (state.ws) {
11717
11791
  const ws = state.ws;
11718
11792
  state.ws = null;
@@ -11875,7 +11949,8 @@ var createVoiceStreamStore = () => {
11875
11949
  case "error":
11876
11950
  state = {
11877
11951
  ...state,
11878
- error: action.message
11952
+ error: action.message,
11953
+ ...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
11879
11954
  };
11880
11955
  break;
11881
11956
  case "final":
@@ -12015,7 +12090,7 @@ var createVoiceStream = (path, options = {}) => {
12015
12090
  return store.getSnapshot().call;
12016
12091
  },
12017
12092
  callControl(message) {
12018
- connection.callControl(message);
12093
+ return connection.callControl(message);
12019
12094
  },
12020
12095
  close(reason) {
12021
12096
  unsubscribeConnection();
@@ -1,7 +1,7 @@
1
1
  import type { VoiceControllerOptions } from "../core/types";
2
2
  export declare const useVoiceController: <TResult = unknown>(path: string, options?: VoiceControllerOptions) => {
3
3
  bindHTMX: (options: import("..").VoiceHTMXBindingOptions) => () => void;
4
- callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "type">) => void>[0]) => void;
4
+ callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "requestId" | "type">) => Promise<void>>[0]) => Promise<void>;
5
5
  close: () => void;
6
6
  endTurn: () => void;
7
7
  sendAudio: (audio: Uint8Array | ArrayBuffer) => void;
@@ -1,6 +1,6 @@
1
1
  import type { VoiceConnectionOptions } from "../core/types";
2
2
  export declare const useVoiceStream: <TResult = unknown>(path: string, options?: VoiceConnectionOptions) => {
3
- callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "type">) => void>[0]) => void;
3
+ callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "requestId" | "type">) => Promise<void>>[0]) => Promise<void>;
4
4
  close: () => void;
5
5
  endTurn: () => void;
6
6
  sendAudio: (audio: Uint8Array | ArrayBuffer) => void;
@@ -637,8 +637,11 @@ var serverMessageToAction = (message) => {
637
637
  case "error":
638
638
  return {
639
639
  message: normalizeErrorMessage(message.message),
640
+ recoverable: message.recoverable,
640
641
  type: "error"
641
642
  };
643
+ case "call_control_ack":
644
+ return null;
642
645
  case "final":
643
646
  return {
644
647
  transcript: message.transcript,
@@ -766,14 +769,17 @@ var WS_FORBIDDEN = 4403;
766
769
  var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
767
770
  var DEFAULT_PING_INTERVAL = 30000;
768
771
  var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
772
+ var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
773
+ var MAX_PENDING_CONTROL_MESSAGES = 100;
769
774
  var RECONNECT_BASE_DELAY_MS = 500;
770
775
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
771
776
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
772
777
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
773
778
  var noop = () => {};
779
+ var noopAsync = () => Promise.resolve();
774
780
  var noopUnsubscribe = () => noop;
775
781
  var NOOP_CONNECTION = {
776
- callControl: noop,
782
+ callControl: noopAsync,
777
783
  close: noop,
778
784
  disconnect: noop,
779
785
  endTurn: noop,
@@ -811,6 +817,7 @@ var isVoiceServerMessage = (value) => {
811
817
  case "audio":
812
818
  case "assistant":
813
819
  case "call_lifecycle":
820
+ case "call_control_ack":
814
821
  case "complete":
815
822
  case "connection":
816
823
  case "error":
@@ -841,6 +848,7 @@ var createVoiceConnection = (path, options = {}) => {
841
848
  return NOOP_CONNECTION;
842
849
  }
843
850
  const listeners = new Set;
851
+ const pendingCallControls = new Map;
844
852
  const shouldReconnect = options.reconnect !== false;
845
853
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
846
854
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
@@ -886,6 +894,13 @@ var createVoiceConnection = (path, options = {}) => {
886
894
  }
887
895
  }
888
896
  };
897
+ const rejectPendingCallControls = (message) => {
898
+ for (const pending of pendingCallControls.values()) {
899
+ clearTimeout(pending.timeout);
900
+ pending.reject(new Error(message));
901
+ }
902
+ pendingCallControls.clear();
903
+ };
889
904
  const scheduleReconnect = () => {
890
905
  state.reconnectAttempts += 1;
891
906
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
@@ -900,7 +915,7 @@ var createVoiceConnection = (path, options = {}) => {
900
915
  },
901
916
  type: "connection"
902
917
  });
903
- state.reconnectTimeout = setTimeout(() => {
918
+ state.reconnectTimeout = setTimeout(async () => {
904
919
  if (state.reconnectAttempts > maxReconnectAttempts) {
905
920
  emitConnection({
906
921
  reconnect: {
@@ -912,6 +927,28 @@ var createVoiceConnection = (path, options = {}) => {
912
927
  });
913
928
  return;
914
929
  }
930
+ try {
931
+ await options.prepareReconnect?.({
932
+ attempt: state.reconnectAttempts,
933
+ path,
934
+ scenarioId: state.scenarioId,
935
+ sessionId: state.sessionId
936
+ });
937
+ } catch {
938
+ if (state.reconnectAttempts < maxReconnectAttempts) {
939
+ scheduleReconnect();
940
+ } else {
941
+ emitConnection({
942
+ reconnect: {
943
+ attempts: state.reconnectAttempts,
944
+ maxAttempts: maxReconnectAttempts,
945
+ status: "exhausted"
946
+ },
947
+ type: "connection"
948
+ });
949
+ }
950
+ return;
951
+ }
915
952
  connect();
916
953
  }, delayMs);
917
954
  };
@@ -966,6 +1003,18 @@ var createVoiceConnection = (path, options = {}) => {
966
1003
  state.sessionId = parsed.sessionId;
967
1004
  state.scenarioId = parsed.scenarioId ?? state.scenarioId;
968
1005
  }
1006
+ if (parsed.type === "call_control_ack") {
1007
+ const pending = pendingCallControls.get(parsed.requestId);
1008
+ if (pending) {
1009
+ clearTimeout(pending.timeout);
1010
+ pendingCallControls.delete(parsed.requestId);
1011
+ if (parsed.ok) {
1012
+ pending.resolve();
1013
+ } else {
1014
+ pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
1015
+ }
1016
+ }
1017
+ }
969
1018
  listeners.forEach((listener) => listener(parsed));
970
1019
  };
971
1020
  ws.onclose = (event) => {
@@ -975,6 +1024,14 @@ var createVoiceConnection = (path, options = {}) => {
975
1024
  state.isConnected = false;
976
1025
  clearTimers();
977
1026
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
1027
+ const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
1028
+ if (authorizationFailure) {
1029
+ listeners.forEach((listener) => listener({
1030
+ message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
1031
+ recoverable: false,
1032
+ type: "error"
1033
+ }));
1034
+ }
978
1035
  if (reconnectable) {
979
1036
  scheduleReconnect();
980
1037
  } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
@@ -996,6 +1053,12 @@ var createVoiceConnection = (path, options = {}) => {
996
1053
  state.ws.send(value);
997
1054
  return;
998
1055
  }
1056
+ if (typeof value !== "string") {
1057
+ return;
1058
+ }
1059
+ if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
1060
+ state.pendingMessages.shift();
1061
+ }
999
1062
  state.pendingMessages.push(value);
1000
1063
  };
1001
1064
  const send = (message) => {
@@ -1021,13 +1084,23 @@ var createVoiceConnection = (path, options = {}) => {
1021
1084
  send({ type: "end_turn" });
1022
1085
  };
1023
1086
  const callControl = (message) => {
1087
+ const requestId = crypto.randomUUID();
1088
+ const { promise, reject, resolve } = Promise.withResolvers();
1089
+ const timeout = setTimeout(() => {
1090
+ pendingCallControls.delete(requestId);
1091
+ reject(new Error(`${message.action} acknowledgement timed out`));
1092
+ }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
1093
+ pendingCallControls.set(requestId, { reject, resolve, timeout });
1024
1094
  send({
1025
1095
  ...message,
1096
+ requestId,
1026
1097
  type: "call_control"
1027
1098
  });
1099
+ return promise;
1028
1100
  };
1029
1101
  const close = (reason = "client-close") => {
1030
1102
  clearTimers();
1103
+ rejectPendingCallControls("Voice connection closed");
1031
1104
  if (state.ws) {
1032
1105
  const ws = state.ws;
1033
1106
  state.ws = null;
@@ -1041,6 +1114,7 @@ var createVoiceConnection = (path, options = {}) => {
1041
1114
  };
1042
1115
  const disconnect = () => {
1043
1116
  clearTimers();
1117
+ rejectPendingCallControls("Voice connection disconnected");
1044
1118
  if (state.ws) {
1045
1119
  const ws = state.ws;
1046
1120
  state.ws = null;
@@ -1203,7 +1277,8 @@ var createVoiceStreamStore = () => {
1203
1277
  case "error":
1204
1278
  state = {
1205
1279
  ...state,
1206
- error: action.message
1280
+ error: action.message,
1281
+ ...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
1207
1282
  };
1208
1283
  break;
1209
1284
  case "final":
@@ -1343,7 +1418,7 @@ var createVoiceStream = (path, options = {}) => {
1343
1418
  return store.getSnapshot().call;
1344
1419
  },
1345
1420
  callControl(message) {
1346
- connection.callControl(message);
1421
+ return connection.callControl(message);
1347
1422
  },
1348
1423
  close(reason) {
1349
1424
  unsubscribeConnection();
@@ -2858,8 +2858,11 @@ var serverMessageToAction = (message) => {
2858
2858
  case "error":
2859
2859
  return {
2860
2860
  message: normalizeErrorMessage(message.message),
2861
+ recoverable: message.recoverable,
2861
2862
  type: "error"
2862
2863
  };
2864
+ case "call_control_ack":
2865
+ return null;
2863
2866
  case "final":
2864
2867
  return {
2865
2868
  transcript: message.transcript,
@@ -2987,14 +2990,17 @@ var WS_FORBIDDEN = 4403;
2987
2990
  var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
2988
2991
  var DEFAULT_PING_INTERVAL = 30000;
2989
2992
  var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
2993
+ var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
2994
+ var MAX_PENDING_CONTROL_MESSAGES = 100;
2990
2995
  var RECONNECT_BASE_DELAY_MS = 500;
2991
2996
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
2992
2997
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
2993
2998
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
2994
2999
  var noop = () => {};
3000
+ var noopAsync = () => Promise.resolve();
2995
3001
  var noopUnsubscribe = () => noop;
2996
3002
  var NOOP_CONNECTION = {
2997
- callControl: noop,
3003
+ callControl: noopAsync,
2998
3004
  close: noop,
2999
3005
  disconnect: noop,
3000
3006
  endTurn: noop,
@@ -3032,6 +3038,7 @@ var isVoiceServerMessage = (value) => {
3032
3038
  case "audio":
3033
3039
  case "assistant":
3034
3040
  case "call_lifecycle":
3041
+ case "call_control_ack":
3035
3042
  case "complete":
3036
3043
  case "connection":
3037
3044
  case "error":
@@ -3062,6 +3069,7 @@ var createVoiceConnection = (path, options = {}) => {
3062
3069
  return NOOP_CONNECTION;
3063
3070
  }
3064
3071
  const listeners = new Set;
3072
+ const pendingCallControls = new Map;
3065
3073
  const shouldReconnect = options.reconnect !== false;
3066
3074
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
3067
3075
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
@@ -3107,6 +3115,13 @@ var createVoiceConnection = (path, options = {}) => {
3107
3115
  }
3108
3116
  }
3109
3117
  };
3118
+ const rejectPendingCallControls = (message) => {
3119
+ for (const pending of pendingCallControls.values()) {
3120
+ clearTimeout(pending.timeout);
3121
+ pending.reject(new Error(message));
3122
+ }
3123
+ pendingCallControls.clear();
3124
+ };
3110
3125
  const scheduleReconnect = () => {
3111
3126
  state.reconnectAttempts += 1;
3112
3127
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
@@ -3121,7 +3136,7 @@ var createVoiceConnection = (path, options = {}) => {
3121
3136
  },
3122
3137
  type: "connection"
3123
3138
  });
3124
- state.reconnectTimeout = setTimeout(() => {
3139
+ state.reconnectTimeout = setTimeout(async () => {
3125
3140
  if (state.reconnectAttempts > maxReconnectAttempts) {
3126
3141
  emitConnection({
3127
3142
  reconnect: {
@@ -3133,6 +3148,28 @@ var createVoiceConnection = (path, options = {}) => {
3133
3148
  });
3134
3149
  return;
3135
3150
  }
3151
+ try {
3152
+ await options.prepareReconnect?.({
3153
+ attempt: state.reconnectAttempts,
3154
+ path,
3155
+ scenarioId: state.scenarioId,
3156
+ sessionId: state.sessionId
3157
+ });
3158
+ } catch {
3159
+ if (state.reconnectAttempts < maxReconnectAttempts) {
3160
+ scheduleReconnect();
3161
+ } else {
3162
+ emitConnection({
3163
+ reconnect: {
3164
+ attempts: state.reconnectAttempts,
3165
+ maxAttempts: maxReconnectAttempts,
3166
+ status: "exhausted"
3167
+ },
3168
+ type: "connection"
3169
+ });
3170
+ }
3171
+ return;
3172
+ }
3136
3173
  connect();
3137
3174
  }, delayMs);
3138
3175
  };
@@ -3187,6 +3224,18 @@ var createVoiceConnection = (path, options = {}) => {
3187
3224
  state.sessionId = parsed.sessionId;
3188
3225
  state.scenarioId = parsed.scenarioId ?? state.scenarioId;
3189
3226
  }
3227
+ if (parsed.type === "call_control_ack") {
3228
+ const pending = pendingCallControls.get(parsed.requestId);
3229
+ if (pending) {
3230
+ clearTimeout(pending.timeout);
3231
+ pendingCallControls.delete(parsed.requestId);
3232
+ if (parsed.ok) {
3233
+ pending.resolve();
3234
+ } else {
3235
+ pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
3236
+ }
3237
+ }
3238
+ }
3190
3239
  listeners.forEach((listener) => listener(parsed));
3191
3240
  };
3192
3241
  ws.onclose = (event) => {
@@ -3196,6 +3245,14 @@ var createVoiceConnection = (path, options = {}) => {
3196
3245
  state.isConnected = false;
3197
3246
  clearTimers();
3198
3247
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
3248
+ const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
3249
+ if (authorizationFailure) {
3250
+ listeners.forEach((listener) => listener({
3251
+ message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
3252
+ recoverable: false,
3253
+ type: "error"
3254
+ }));
3255
+ }
3199
3256
  if (reconnectable) {
3200
3257
  scheduleReconnect();
3201
3258
  } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
@@ -3217,6 +3274,12 @@ var createVoiceConnection = (path, options = {}) => {
3217
3274
  state.ws.send(value);
3218
3275
  return;
3219
3276
  }
3277
+ if (typeof value !== "string") {
3278
+ return;
3279
+ }
3280
+ if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
3281
+ state.pendingMessages.shift();
3282
+ }
3220
3283
  state.pendingMessages.push(value);
3221
3284
  };
3222
3285
  const send = (message) => {
@@ -3242,13 +3305,23 @@ var createVoiceConnection = (path, options = {}) => {
3242
3305
  send({ type: "end_turn" });
3243
3306
  };
3244
3307
  const callControl = (message) => {
3308
+ const requestId = crypto.randomUUID();
3309
+ const { promise, reject, resolve } = Promise.withResolvers();
3310
+ const timeout = setTimeout(() => {
3311
+ pendingCallControls.delete(requestId);
3312
+ reject(new Error(`${message.action} acknowledgement timed out`));
3313
+ }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
3314
+ pendingCallControls.set(requestId, { reject, resolve, timeout });
3245
3315
  send({
3246
3316
  ...message,
3317
+ requestId,
3247
3318
  type: "call_control"
3248
3319
  });
3320
+ return promise;
3249
3321
  };
3250
3322
  const close = (reason = "client-close") => {
3251
3323
  clearTimers();
3324
+ rejectPendingCallControls("Voice connection closed");
3252
3325
  if (state.ws) {
3253
3326
  const ws = state.ws;
3254
3327
  state.ws = null;
@@ -3262,6 +3335,7 @@ var createVoiceConnection = (path, options = {}) => {
3262
3335
  };
3263
3336
  const disconnect = () => {
3264
3337
  clearTimers();
3338
+ rejectPendingCallControls("Voice connection disconnected");
3265
3339
  if (state.ws) {
3266
3340
  const ws = state.ws;
3267
3341
  state.ws = null;
@@ -3424,7 +3498,8 @@ var createVoiceStreamStore = () => {
3424
3498
  case "error":
3425
3499
  state = {
3426
3500
  ...state,
3427
- error: action.message
3501
+ error: action.message,
3502
+ ...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
3428
3503
  };
3429
3504
  break;
3430
3505
  case "final":
@@ -3564,7 +3639,7 @@ var createVoiceStream = (path, options = {}) => {
3564
3639
  return store.getSnapshot().call;
3565
3640
  },
3566
3641
  callControl(message) {
3567
- connection.callControl(message);
3642
+ return connection.callControl(message);
3568
3643
  },
3569
3644
  close(reason) {
3570
3645
  unsubscribeConnection();