@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.
@@ -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;
@@ -861,6 +869,21 @@ var createVoiceConnection = (path, options = {}) => {
861
869
  const emitConnection = (reconnect) => {
862
870
  listeners.forEach((listener) => listener(reconnect));
863
871
  };
872
+ const emitTerminalFailure = (message) => {
873
+ listeners.forEach((listener) => listener({
874
+ message,
875
+ recoverable: false,
876
+ type: "error"
877
+ }));
878
+ emitConnection({
879
+ reconnect: {
880
+ attempts: state.reconnectAttempts,
881
+ maxAttempts: maxReconnectAttempts,
882
+ status: "exhausted"
883
+ },
884
+ type: "connection"
885
+ });
886
+ };
864
887
  const clearTimers = () => {
865
888
  if (state.pingInterval) {
866
889
  clearInterval(state.pingInterval);
@@ -886,6 +909,13 @@ var createVoiceConnection = (path, options = {}) => {
886
909
  }
887
910
  }
888
911
  };
912
+ const rejectPendingCallControls = (message) => {
913
+ for (const pending of pendingCallControls.values()) {
914
+ clearTimeout(pending.timeout);
915
+ pending.reject(new Error(message));
916
+ }
917
+ pendingCallControls.clear();
918
+ };
889
919
  const scheduleReconnect = () => {
890
920
  state.reconnectAttempts += 1;
891
921
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
@@ -900,16 +930,24 @@ var createVoiceConnection = (path, options = {}) => {
900
930
  },
901
931
  type: "connection"
902
932
  });
903
- state.reconnectTimeout = setTimeout(() => {
933
+ state.reconnectTimeout = setTimeout(async () => {
904
934
  if (state.reconnectAttempts > maxReconnectAttempts) {
905
- emitConnection({
906
- reconnect: {
907
- attempts: state.reconnectAttempts,
908
- maxAttempts: maxReconnectAttempts,
909
- status: "exhausted"
910
- },
911
- type: "connection"
935
+ emitTerminalFailure("Voice connection could not be restored.");
936
+ return;
937
+ }
938
+ try {
939
+ await options.prepareReconnect?.({
940
+ attempt: state.reconnectAttempts,
941
+ path,
942
+ scenarioId: state.scenarioId,
943
+ sessionId: state.sessionId
912
944
  });
945
+ } catch {
946
+ if (state.reconnectAttempts < maxReconnectAttempts) {
947
+ scheduleReconnect();
948
+ } else {
949
+ emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
950
+ }
913
951
  return;
914
952
  }
915
953
  connect();
@@ -966,6 +1004,18 @@ var createVoiceConnection = (path, options = {}) => {
966
1004
  state.sessionId = parsed.sessionId;
967
1005
  state.scenarioId = parsed.scenarioId ?? state.scenarioId;
968
1006
  }
1007
+ if (parsed.type === "call_control_ack") {
1008
+ const pending = pendingCallControls.get(parsed.requestId);
1009
+ if (pending) {
1010
+ clearTimeout(pending.timeout);
1011
+ pendingCallControls.delete(parsed.requestId);
1012
+ if (parsed.ok) {
1013
+ pending.resolve();
1014
+ } else {
1015
+ pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
1016
+ }
1017
+ }
1018
+ }
969
1019
  listeners.forEach((listener) => listener(parsed));
970
1020
  };
971
1021
  ws.onclose = (event) => {
@@ -975,18 +1025,30 @@ var createVoiceConnection = (path, options = {}) => {
975
1025
  state.isConnected = false;
976
1026
  clearTimers();
977
1027
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
1028
+ const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
1029
+ if (authorizationFailure) {
1030
+ listeners.forEach((listener) => listener({
1031
+ message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
1032
+ recoverable: false,
1033
+ type: "error"
1034
+ }));
1035
+ }
978
1036
  if (reconnectable) {
979
1037
  scheduleReconnect();
980
1038
  } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
981
- emitConnection({
982
- reconnect: {
983
- attempts: state.reconnectAttempts,
984
- lastDisconnectAt: Date.now(),
985
- maxAttempts: maxReconnectAttempts,
986
- status: "exhausted"
987
- },
988
- type: "connection"
989
- });
1039
+ if (authorizationFailure) {
1040
+ emitConnection({
1041
+ reconnect: {
1042
+ attempts: state.reconnectAttempts,
1043
+ lastDisconnectAt: Date.now(),
1044
+ maxAttempts: maxReconnectAttempts,
1045
+ status: "exhausted"
1046
+ },
1047
+ type: "connection"
1048
+ });
1049
+ } else {
1050
+ emitTerminalFailure("Voice connection could not be restored.");
1051
+ }
990
1052
  }
991
1053
  };
992
1054
  state.ws = ws;
@@ -996,6 +1058,12 @@ var createVoiceConnection = (path, options = {}) => {
996
1058
  state.ws.send(value);
997
1059
  return;
998
1060
  }
1061
+ if (typeof value !== "string") {
1062
+ return;
1063
+ }
1064
+ if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
1065
+ state.pendingMessages.shift();
1066
+ }
999
1067
  state.pendingMessages.push(value);
1000
1068
  };
1001
1069
  const send = (message) => {
@@ -1021,13 +1089,23 @@ var createVoiceConnection = (path, options = {}) => {
1021
1089
  send({ type: "end_turn" });
1022
1090
  };
1023
1091
  const callControl = (message) => {
1092
+ const requestId = crypto.randomUUID();
1093
+ const { promise, reject, resolve } = Promise.withResolvers();
1094
+ const timeout = setTimeout(() => {
1095
+ pendingCallControls.delete(requestId);
1096
+ reject(new Error(`${message.action} acknowledgement timed out`));
1097
+ }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
1098
+ pendingCallControls.set(requestId, { reject, resolve, timeout });
1024
1099
  send({
1025
1100
  ...message,
1101
+ requestId,
1026
1102
  type: "call_control"
1027
1103
  });
1104
+ return promise;
1028
1105
  };
1029
1106
  const close = (reason = "client-close") => {
1030
1107
  clearTimers();
1108
+ rejectPendingCallControls("Voice connection closed");
1031
1109
  if (state.ws) {
1032
1110
  const ws = state.ws;
1033
1111
  state.ws = null;
@@ -1041,6 +1119,7 @@ var createVoiceConnection = (path, options = {}) => {
1041
1119
  };
1042
1120
  const disconnect = () => {
1043
1121
  clearTimers();
1122
+ rejectPendingCallControls("Voice connection disconnected");
1044
1123
  if (state.ws) {
1045
1124
  const ws = state.ws;
1046
1125
  state.ws = null;
@@ -1203,7 +1282,8 @@ var createVoiceStreamStore = () => {
1203
1282
  case "error":
1204
1283
  state = {
1205
1284
  ...state,
1206
- error: action.message
1285
+ error: action.message,
1286
+ ...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
1207
1287
  };
1208
1288
  break;
1209
1289
  case "final":
@@ -1343,7 +1423,7 @@ var createVoiceStream = (path, options = {}) => {
1343
1423
  return store.getSnapshot().call;
1344
1424
  },
1345
1425
  callControl(message) {
1346
- connection.callControl(message);
1426
+ return connection.callControl(message);
1347
1427
  },
1348
1428
  close(reason) {
1349
1429
  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;
@@ -3082,6 +3090,21 @@ var createVoiceConnection = (path, options = {}) => {
3082
3090
  const emitConnection = (reconnect) => {
3083
3091
  listeners.forEach((listener) => listener(reconnect));
3084
3092
  };
3093
+ const emitTerminalFailure = (message) => {
3094
+ listeners.forEach((listener) => listener({
3095
+ message,
3096
+ recoverable: false,
3097
+ type: "error"
3098
+ }));
3099
+ emitConnection({
3100
+ reconnect: {
3101
+ attempts: state.reconnectAttempts,
3102
+ maxAttempts: maxReconnectAttempts,
3103
+ status: "exhausted"
3104
+ },
3105
+ type: "connection"
3106
+ });
3107
+ };
3085
3108
  const clearTimers = () => {
3086
3109
  if (state.pingInterval) {
3087
3110
  clearInterval(state.pingInterval);
@@ -3107,6 +3130,13 @@ var createVoiceConnection = (path, options = {}) => {
3107
3130
  }
3108
3131
  }
3109
3132
  };
3133
+ const rejectPendingCallControls = (message) => {
3134
+ for (const pending of pendingCallControls.values()) {
3135
+ clearTimeout(pending.timeout);
3136
+ pending.reject(new Error(message));
3137
+ }
3138
+ pendingCallControls.clear();
3139
+ };
3110
3140
  const scheduleReconnect = () => {
3111
3141
  state.reconnectAttempts += 1;
3112
3142
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
@@ -3121,16 +3151,24 @@ var createVoiceConnection = (path, options = {}) => {
3121
3151
  },
3122
3152
  type: "connection"
3123
3153
  });
3124
- state.reconnectTimeout = setTimeout(() => {
3154
+ state.reconnectTimeout = setTimeout(async () => {
3125
3155
  if (state.reconnectAttempts > maxReconnectAttempts) {
3126
- emitConnection({
3127
- reconnect: {
3128
- attempts: state.reconnectAttempts,
3129
- maxAttempts: maxReconnectAttempts,
3130
- status: "exhausted"
3131
- },
3132
- type: "connection"
3156
+ emitTerminalFailure("Voice connection could not be restored.");
3157
+ return;
3158
+ }
3159
+ try {
3160
+ await options.prepareReconnect?.({
3161
+ attempt: state.reconnectAttempts,
3162
+ path,
3163
+ scenarioId: state.scenarioId,
3164
+ sessionId: state.sessionId
3133
3165
  });
3166
+ } catch {
3167
+ if (state.reconnectAttempts < maxReconnectAttempts) {
3168
+ scheduleReconnect();
3169
+ } else {
3170
+ emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
3171
+ }
3134
3172
  return;
3135
3173
  }
3136
3174
  connect();
@@ -3187,6 +3225,18 @@ var createVoiceConnection = (path, options = {}) => {
3187
3225
  state.sessionId = parsed.sessionId;
3188
3226
  state.scenarioId = parsed.scenarioId ?? state.scenarioId;
3189
3227
  }
3228
+ if (parsed.type === "call_control_ack") {
3229
+ const pending = pendingCallControls.get(parsed.requestId);
3230
+ if (pending) {
3231
+ clearTimeout(pending.timeout);
3232
+ pendingCallControls.delete(parsed.requestId);
3233
+ if (parsed.ok) {
3234
+ pending.resolve();
3235
+ } else {
3236
+ pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
3237
+ }
3238
+ }
3239
+ }
3190
3240
  listeners.forEach((listener) => listener(parsed));
3191
3241
  };
3192
3242
  ws.onclose = (event) => {
@@ -3196,18 +3246,30 @@ var createVoiceConnection = (path, options = {}) => {
3196
3246
  state.isConnected = false;
3197
3247
  clearTimers();
3198
3248
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
3249
+ const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
3250
+ if (authorizationFailure) {
3251
+ listeners.forEach((listener) => listener({
3252
+ message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
3253
+ recoverable: false,
3254
+ type: "error"
3255
+ }));
3256
+ }
3199
3257
  if (reconnectable) {
3200
3258
  scheduleReconnect();
3201
3259
  } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
3202
- emitConnection({
3203
- reconnect: {
3204
- attempts: state.reconnectAttempts,
3205
- lastDisconnectAt: Date.now(),
3206
- maxAttempts: maxReconnectAttempts,
3207
- status: "exhausted"
3208
- },
3209
- type: "connection"
3210
- });
3260
+ if (authorizationFailure) {
3261
+ emitConnection({
3262
+ reconnect: {
3263
+ attempts: state.reconnectAttempts,
3264
+ lastDisconnectAt: Date.now(),
3265
+ maxAttempts: maxReconnectAttempts,
3266
+ status: "exhausted"
3267
+ },
3268
+ type: "connection"
3269
+ });
3270
+ } else {
3271
+ emitTerminalFailure("Voice connection could not be restored.");
3272
+ }
3211
3273
  }
3212
3274
  };
3213
3275
  state.ws = ws;
@@ -3217,6 +3279,12 @@ var createVoiceConnection = (path, options = {}) => {
3217
3279
  state.ws.send(value);
3218
3280
  return;
3219
3281
  }
3282
+ if (typeof value !== "string") {
3283
+ return;
3284
+ }
3285
+ if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
3286
+ state.pendingMessages.shift();
3287
+ }
3220
3288
  state.pendingMessages.push(value);
3221
3289
  };
3222
3290
  const send = (message) => {
@@ -3242,13 +3310,23 @@ var createVoiceConnection = (path, options = {}) => {
3242
3310
  send({ type: "end_turn" });
3243
3311
  };
3244
3312
  const callControl = (message) => {
3313
+ const requestId = crypto.randomUUID();
3314
+ const { promise, reject, resolve } = Promise.withResolvers();
3315
+ const timeout = setTimeout(() => {
3316
+ pendingCallControls.delete(requestId);
3317
+ reject(new Error(`${message.action} acknowledgement timed out`));
3318
+ }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
3319
+ pendingCallControls.set(requestId, { reject, resolve, timeout });
3245
3320
  send({
3246
3321
  ...message,
3322
+ requestId,
3247
3323
  type: "call_control"
3248
3324
  });
3325
+ return promise;
3249
3326
  };
3250
3327
  const close = (reason = "client-close") => {
3251
3328
  clearTimers();
3329
+ rejectPendingCallControls("Voice connection closed");
3252
3330
  if (state.ws) {
3253
3331
  const ws = state.ws;
3254
3332
  state.ws = null;
@@ -3262,6 +3340,7 @@ var createVoiceConnection = (path, options = {}) => {
3262
3340
  };
3263
3341
  const disconnect = () => {
3264
3342
  clearTimers();
3343
+ rejectPendingCallControls("Voice connection disconnected");
3265
3344
  if (state.ws) {
3266
3345
  const ws = state.ws;
3267
3346
  state.ws = null;
@@ -3424,7 +3503,8 @@ var createVoiceStreamStore = () => {
3424
3503
  case "error":
3425
3504
  state = {
3426
3505
  ...state,
3427
- error: action.message
3506
+ error: action.message,
3507
+ ...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
3428
3508
  };
3429
3509
  break;
3430
3510
  case "final":
@@ -3564,7 +3644,7 @@ var createVoiceStream = (path, options = {}) => {
3564
3644
  return store.getSnapshot().call;
3565
3645
  },
3566
3646
  callControl(message) {
3567
- connection.callControl(message);
3647
+ return connection.callControl(message);
3568
3648
  },
3569
3649
  close(reason) {
3570
3650
  unsubscribeConnection();