@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/angular/index.js
CHANGED
|
@@ -455,8 +455,11 @@ var serverMessageToAction = (message) => {
|
|
|
455
455
|
case "error":
|
|
456
456
|
return {
|
|
457
457
|
message: normalizeErrorMessage(message.message),
|
|
458
|
+
recoverable: message.recoverable,
|
|
458
459
|
type: "error"
|
|
459
460
|
};
|
|
461
|
+
case "call_control_ack":
|
|
462
|
+
return null;
|
|
460
463
|
case "final":
|
|
461
464
|
return {
|
|
462
465
|
transcript: message.transcript,
|
|
@@ -584,14 +587,17 @@ var WS_FORBIDDEN = 4403;
|
|
|
584
587
|
var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
585
588
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
586
589
|
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
590
|
+
var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
|
|
591
|
+
var MAX_PENDING_CONTROL_MESSAGES = 100;
|
|
587
592
|
var RECONNECT_BASE_DELAY_MS = 500;
|
|
588
593
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
589
594
|
var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
|
|
590
595
|
var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
|
|
591
596
|
var noop = () => {};
|
|
597
|
+
var noopAsync = () => Promise.resolve();
|
|
592
598
|
var noopUnsubscribe = () => noop;
|
|
593
599
|
var NOOP_CONNECTION = {
|
|
594
|
-
callControl:
|
|
600
|
+
callControl: noopAsync,
|
|
595
601
|
close: noop,
|
|
596
602
|
disconnect: noop,
|
|
597
603
|
endTurn: noop,
|
|
@@ -629,6 +635,7 @@ var isVoiceServerMessage = (value) => {
|
|
|
629
635
|
case "audio":
|
|
630
636
|
case "assistant":
|
|
631
637
|
case "call_lifecycle":
|
|
638
|
+
case "call_control_ack":
|
|
632
639
|
case "complete":
|
|
633
640
|
case "connection":
|
|
634
641
|
case "error":
|
|
@@ -659,6 +666,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
659
666
|
return NOOP_CONNECTION;
|
|
660
667
|
}
|
|
661
668
|
const listeners = new Set;
|
|
669
|
+
const pendingCallControls = new Map;
|
|
662
670
|
const shouldReconnect = options.reconnect !== false;
|
|
663
671
|
const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
664
672
|
const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
@@ -679,6 +687,21 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
679
687
|
const emitConnection = (reconnect) => {
|
|
680
688
|
listeners.forEach((listener) => listener(reconnect));
|
|
681
689
|
};
|
|
690
|
+
const emitTerminalFailure = (message) => {
|
|
691
|
+
listeners.forEach((listener) => listener({
|
|
692
|
+
message,
|
|
693
|
+
recoverable: false,
|
|
694
|
+
type: "error"
|
|
695
|
+
}));
|
|
696
|
+
emitConnection({
|
|
697
|
+
reconnect: {
|
|
698
|
+
attempts: state.reconnectAttempts,
|
|
699
|
+
maxAttempts: maxReconnectAttempts,
|
|
700
|
+
status: "exhausted"
|
|
701
|
+
},
|
|
702
|
+
type: "connection"
|
|
703
|
+
});
|
|
704
|
+
};
|
|
682
705
|
const clearTimers = () => {
|
|
683
706
|
if (state.pingInterval) {
|
|
684
707
|
clearInterval(state.pingInterval);
|
|
@@ -704,6 +727,13 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
704
727
|
}
|
|
705
728
|
}
|
|
706
729
|
};
|
|
730
|
+
const rejectPendingCallControls = (message) => {
|
|
731
|
+
for (const pending of pendingCallControls.values()) {
|
|
732
|
+
clearTimeout(pending.timeout);
|
|
733
|
+
pending.reject(new Error(message));
|
|
734
|
+
}
|
|
735
|
+
pendingCallControls.clear();
|
|
736
|
+
};
|
|
707
737
|
const scheduleReconnect = () => {
|
|
708
738
|
state.reconnectAttempts += 1;
|
|
709
739
|
const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
|
|
@@ -718,16 +748,24 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
718
748
|
},
|
|
719
749
|
type: "connection"
|
|
720
750
|
});
|
|
721
|
-
state.reconnectTimeout = setTimeout(() => {
|
|
751
|
+
state.reconnectTimeout = setTimeout(async () => {
|
|
722
752
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
753
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
await options.prepareReconnect?.({
|
|
758
|
+
attempt: state.reconnectAttempts,
|
|
759
|
+
path,
|
|
760
|
+
scenarioId: state.scenarioId,
|
|
761
|
+
sessionId: state.sessionId
|
|
730
762
|
});
|
|
763
|
+
} catch {
|
|
764
|
+
if (state.reconnectAttempts < maxReconnectAttempts) {
|
|
765
|
+
scheduleReconnect();
|
|
766
|
+
} else {
|
|
767
|
+
emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
|
|
768
|
+
}
|
|
731
769
|
return;
|
|
732
770
|
}
|
|
733
771
|
connect();
|
|
@@ -784,6 +822,18 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
784
822
|
state.sessionId = parsed.sessionId;
|
|
785
823
|
state.scenarioId = parsed.scenarioId ?? state.scenarioId;
|
|
786
824
|
}
|
|
825
|
+
if (parsed.type === "call_control_ack") {
|
|
826
|
+
const pending = pendingCallControls.get(parsed.requestId);
|
|
827
|
+
if (pending) {
|
|
828
|
+
clearTimeout(pending.timeout);
|
|
829
|
+
pendingCallControls.delete(parsed.requestId);
|
|
830
|
+
if (parsed.ok) {
|
|
831
|
+
pending.resolve();
|
|
832
|
+
} else {
|
|
833
|
+
pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
787
837
|
listeners.forEach((listener) => listener(parsed));
|
|
788
838
|
};
|
|
789
839
|
ws.onclose = (event) => {
|
|
@@ -793,18 +843,30 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
793
843
|
state.isConnected = false;
|
|
794
844
|
clearTimers();
|
|
795
845
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
|
|
846
|
+
const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
|
|
847
|
+
if (authorizationFailure) {
|
|
848
|
+
listeners.forEach((listener) => listener({
|
|
849
|
+
message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
|
|
850
|
+
recoverable: false,
|
|
851
|
+
type: "error"
|
|
852
|
+
}));
|
|
853
|
+
}
|
|
796
854
|
if (reconnectable) {
|
|
797
855
|
scheduleReconnect();
|
|
798
856
|
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
857
|
+
if (authorizationFailure) {
|
|
858
|
+
emitConnection({
|
|
859
|
+
reconnect: {
|
|
860
|
+
attempts: state.reconnectAttempts,
|
|
861
|
+
lastDisconnectAt: Date.now(),
|
|
862
|
+
maxAttempts: maxReconnectAttempts,
|
|
863
|
+
status: "exhausted"
|
|
864
|
+
},
|
|
865
|
+
type: "connection"
|
|
866
|
+
});
|
|
867
|
+
} else {
|
|
868
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
869
|
+
}
|
|
808
870
|
}
|
|
809
871
|
};
|
|
810
872
|
state.ws = ws;
|
|
@@ -814,6 +876,12 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
814
876
|
state.ws.send(value);
|
|
815
877
|
return;
|
|
816
878
|
}
|
|
879
|
+
if (typeof value !== "string") {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
|
|
883
|
+
state.pendingMessages.shift();
|
|
884
|
+
}
|
|
817
885
|
state.pendingMessages.push(value);
|
|
818
886
|
};
|
|
819
887
|
const send = (message) => {
|
|
@@ -839,13 +907,23 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
839
907
|
send({ type: "end_turn" });
|
|
840
908
|
};
|
|
841
909
|
const callControl = (message) => {
|
|
910
|
+
const requestId = crypto.randomUUID();
|
|
911
|
+
const { promise, reject, resolve } = Promise.withResolvers();
|
|
912
|
+
const timeout = setTimeout(() => {
|
|
913
|
+
pendingCallControls.delete(requestId);
|
|
914
|
+
reject(new Error(`${message.action} acknowledgement timed out`));
|
|
915
|
+
}, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
|
|
916
|
+
pendingCallControls.set(requestId, { reject, resolve, timeout });
|
|
842
917
|
send({
|
|
843
918
|
...message,
|
|
919
|
+
requestId,
|
|
844
920
|
type: "call_control"
|
|
845
921
|
});
|
|
922
|
+
return promise;
|
|
846
923
|
};
|
|
847
924
|
const close = (reason = "client-close") => {
|
|
848
925
|
clearTimers();
|
|
926
|
+
rejectPendingCallControls("Voice connection closed");
|
|
849
927
|
if (state.ws) {
|
|
850
928
|
const ws = state.ws;
|
|
851
929
|
state.ws = null;
|
|
@@ -859,6 +937,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
859
937
|
};
|
|
860
938
|
const disconnect = () => {
|
|
861
939
|
clearTimers();
|
|
940
|
+
rejectPendingCallControls("Voice connection disconnected");
|
|
862
941
|
if (state.ws) {
|
|
863
942
|
const ws = state.ws;
|
|
864
943
|
state.ws = null;
|
|
@@ -1021,7 +1100,8 @@ var createVoiceStreamStore = () => {
|
|
|
1021
1100
|
case "error":
|
|
1022
1101
|
state = {
|
|
1023
1102
|
...state,
|
|
1024
|
-
error: action.message
|
|
1103
|
+
error: action.message,
|
|
1104
|
+
...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
|
|
1025
1105
|
};
|
|
1026
1106
|
break;
|
|
1027
1107
|
case "final":
|
|
@@ -1161,7 +1241,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1161
1241
|
return store.getSnapshot().call;
|
|
1162
1242
|
},
|
|
1163
1243
|
callControl(message) {
|
|
1164
|
-
connection.callControl(message);
|
|
1244
|
+
return connection.callControl(message);
|
|
1165
1245
|
},
|
|
1166
1246
|
close(reason) {
|
|
1167
1247
|
unsubscribeConnection();
|
|
@@ -10,7 +10,7 @@ export declare class VoiceStreamService {
|
|
|
10
10
|
assistantStreamingText: import("@angular/core").Signal<string>;
|
|
11
11
|
assistantTexts: import("@angular/core").Signal<string[]>;
|
|
12
12
|
call: import("@angular/core").Signal<import("..").VoiceCallLifecycleState | null>;
|
|
13
|
-
callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "type">) => void
|
|
13
|
+
callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "requestId" | "type">) => Promise<void>>[0]) => Promise<void>;
|
|
14
14
|
close: () => void;
|
|
15
15
|
endTurn: () => void;
|
|
16
16
|
error: import("@angular/core").Signal<string | null>;
|
package/dist/client/actions.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
|
|
|
11
11
|
reconnect?: undefined;
|
|
12
12
|
event?: undefined;
|
|
13
13
|
message?: undefined;
|
|
14
|
+
recoverable?: undefined;
|
|
14
15
|
transcript?: undefined;
|
|
15
16
|
assistantTexts?: undefined;
|
|
16
17
|
call?: undefined;
|
|
@@ -32,6 +33,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
|
|
|
32
33
|
reconnect?: undefined;
|
|
33
34
|
event?: undefined;
|
|
34
35
|
message?: undefined;
|
|
36
|
+
recoverable?: undefined;
|
|
35
37
|
transcript?: undefined;
|
|
36
38
|
assistantTexts?: undefined;
|
|
37
39
|
call?: undefined;
|
|
@@ -53,6 +55,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
|
|
|
53
55
|
reconnect?: undefined;
|
|
54
56
|
event?: undefined;
|
|
55
57
|
message?: undefined;
|
|
58
|
+
recoverable?: undefined;
|
|
56
59
|
transcript?: undefined;
|
|
57
60
|
assistantTexts?: undefined;
|
|
58
61
|
call?: undefined;
|
|
@@ -74,6 +77,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
|
|
|
74
77
|
reconnect?: undefined;
|
|
75
78
|
event?: undefined;
|
|
76
79
|
message?: undefined;
|
|
80
|
+
recoverable?: undefined;
|
|
77
81
|
transcript?: undefined;
|
|
78
82
|
assistantTexts?: undefined;
|
|
79
83
|
call?: undefined;
|
|
@@ -95,6 +99,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
|
|
|
95
99
|
sessionId?: undefined;
|
|
96
100
|
event?: undefined;
|
|
97
101
|
message?: undefined;
|
|
102
|
+
recoverable?: undefined;
|
|
98
103
|
transcript?: undefined;
|
|
99
104
|
assistantTexts?: undefined;
|
|
100
105
|
call?: undefined;
|
|
@@ -116,6 +121,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
|
|
|
116
121
|
delta?: undefined;
|
|
117
122
|
reconnect?: undefined;
|
|
118
123
|
message?: undefined;
|
|
124
|
+
recoverable?: undefined;
|
|
119
125
|
transcript?: undefined;
|
|
120
126
|
assistantTexts?: undefined;
|
|
121
127
|
call?: undefined;
|
|
@@ -127,6 +133,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
|
|
|
127
133
|
turn?: undefined;
|
|
128
134
|
} | {
|
|
129
135
|
message: string;
|
|
136
|
+
recoverable: boolean | undefined;
|
|
130
137
|
type: "error";
|
|
131
138
|
chunk?: undefined;
|
|
132
139
|
format?: undefined;
|
|
@@ -159,6 +166,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
|
|
|
159
166
|
reconnect?: undefined;
|
|
160
167
|
event?: undefined;
|
|
161
168
|
message?: undefined;
|
|
169
|
+
recoverable?: undefined;
|
|
162
170
|
assistantTexts?: undefined;
|
|
163
171
|
call?: undefined;
|
|
164
172
|
partial?: undefined;
|
|
@@ -180,6 +188,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
|
|
|
180
188
|
reconnect?: undefined;
|
|
181
189
|
event?: undefined;
|
|
182
190
|
message?: undefined;
|
|
191
|
+
recoverable?: undefined;
|
|
183
192
|
assistantTexts?: undefined;
|
|
184
193
|
call?: undefined;
|
|
185
194
|
partial?: undefined;
|
|
@@ -207,6 +216,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
|
|
|
207
216
|
reconnect?: undefined;
|
|
208
217
|
event?: undefined;
|
|
209
218
|
message?: undefined;
|
|
219
|
+
recoverable?: undefined;
|
|
210
220
|
transcript?: undefined;
|
|
211
221
|
turn?: undefined;
|
|
212
222
|
} | {
|
|
@@ -224,6 +234,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
|
|
|
224
234
|
reconnect?: undefined;
|
|
225
235
|
event?: undefined;
|
|
226
236
|
message?: undefined;
|
|
237
|
+
recoverable?: undefined;
|
|
227
238
|
transcript?: undefined;
|
|
228
239
|
assistantTexts?: undefined;
|
|
229
240
|
call?: undefined;
|
|
@@ -243,6 +254,7 @@ export declare const serverMessageToAction: <TResult = unknown>(message: VoiceSe
|
|
|
243
254
|
reconnect?: undefined;
|
|
244
255
|
event?: undefined;
|
|
245
256
|
message?: undefined;
|
|
257
|
+
recoverable?: undefined;
|
|
246
258
|
transcript?: undefined;
|
|
247
259
|
assistantTexts?: undefined;
|
|
248
260
|
call?: undefined;
|
|
@@ -6,7 +6,7 @@ export declare const computeVoiceReconnectDelayMs: (attempt: number, baseMs: num
|
|
|
6
6
|
type VoiceConnectionHandle = {
|
|
7
7
|
callControl: (message: Omit<VoiceClientMessage & {
|
|
8
8
|
type: "call_control";
|
|
9
|
-
}, "type">) => void
|
|
9
|
+
}, "requestId" | "type">) => Promise<void>;
|
|
10
10
|
start: (input?: {
|
|
11
11
|
sessionId?: string;
|
|
12
12
|
scenarioId?: string;
|
|
@@ -223,8 +223,11 @@ var serverMessageToAction = (message) => {
|
|
|
223
223
|
case "error":
|
|
224
224
|
return {
|
|
225
225
|
message: normalizeErrorMessage(message.message),
|
|
226
|
+
recoverable: message.recoverable,
|
|
226
227
|
type: "error"
|
|
227
228
|
};
|
|
229
|
+
case "call_control_ack":
|
|
230
|
+
return null;
|
|
228
231
|
case "final":
|
|
229
232
|
return {
|
|
230
233
|
transcript: message.transcript,
|
|
@@ -501,14 +504,17 @@ var WS_FORBIDDEN = 4403;
|
|
|
501
504
|
var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
502
505
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
503
506
|
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
507
|
+
var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
|
|
508
|
+
var MAX_PENDING_CONTROL_MESSAGES = 100;
|
|
504
509
|
var RECONNECT_BASE_DELAY_MS = 500;
|
|
505
510
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
506
511
|
var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
|
|
507
512
|
var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
|
|
508
513
|
var noop = () => {};
|
|
514
|
+
var noopAsync = () => Promise.resolve();
|
|
509
515
|
var noopUnsubscribe = () => noop;
|
|
510
516
|
var NOOP_CONNECTION = {
|
|
511
|
-
callControl:
|
|
517
|
+
callControl: noopAsync,
|
|
512
518
|
close: noop,
|
|
513
519
|
disconnect: noop,
|
|
514
520
|
endTurn: noop,
|
|
@@ -546,6 +552,7 @@ var isVoiceServerMessage = (value) => {
|
|
|
546
552
|
case "audio":
|
|
547
553
|
case "assistant":
|
|
548
554
|
case "call_lifecycle":
|
|
555
|
+
case "call_control_ack":
|
|
549
556
|
case "complete":
|
|
550
557
|
case "connection":
|
|
551
558
|
case "error":
|
|
@@ -576,6 +583,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
576
583
|
return NOOP_CONNECTION;
|
|
577
584
|
}
|
|
578
585
|
const listeners = new Set;
|
|
586
|
+
const pendingCallControls = new Map;
|
|
579
587
|
const shouldReconnect = options.reconnect !== false;
|
|
580
588
|
const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
581
589
|
const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
@@ -596,6 +604,21 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
596
604
|
const emitConnection = (reconnect) => {
|
|
597
605
|
listeners.forEach((listener) => listener(reconnect));
|
|
598
606
|
};
|
|
607
|
+
const emitTerminalFailure = (message) => {
|
|
608
|
+
listeners.forEach((listener) => listener({
|
|
609
|
+
message,
|
|
610
|
+
recoverable: false,
|
|
611
|
+
type: "error"
|
|
612
|
+
}));
|
|
613
|
+
emitConnection({
|
|
614
|
+
reconnect: {
|
|
615
|
+
attempts: state.reconnectAttempts,
|
|
616
|
+
maxAttempts: maxReconnectAttempts,
|
|
617
|
+
status: "exhausted"
|
|
618
|
+
},
|
|
619
|
+
type: "connection"
|
|
620
|
+
});
|
|
621
|
+
};
|
|
599
622
|
const clearTimers = () => {
|
|
600
623
|
if (state.pingInterval) {
|
|
601
624
|
clearInterval(state.pingInterval);
|
|
@@ -621,6 +644,13 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
621
644
|
}
|
|
622
645
|
}
|
|
623
646
|
};
|
|
647
|
+
const rejectPendingCallControls = (message) => {
|
|
648
|
+
for (const pending of pendingCallControls.values()) {
|
|
649
|
+
clearTimeout(pending.timeout);
|
|
650
|
+
pending.reject(new Error(message));
|
|
651
|
+
}
|
|
652
|
+
pendingCallControls.clear();
|
|
653
|
+
};
|
|
624
654
|
const scheduleReconnect = () => {
|
|
625
655
|
state.reconnectAttempts += 1;
|
|
626
656
|
const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
|
|
@@ -635,16 +665,24 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
635
665
|
},
|
|
636
666
|
type: "connection"
|
|
637
667
|
});
|
|
638
|
-
state.reconnectTimeout = setTimeout(() => {
|
|
668
|
+
state.reconnectTimeout = setTimeout(async () => {
|
|
639
669
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
670
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
await options.prepareReconnect?.({
|
|
675
|
+
attempt: state.reconnectAttempts,
|
|
676
|
+
path,
|
|
677
|
+
scenarioId: state.scenarioId,
|
|
678
|
+
sessionId: state.sessionId
|
|
647
679
|
});
|
|
680
|
+
} catch {
|
|
681
|
+
if (state.reconnectAttempts < maxReconnectAttempts) {
|
|
682
|
+
scheduleReconnect();
|
|
683
|
+
} else {
|
|
684
|
+
emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
|
|
685
|
+
}
|
|
648
686
|
return;
|
|
649
687
|
}
|
|
650
688
|
connect();
|
|
@@ -701,6 +739,18 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
701
739
|
state.sessionId = parsed.sessionId;
|
|
702
740
|
state.scenarioId = parsed.scenarioId ?? state.scenarioId;
|
|
703
741
|
}
|
|
742
|
+
if (parsed.type === "call_control_ack") {
|
|
743
|
+
const pending = pendingCallControls.get(parsed.requestId);
|
|
744
|
+
if (pending) {
|
|
745
|
+
clearTimeout(pending.timeout);
|
|
746
|
+
pendingCallControls.delete(parsed.requestId);
|
|
747
|
+
if (parsed.ok) {
|
|
748
|
+
pending.resolve();
|
|
749
|
+
} else {
|
|
750
|
+
pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
704
754
|
listeners.forEach((listener) => listener(parsed));
|
|
705
755
|
};
|
|
706
756
|
ws.onclose = (event) => {
|
|
@@ -710,18 +760,30 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
710
760
|
state.isConnected = false;
|
|
711
761
|
clearTimers();
|
|
712
762
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
|
|
763
|
+
const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
|
|
764
|
+
if (authorizationFailure) {
|
|
765
|
+
listeners.forEach((listener) => listener({
|
|
766
|
+
message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
|
|
767
|
+
recoverable: false,
|
|
768
|
+
type: "error"
|
|
769
|
+
}));
|
|
770
|
+
}
|
|
713
771
|
if (reconnectable) {
|
|
714
772
|
scheduleReconnect();
|
|
715
773
|
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
774
|
+
if (authorizationFailure) {
|
|
775
|
+
emitConnection({
|
|
776
|
+
reconnect: {
|
|
777
|
+
attempts: state.reconnectAttempts,
|
|
778
|
+
lastDisconnectAt: Date.now(),
|
|
779
|
+
maxAttempts: maxReconnectAttempts,
|
|
780
|
+
status: "exhausted"
|
|
781
|
+
},
|
|
782
|
+
type: "connection"
|
|
783
|
+
});
|
|
784
|
+
} else {
|
|
785
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
786
|
+
}
|
|
725
787
|
}
|
|
726
788
|
};
|
|
727
789
|
state.ws = ws;
|
|
@@ -731,6 +793,12 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
731
793
|
state.ws.send(value);
|
|
732
794
|
return;
|
|
733
795
|
}
|
|
796
|
+
if (typeof value !== "string") {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
|
|
800
|
+
state.pendingMessages.shift();
|
|
801
|
+
}
|
|
734
802
|
state.pendingMessages.push(value);
|
|
735
803
|
};
|
|
736
804
|
const send = (message) => {
|
|
@@ -756,13 +824,23 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
756
824
|
send({ type: "end_turn" });
|
|
757
825
|
};
|
|
758
826
|
const callControl = (message) => {
|
|
827
|
+
const requestId = crypto.randomUUID();
|
|
828
|
+
const { promise, reject, resolve } = Promise.withResolvers();
|
|
829
|
+
const timeout = setTimeout(() => {
|
|
830
|
+
pendingCallControls.delete(requestId);
|
|
831
|
+
reject(new Error(`${message.action} acknowledgement timed out`));
|
|
832
|
+
}, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
|
|
833
|
+
pendingCallControls.set(requestId, { reject, resolve, timeout });
|
|
759
834
|
send({
|
|
760
835
|
...message,
|
|
836
|
+
requestId,
|
|
761
837
|
type: "call_control"
|
|
762
838
|
});
|
|
839
|
+
return promise;
|
|
763
840
|
};
|
|
764
841
|
const close = (reason = "client-close") => {
|
|
765
842
|
clearTimers();
|
|
843
|
+
rejectPendingCallControls("Voice connection closed");
|
|
766
844
|
if (state.ws) {
|
|
767
845
|
const ws = state.ws;
|
|
768
846
|
state.ws = null;
|
|
@@ -776,6 +854,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
776
854
|
};
|
|
777
855
|
const disconnect = () => {
|
|
778
856
|
clearTimers();
|
|
857
|
+
rejectPendingCallControls("Voice connection disconnected");
|
|
779
858
|
if (state.ws) {
|
|
780
859
|
const ws = state.ws;
|
|
781
860
|
state.ws = null;
|
|
@@ -938,7 +1017,8 @@ var createVoiceStreamStore = () => {
|
|
|
938
1017
|
case "error":
|
|
939
1018
|
state = {
|
|
940
1019
|
...state,
|
|
941
|
-
error: action.message
|
|
1020
|
+
error: action.message,
|
|
1021
|
+
...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
|
|
942
1022
|
};
|
|
943
1023
|
break;
|
|
944
1024
|
case "final":
|
|
@@ -1078,7 +1158,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1078
1158
|
return store.getSnapshot().call;
|
|
1079
1159
|
},
|
|
1080
1160
|
callControl(message) {
|
|
1081
|
-
connection.callControl(message);
|
|
1161
|
+
return connection.callControl(message);
|
|
1082
1162
|
},
|
|
1083
1163
|
close(reason) {
|
|
1084
1164
|
unsubscribeConnection();
|