@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.
@@ -109,14 +109,17 @@ var WS_FORBIDDEN = 4403;
109
109
  var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
110
110
  var DEFAULT_PING_INTERVAL = 30000;
111
111
  var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
112
+ var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
113
+ var MAX_PENDING_CONTROL_MESSAGES = 100;
112
114
  var RECONNECT_BASE_DELAY_MS = 500;
113
115
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
114
116
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
115
117
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
116
118
  var noop = () => {};
119
+ var noopAsync = () => Promise.resolve();
117
120
  var noopUnsubscribe = () => noop;
118
121
  var NOOP_CONNECTION = {
119
- callControl: noop,
122
+ callControl: noopAsync,
120
123
  close: noop,
121
124
  disconnect: noop,
122
125
  endTurn: noop,
@@ -154,6 +157,7 @@ var isVoiceServerMessage = (value) => {
154
157
  case "audio":
155
158
  case "assistant":
156
159
  case "call_lifecycle":
160
+ case "call_control_ack":
157
161
  case "complete":
158
162
  case "connection":
159
163
  case "error":
@@ -184,6 +188,7 @@ var createVoiceConnection = (path, options = {}) => {
184
188
  return NOOP_CONNECTION;
185
189
  }
186
190
  const listeners = new Set;
191
+ const pendingCallControls = new Map;
187
192
  const shouldReconnect = options.reconnect !== false;
188
193
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
189
194
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
@@ -204,6 +209,21 @@ var createVoiceConnection = (path, options = {}) => {
204
209
  const emitConnection = (reconnect) => {
205
210
  listeners.forEach((listener) => listener(reconnect));
206
211
  };
212
+ const emitTerminalFailure = (message) => {
213
+ listeners.forEach((listener) => listener({
214
+ message,
215
+ recoverable: false,
216
+ type: "error"
217
+ }));
218
+ emitConnection({
219
+ reconnect: {
220
+ attempts: state.reconnectAttempts,
221
+ maxAttempts: maxReconnectAttempts,
222
+ status: "exhausted"
223
+ },
224
+ type: "connection"
225
+ });
226
+ };
207
227
  const clearTimers = () => {
208
228
  if (state.pingInterval) {
209
229
  clearInterval(state.pingInterval);
@@ -229,6 +249,13 @@ var createVoiceConnection = (path, options = {}) => {
229
249
  }
230
250
  }
231
251
  };
252
+ const rejectPendingCallControls = (message) => {
253
+ for (const pending of pendingCallControls.values()) {
254
+ clearTimeout(pending.timeout);
255
+ pending.reject(new Error(message));
256
+ }
257
+ pendingCallControls.clear();
258
+ };
232
259
  const scheduleReconnect = () => {
233
260
  state.reconnectAttempts += 1;
234
261
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
@@ -243,16 +270,24 @@ var createVoiceConnection = (path, options = {}) => {
243
270
  },
244
271
  type: "connection"
245
272
  });
246
- state.reconnectTimeout = setTimeout(() => {
273
+ state.reconnectTimeout = setTimeout(async () => {
247
274
  if (state.reconnectAttempts > maxReconnectAttempts) {
248
- emitConnection({
249
- reconnect: {
250
- attempts: state.reconnectAttempts,
251
- maxAttempts: maxReconnectAttempts,
252
- status: "exhausted"
253
- },
254
- type: "connection"
275
+ emitTerminalFailure("Voice connection could not be restored.");
276
+ return;
277
+ }
278
+ try {
279
+ await options.prepareReconnect?.({
280
+ attempt: state.reconnectAttempts,
281
+ path,
282
+ scenarioId: state.scenarioId,
283
+ sessionId: state.sessionId
255
284
  });
285
+ } catch {
286
+ if (state.reconnectAttempts < maxReconnectAttempts) {
287
+ scheduleReconnect();
288
+ } else {
289
+ emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
290
+ }
256
291
  return;
257
292
  }
258
293
  connect();
@@ -309,6 +344,18 @@ var createVoiceConnection = (path, options = {}) => {
309
344
  state.sessionId = parsed.sessionId;
310
345
  state.scenarioId = parsed.scenarioId ?? state.scenarioId;
311
346
  }
347
+ if (parsed.type === "call_control_ack") {
348
+ const pending = pendingCallControls.get(parsed.requestId);
349
+ if (pending) {
350
+ clearTimeout(pending.timeout);
351
+ pendingCallControls.delete(parsed.requestId);
352
+ if (parsed.ok) {
353
+ pending.resolve();
354
+ } else {
355
+ pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
356
+ }
357
+ }
358
+ }
312
359
  listeners.forEach((listener) => listener(parsed));
313
360
  };
314
361
  ws.onclose = (event) => {
@@ -318,18 +365,30 @@ var createVoiceConnection = (path, options = {}) => {
318
365
  state.isConnected = false;
319
366
  clearTimers();
320
367
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
368
+ const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
369
+ if (authorizationFailure) {
370
+ listeners.forEach((listener) => listener({
371
+ message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
372
+ recoverable: false,
373
+ type: "error"
374
+ }));
375
+ }
321
376
  if (reconnectable) {
322
377
  scheduleReconnect();
323
378
  } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
324
- emitConnection({
325
- reconnect: {
326
- attempts: state.reconnectAttempts,
327
- lastDisconnectAt: Date.now(),
328
- maxAttempts: maxReconnectAttempts,
329
- status: "exhausted"
330
- },
331
- type: "connection"
332
- });
379
+ if (authorizationFailure) {
380
+ emitConnection({
381
+ reconnect: {
382
+ attempts: state.reconnectAttempts,
383
+ lastDisconnectAt: Date.now(),
384
+ maxAttempts: maxReconnectAttempts,
385
+ status: "exhausted"
386
+ },
387
+ type: "connection"
388
+ });
389
+ } else {
390
+ emitTerminalFailure("Voice connection could not be restored.");
391
+ }
333
392
  }
334
393
  };
335
394
  state.ws = ws;
@@ -339,6 +398,12 @@ var createVoiceConnection = (path, options = {}) => {
339
398
  state.ws.send(value);
340
399
  return;
341
400
  }
401
+ if (typeof value !== "string") {
402
+ return;
403
+ }
404
+ if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
405
+ state.pendingMessages.shift();
406
+ }
342
407
  state.pendingMessages.push(value);
343
408
  };
344
409
  const send = (message) => {
@@ -364,13 +429,23 @@ var createVoiceConnection = (path, options = {}) => {
364
429
  send({ type: "end_turn" });
365
430
  };
366
431
  const callControl = (message) => {
432
+ const requestId = crypto.randomUUID();
433
+ const { promise, reject, resolve } = Promise.withResolvers();
434
+ const timeout = setTimeout(() => {
435
+ pendingCallControls.delete(requestId);
436
+ reject(new Error(`${message.action} acknowledgement timed out`));
437
+ }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
438
+ pendingCallControls.set(requestId, { reject, resolve, timeout });
367
439
  send({
368
440
  ...message,
441
+ requestId,
369
442
  type: "call_control"
370
443
  });
444
+ return promise;
371
445
  };
372
446
  const close = (reason = "client-close") => {
373
447
  clearTimers();
448
+ rejectPendingCallControls("Voice connection closed");
374
449
  if (state.ws) {
375
450
  const ws = state.ws;
376
451
  state.ws = null;
@@ -384,6 +459,7 @@ var createVoiceConnection = (path, options = {}) => {
384
459
  };
385
460
  const disconnect = () => {
386
461
  clearTimers();
462
+ rejectPendingCallControls("Voice connection disconnected");
387
463
  if (state.ws) {
388
464
  const ws = state.ws;
389
465
  state.ws = null;
@@ -1219,8 +1295,11 @@ var serverMessageToAction = (message) => {
1219
1295
  case "error":
1220
1296
  return {
1221
1297
  message: normalizeErrorMessage(message.message),
1298
+ recoverable: message.recoverable,
1222
1299
  type: "error"
1223
1300
  };
1301
+ case "call_control_ack":
1302
+ return null;
1224
1303
  case "final":
1225
1304
  return {
1226
1305
  transcript: message.transcript,
@@ -1465,7 +1544,8 @@ var createVoiceStreamStore = () => {
1465
1544
  case "error":
1466
1545
  state = {
1467
1546
  ...state,
1468
- error: action.message
1547
+ error: action.message,
1548
+ ...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
1469
1549
  };
1470
1550
  break;
1471
1551
  case "final":
@@ -1605,7 +1685,7 @@ var createVoiceStream = (path, options = {}) => {
1605
1685
  return store.getSnapshot().call;
1606
1686
  },
1607
1687
  callControl(message) {
1608
- connection.callControl(message);
1688
+ return connection.callControl(message);
1609
1689
  },
1610
1690
  close(reason) {
1611
1691
  unsubscribeConnection();
@@ -1256,11 +1256,19 @@ export type VoiceClientCloseMessage = {
1256
1256
  };
1257
1257
  export type VoiceClientCallControlMessage = {
1258
1258
  type: "call_control";
1259
+ requestId?: string;
1259
1260
  action: "complete" | "escalate" | "no-answer" | "pause" | "resume" | "transfer" | "voicemail";
1260
1261
  metadata?: Record<string, unknown>;
1261
1262
  reason?: string;
1262
1263
  target?: string;
1263
1264
  };
1265
+ export type VoiceServerCallControlAckMessage = {
1266
+ type: "call_control_ack";
1267
+ action: VoiceClientCallControlMessage["action"];
1268
+ message?: string;
1269
+ ok: boolean;
1270
+ requestId: string;
1271
+ };
1264
1272
  export type VoiceClientPingMessage = {
1265
1273
  type: "ping";
1266
1274
  };
@@ -1333,7 +1341,7 @@ export type VoiceServerConnectionMessage = {
1333
1341
  type: "connection";
1334
1342
  reconnect: VoiceReconnectClientState;
1335
1343
  };
1336
- export type VoiceServerMessage<TResult = unknown> = VoiceServerSessionMessage | VoiceServerReplayMessage<TResult> | VoiceServerPartialMessage | VoiceServerFinalMessage | VoiceServerTurnMessage<TResult> | VoiceServerAssistantMessage | VoiceServerAssistantDeltaMessage | VoiceServerAudioMessage | VoiceServerCallLifecycleMessage | VoiceServerCompleteMessage | VoiceServerErrorMessage | VoiceServerPongMessage | VoiceServerConnectionMessage;
1344
+ export type VoiceServerMessage<TResult = unknown> = VoiceServerSessionMessage | VoiceServerReplayMessage<TResult> | VoiceServerPartialMessage | VoiceServerFinalMessage | VoiceServerTurnMessage<TResult> | VoiceServerAssistantMessage | VoiceServerAssistantDeltaMessage | VoiceServerAudioMessage | VoiceServerCallControlAckMessage | VoiceServerCallLifecycleMessage | VoiceServerCompleteMessage | VoiceServerErrorMessage | VoiceServerPongMessage | VoiceServerConnectionMessage;
1337
1345
  export type VoiceConnectionOptions = {
1338
1346
  browserMedia?: false | VoiceBrowserMediaReporterOptions;
1339
1347
  protocols?: string[];
@@ -1351,6 +1359,15 @@ export type VoiceConnectionOptions = {
1351
1359
  * reset. This prevents a server that accepts and immediately drops sockets
1352
1360
  * from creating an unbounded reconnect loop. Default 30000. */
1353
1361
  reconnectResetAfterMs?: number;
1362
+ /** Refresh application-owned admission state before each reconnect attempt.
1363
+ * Throwing keeps the transport disconnected and consumes one bounded retry
1364
+ * attempt instead of opening a socket with stale credentials. */
1365
+ prepareReconnect?: (input: {
1366
+ attempt: number;
1367
+ path: string;
1368
+ scenarioId: string | null;
1369
+ sessionId: string;
1370
+ }) => Promise<void> | void;
1354
1371
  pingInterval?: number;
1355
1372
  /** Additional query values sent on every initial or reconnecting socket. */
1356
1373
  query?: Record<string, string>;
@@ -1524,7 +1541,7 @@ export type VoiceStreamState<TResult = unknown> = {
1524
1541
  };
1525
1542
  export type VoiceStream<TResult = unknown> = {
1526
1543
  call: VoiceCallLifecycleState | null;
1527
- callControl: (message: Omit<VoiceClientCallControlMessage, "type">) => void;
1544
+ callControl: (message: Omit<VoiceClientCallControlMessage, "requestId" | "type">) => Promise<void>;
1528
1545
  close: (reason?: string) => void;
1529
1546
  /** Release the transport without terminally completing the server session. */
1530
1547
  disconnect: () => void;
@@ -1621,7 +1638,7 @@ export type VoiceBargeInBinding = {
1621
1638
  export type VoiceController<TResult = unknown> = {
1622
1639
  bindHTMX: (options: VoiceHTMXBindingOptions) => () => void;
1623
1640
  call: VoiceCallLifecycleState | null;
1624
- callControl: (message: Omit<VoiceClientCallControlMessage, "type">) => void;
1641
+ callControl: (message: Omit<VoiceClientCallControlMessage, "requestId" | "type">) => Promise<void>;
1625
1642
  close: (reason?: string) => void;
1626
1643
  /** Release the transport while leaving the server session resumable. */
1627
1644
  disconnect: () => void;
@@ -1717,6 +1734,7 @@ export type VoiceStoreAction<TResult = unknown> = {
1717
1734
  } | {
1718
1735
  type: "error";
1719
1736
  message: string;
1737
+ recoverable?: boolean;
1720
1738
  } | {
1721
1739
  type: "connected";
1722
1740
  } | {
@@ -220,8 +220,11 @@ var serverMessageToAction = (message) => {
220
220
  case "error":
221
221
  return {
222
222
  message: normalizeErrorMessage(message.message),
223
+ recoverable: message.recoverable,
223
224
  type: "error"
224
225
  };
226
+ case "call_control_ack":
227
+ return null;
225
228
  case "final":
226
229
  return {
227
230
  transcript: message.transcript,
@@ -498,14 +501,17 @@ var WS_FORBIDDEN = 4403;
498
501
  var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
499
502
  var DEFAULT_PING_INTERVAL = 30000;
500
503
  var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
504
+ var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
505
+ var MAX_PENDING_CONTROL_MESSAGES = 100;
501
506
  var RECONNECT_BASE_DELAY_MS = 500;
502
507
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
503
508
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
504
509
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
505
510
  var noop = () => {};
511
+ var noopAsync = () => Promise.resolve();
506
512
  var noopUnsubscribe = () => noop;
507
513
  var NOOP_CONNECTION = {
508
- callControl: noop,
514
+ callControl: noopAsync,
509
515
  close: noop,
510
516
  disconnect: noop,
511
517
  endTurn: noop,
@@ -543,6 +549,7 @@ var isVoiceServerMessage = (value) => {
543
549
  case "audio":
544
550
  case "assistant":
545
551
  case "call_lifecycle":
552
+ case "call_control_ack":
546
553
  case "complete":
547
554
  case "connection":
548
555
  case "error":
@@ -573,6 +580,7 @@ var createVoiceConnection = (path, options = {}) => {
573
580
  return NOOP_CONNECTION;
574
581
  }
575
582
  const listeners = new Set;
583
+ const pendingCallControls = new Map;
576
584
  const shouldReconnect = options.reconnect !== false;
577
585
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
578
586
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
@@ -593,6 +601,21 @@ var createVoiceConnection = (path, options = {}) => {
593
601
  const emitConnection = (reconnect) => {
594
602
  listeners.forEach((listener) => listener(reconnect));
595
603
  };
604
+ const emitTerminalFailure = (message) => {
605
+ listeners.forEach((listener) => listener({
606
+ message,
607
+ recoverable: false,
608
+ type: "error"
609
+ }));
610
+ emitConnection({
611
+ reconnect: {
612
+ attempts: state.reconnectAttempts,
613
+ maxAttempts: maxReconnectAttempts,
614
+ status: "exhausted"
615
+ },
616
+ type: "connection"
617
+ });
618
+ };
596
619
  const clearTimers = () => {
597
620
  if (state.pingInterval) {
598
621
  clearInterval(state.pingInterval);
@@ -618,6 +641,13 @@ var createVoiceConnection = (path, options = {}) => {
618
641
  }
619
642
  }
620
643
  };
644
+ const rejectPendingCallControls = (message) => {
645
+ for (const pending of pendingCallControls.values()) {
646
+ clearTimeout(pending.timeout);
647
+ pending.reject(new Error(message));
648
+ }
649
+ pendingCallControls.clear();
650
+ };
621
651
  const scheduleReconnect = () => {
622
652
  state.reconnectAttempts += 1;
623
653
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
@@ -632,16 +662,24 @@ var createVoiceConnection = (path, options = {}) => {
632
662
  },
633
663
  type: "connection"
634
664
  });
635
- state.reconnectTimeout = setTimeout(() => {
665
+ state.reconnectTimeout = setTimeout(async () => {
636
666
  if (state.reconnectAttempts > maxReconnectAttempts) {
637
- emitConnection({
638
- reconnect: {
639
- attempts: state.reconnectAttempts,
640
- maxAttempts: maxReconnectAttempts,
641
- status: "exhausted"
642
- },
643
- type: "connection"
667
+ emitTerminalFailure("Voice connection could not be restored.");
668
+ return;
669
+ }
670
+ try {
671
+ await options.prepareReconnect?.({
672
+ attempt: state.reconnectAttempts,
673
+ path,
674
+ scenarioId: state.scenarioId,
675
+ sessionId: state.sessionId
644
676
  });
677
+ } catch {
678
+ if (state.reconnectAttempts < maxReconnectAttempts) {
679
+ scheduleReconnect();
680
+ } else {
681
+ emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
682
+ }
645
683
  return;
646
684
  }
647
685
  connect();
@@ -698,6 +736,18 @@ var createVoiceConnection = (path, options = {}) => {
698
736
  state.sessionId = parsed.sessionId;
699
737
  state.scenarioId = parsed.scenarioId ?? state.scenarioId;
700
738
  }
739
+ if (parsed.type === "call_control_ack") {
740
+ const pending = pendingCallControls.get(parsed.requestId);
741
+ if (pending) {
742
+ clearTimeout(pending.timeout);
743
+ pendingCallControls.delete(parsed.requestId);
744
+ if (parsed.ok) {
745
+ pending.resolve();
746
+ } else {
747
+ pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
748
+ }
749
+ }
750
+ }
701
751
  listeners.forEach((listener) => listener(parsed));
702
752
  };
703
753
  ws.onclose = (event) => {
@@ -707,18 +757,30 @@ var createVoiceConnection = (path, options = {}) => {
707
757
  state.isConnected = false;
708
758
  clearTimers();
709
759
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
760
+ const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
761
+ if (authorizationFailure) {
762
+ listeners.forEach((listener) => listener({
763
+ message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
764
+ recoverable: false,
765
+ type: "error"
766
+ }));
767
+ }
710
768
  if (reconnectable) {
711
769
  scheduleReconnect();
712
770
  } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
713
- emitConnection({
714
- reconnect: {
715
- attempts: state.reconnectAttempts,
716
- lastDisconnectAt: Date.now(),
717
- maxAttempts: maxReconnectAttempts,
718
- status: "exhausted"
719
- },
720
- type: "connection"
721
- });
771
+ if (authorizationFailure) {
772
+ emitConnection({
773
+ reconnect: {
774
+ attempts: state.reconnectAttempts,
775
+ lastDisconnectAt: Date.now(),
776
+ maxAttempts: maxReconnectAttempts,
777
+ status: "exhausted"
778
+ },
779
+ type: "connection"
780
+ });
781
+ } else {
782
+ emitTerminalFailure("Voice connection could not be restored.");
783
+ }
722
784
  }
723
785
  };
724
786
  state.ws = ws;
@@ -728,6 +790,12 @@ var createVoiceConnection = (path, options = {}) => {
728
790
  state.ws.send(value);
729
791
  return;
730
792
  }
793
+ if (typeof value !== "string") {
794
+ return;
795
+ }
796
+ if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
797
+ state.pendingMessages.shift();
798
+ }
731
799
  state.pendingMessages.push(value);
732
800
  };
733
801
  const send = (message) => {
@@ -753,13 +821,23 @@ var createVoiceConnection = (path, options = {}) => {
753
821
  send({ type: "end_turn" });
754
822
  };
755
823
  const callControl = (message) => {
824
+ const requestId = crypto.randomUUID();
825
+ const { promise, reject, resolve } = Promise.withResolvers();
826
+ const timeout = setTimeout(() => {
827
+ pendingCallControls.delete(requestId);
828
+ reject(new Error(`${message.action} acknowledgement timed out`));
829
+ }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
830
+ pendingCallControls.set(requestId, { reject, resolve, timeout });
756
831
  send({
757
832
  ...message,
833
+ requestId,
758
834
  type: "call_control"
759
835
  });
836
+ return promise;
760
837
  };
761
838
  const close = (reason = "client-close") => {
762
839
  clearTimers();
840
+ rejectPendingCallControls("Voice connection closed");
763
841
  if (state.ws) {
764
842
  const ws = state.ws;
765
843
  state.ws = null;
@@ -773,6 +851,7 @@ var createVoiceConnection = (path, options = {}) => {
773
851
  };
774
852
  const disconnect = () => {
775
853
  clearTimers();
854
+ rejectPendingCallControls("Voice connection disconnected");
776
855
  if (state.ws) {
777
856
  const ws = state.ws;
778
857
  state.ws = null;
@@ -935,7 +1014,8 @@ var createVoiceStreamStore = () => {
935
1014
  case "error":
936
1015
  state = {
937
1016
  ...state,
938
- error: action.message
1017
+ error: action.message,
1018
+ ...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
939
1019
  };
940
1020
  break;
941
1021
  case "final":
@@ -1075,7 +1155,7 @@ var createVoiceStream = (path, options = {}) => {
1075
1155
  return store.getSnapshot().call;
1076
1156
  },
1077
1157
  callControl(message) {
1078
- connection.callControl(message);
1158
+ return connection.callControl(message);
1079
1159
  },
1080
1160
  close(reason) {
1081
1161
  unsubscribeConnection();