@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/angular/index.js +100 -20
- package/dist/angular/voice-stream.service.d.ts +1 -1
- package/dist/client/actions.d.ts +12 -0
- package/dist/client/connection.d.ts +1 -1
- package/dist/client/htmxBootstrap.js +100 -20
- package/dist/client/index.js +100 -20
- package/dist/core/types.d.ts +21 -3
- package/dist/embed/index.js +100 -20
- package/dist/embed/voice-widget.js +8 -8
- package/dist/index.js +55 -40
- package/dist/react/index.js +100 -20
- package/dist/react/useVoiceController.d.ts +1 -1
- package/dist/react/useVoiceStream.d.ts +1 -1
- package/dist/svelte/index.js +100 -20
- package/dist/testing/index.js +100 -20
- package/dist/vue/index.js +100 -20
- package/dist/vue/useVoiceStream.d.ts +1 -1
- package/package.json +1 -1
package/dist/client/index.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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();
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|
} | {
|
package/dist/embed/index.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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();
|