@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.
- package/dist/angular/index.js +79 -4
- 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 +79 -4
- package/dist/client/index.js +79 -4
- package/dist/core/types.d.ts +21 -3
- package/dist/embed/index.js +79 -4
- package/dist/embed/voice-widget.js +8 -8
- package/dist/index.js +55 -40
- package/dist/react/index.js +79 -4
- package/dist/react/useVoiceController.d.ts +1 -1
- package/dist/react/useVoiceStream.d.ts +1 -1
- package/dist/svelte/index.js +79 -4
- package/dist/testing/index.js +79 -4
- package/dist/vue/index.js +79 -4
- package/dist/vue/useVoiceStream.d.ts +1 -1
- package/package.json +1 -1
package/dist/react/index.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
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;
|
package/dist/svelte/index.js
CHANGED
|
@@ -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:
|
|
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();
|
package/dist/testing/index.js
CHANGED
|
@@ -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:
|
|
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();
|