@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.
package/dist/vue/index.js CHANGED
@@ -10705,8 +10705,11 @@ var serverMessageToAction = (message) => {
10705
10705
  case "error":
10706
10706
  return {
10707
10707
  message: normalizeErrorMessage(message.message),
10708
+ recoverable: message.recoverable,
10708
10709
  type: "error"
10709
10710
  };
10711
+ case "call_control_ack":
10712
+ return null;
10710
10713
  case "final":
10711
10714
  return {
10712
10715
  transcript: message.transcript,
@@ -10834,14 +10837,17 @@ var WS_FORBIDDEN = 4403;
10834
10837
  var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
10835
10838
  var DEFAULT_PING_INTERVAL = 30000;
10836
10839
  var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
10840
+ var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
10841
+ var MAX_PENDING_CONTROL_MESSAGES = 100;
10837
10842
  var RECONNECT_BASE_DELAY_MS = 500;
10838
10843
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
10839
10844
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
10840
10845
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
10841
10846
  var noop = () => {};
10847
+ var noopAsync = () => Promise.resolve();
10842
10848
  var noopUnsubscribe = () => noop;
10843
10849
  var NOOP_CONNECTION = {
10844
- callControl: noop,
10850
+ callControl: noopAsync,
10845
10851
  close: noop,
10846
10852
  disconnect: noop,
10847
10853
  endTurn: noop,
@@ -10879,6 +10885,7 @@ var isVoiceServerMessage = (value) => {
10879
10885
  case "audio":
10880
10886
  case "assistant":
10881
10887
  case "call_lifecycle":
10888
+ case "call_control_ack":
10882
10889
  case "complete":
10883
10890
  case "connection":
10884
10891
  case "error":
@@ -10909,6 +10916,7 @@ var createVoiceConnection = (path, options = {}) => {
10909
10916
  return NOOP_CONNECTION;
10910
10917
  }
10911
10918
  const listeners = new Set;
10919
+ const pendingCallControls = new Map;
10912
10920
  const shouldReconnect = options.reconnect !== false;
10913
10921
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
10914
10922
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
@@ -10929,6 +10937,21 @@ var createVoiceConnection = (path, options = {}) => {
10929
10937
  const emitConnection = (reconnect) => {
10930
10938
  listeners.forEach((listener) => listener(reconnect));
10931
10939
  };
10940
+ const emitTerminalFailure = (message) => {
10941
+ listeners.forEach((listener) => listener({
10942
+ message,
10943
+ recoverable: false,
10944
+ type: "error"
10945
+ }));
10946
+ emitConnection({
10947
+ reconnect: {
10948
+ attempts: state.reconnectAttempts,
10949
+ maxAttempts: maxReconnectAttempts,
10950
+ status: "exhausted"
10951
+ },
10952
+ type: "connection"
10953
+ });
10954
+ };
10932
10955
  const clearTimers = () => {
10933
10956
  if (state.pingInterval) {
10934
10957
  clearInterval(state.pingInterval);
@@ -10954,6 +10977,13 @@ var createVoiceConnection = (path, options = {}) => {
10954
10977
  }
10955
10978
  }
10956
10979
  };
10980
+ const rejectPendingCallControls = (message) => {
10981
+ for (const pending of pendingCallControls.values()) {
10982
+ clearTimeout(pending.timeout);
10983
+ pending.reject(new Error(message));
10984
+ }
10985
+ pendingCallControls.clear();
10986
+ };
10957
10987
  const scheduleReconnect = () => {
10958
10988
  state.reconnectAttempts += 1;
10959
10989
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
@@ -10968,16 +10998,24 @@ var createVoiceConnection = (path, options = {}) => {
10968
10998
  },
10969
10999
  type: "connection"
10970
11000
  });
10971
- state.reconnectTimeout = setTimeout(() => {
11001
+ state.reconnectTimeout = setTimeout(async () => {
10972
11002
  if (state.reconnectAttempts > maxReconnectAttempts) {
10973
- emitConnection({
10974
- reconnect: {
10975
- attempts: state.reconnectAttempts,
10976
- maxAttempts: maxReconnectAttempts,
10977
- status: "exhausted"
10978
- },
10979
- type: "connection"
11003
+ emitTerminalFailure("Voice connection could not be restored.");
11004
+ return;
11005
+ }
11006
+ try {
11007
+ await options.prepareReconnect?.({
11008
+ attempt: state.reconnectAttempts,
11009
+ path,
11010
+ scenarioId: state.scenarioId,
11011
+ sessionId: state.sessionId
10980
11012
  });
11013
+ } catch {
11014
+ if (state.reconnectAttempts < maxReconnectAttempts) {
11015
+ scheduleReconnect();
11016
+ } else {
11017
+ emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
11018
+ }
10981
11019
  return;
10982
11020
  }
10983
11021
  connect();
@@ -11034,6 +11072,18 @@ var createVoiceConnection = (path, options = {}) => {
11034
11072
  state.sessionId = parsed.sessionId;
11035
11073
  state.scenarioId = parsed.scenarioId ?? state.scenarioId;
11036
11074
  }
11075
+ if (parsed.type === "call_control_ack") {
11076
+ const pending = pendingCallControls.get(parsed.requestId);
11077
+ if (pending) {
11078
+ clearTimeout(pending.timeout);
11079
+ pendingCallControls.delete(parsed.requestId);
11080
+ if (parsed.ok) {
11081
+ pending.resolve();
11082
+ } else {
11083
+ pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
11084
+ }
11085
+ }
11086
+ }
11037
11087
  listeners.forEach((listener) => listener(parsed));
11038
11088
  };
11039
11089
  ws.onclose = (event) => {
@@ -11043,18 +11093,30 @@ var createVoiceConnection = (path, options = {}) => {
11043
11093
  state.isConnected = false;
11044
11094
  clearTimers();
11045
11095
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
11096
+ const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
11097
+ if (authorizationFailure) {
11098
+ listeners.forEach((listener) => listener({
11099
+ message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
11100
+ recoverable: false,
11101
+ type: "error"
11102
+ }));
11103
+ }
11046
11104
  if (reconnectable) {
11047
11105
  scheduleReconnect();
11048
11106
  } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
11049
- emitConnection({
11050
- reconnect: {
11051
- attempts: state.reconnectAttempts,
11052
- lastDisconnectAt: Date.now(),
11053
- maxAttempts: maxReconnectAttempts,
11054
- status: "exhausted"
11055
- },
11056
- type: "connection"
11057
- });
11107
+ if (authorizationFailure) {
11108
+ emitConnection({
11109
+ reconnect: {
11110
+ attempts: state.reconnectAttempts,
11111
+ lastDisconnectAt: Date.now(),
11112
+ maxAttempts: maxReconnectAttempts,
11113
+ status: "exhausted"
11114
+ },
11115
+ type: "connection"
11116
+ });
11117
+ } else {
11118
+ emitTerminalFailure("Voice connection could not be restored.");
11119
+ }
11058
11120
  }
11059
11121
  };
11060
11122
  state.ws = ws;
@@ -11064,6 +11126,12 @@ var createVoiceConnection = (path, options = {}) => {
11064
11126
  state.ws.send(value);
11065
11127
  return;
11066
11128
  }
11129
+ if (typeof value !== "string") {
11130
+ return;
11131
+ }
11132
+ if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
11133
+ state.pendingMessages.shift();
11134
+ }
11067
11135
  state.pendingMessages.push(value);
11068
11136
  };
11069
11137
  const send = (message) => {
@@ -11089,13 +11157,23 @@ var createVoiceConnection = (path, options = {}) => {
11089
11157
  send({ type: "end_turn" });
11090
11158
  };
11091
11159
  const callControl = (message) => {
11160
+ const requestId = crypto.randomUUID();
11161
+ const { promise, reject, resolve } = Promise.withResolvers();
11162
+ const timeout = setTimeout(() => {
11163
+ pendingCallControls.delete(requestId);
11164
+ reject(new Error(`${message.action} acknowledgement timed out`));
11165
+ }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
11166
+ pendingCallControls.set(requestId, { reject, resolve, timeout });
11092
11167
  send({
11093
11168
  ...message,
11169
+ requestId,
11094
11170
  type: "call_control"
11095
11171
  });
11172
+ return promise;
11096
11173
  };
11097
11174
  const close = (reason = "client-close") => {
11098
11175
  clearTimers();
11176
+ rejectPendingCallControls("Voice connection closed");
11099
11177
  if (state.ws) {
11100
11178
  const ws = state.ws;
11101
11179
  state.ws = null;
@@ -11109,6 +11187,7 @@ var createVoiceConnection = (path, options = {}) => {
11109
11187
  };
11110
11188
  const disconnect = () => {
11111
11189
  clearTimers();
11190
+ rejectPendingCallControls("Voice connection disconnected");
11112
11191
  if (state.ws) {
11113
11192
  const ws = state.ws;
11114
11193
  state.ws = null;
@@ -11271,7 +11350,8 @@ var createVoiceStreamStore = () => {
11271
11350
  case "error":
11272
11351
  state = {
11273
11352
  ...state,
11274
- error: action.message
11353
+ error: action.message,
11354
+ ...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
11275
11355
  };
11276
11356
  break;
11277
11357
  case "final":
@@ -11411,7 +11491,7 @@ var createVoiceStream = (path, options = {}) => {
11411
11491
  return store.getSnapshot().call;
11412
11492
  },
11413
11493
  callControl(message) {
11414
- connection.callControl(message);
11494
+ return connection.callControl(message);
11415
11495
  },
11416
11496
  close(reason) {
11417
11497
  unsubscribeConnection();
@@ -22,7 +22,7 @@ export declare function useVoiceStream<TResult = unknown>(path: string, options?
22
22
  sessionMetadata: import("vue").ShallowRef<Record<string, unknown> | null, Record<string, unknown> | null>;
23
23
  status: import("vue").Ref<import("..").VoiceSessionStatus | "idle", import("..").VoiceSessionStatus | "idle">;
24
24
  turns: import("vue").ShallowRef<VoiceTurnRecord<TResult>[], VoiceTurnRecord<TResult>[]>;
25
- callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "type">) => void>[0]) => void;
25
+ callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "requestId" | "type">) => Promise<void>>[0]) => Promise<void>;
26
26
  close: () => void;
27
27
  endTurn: () => void;
28
28
  sendAudio: (audio: Uint8Array | ArrayBuffer) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.653",
3
+ "version": "0.0.22-beta.655",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",