@absolutejs/voice 0.0.22-beta.652 → 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 +58 -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/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;
|
|
@@ -704,6 +712,13 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
704
712
|
}
|
|
705
713
|
}
|
|
706
714
|
};
|
|
715
|
+
const rejectPendingCallControls = (message) => {
|
|
716
|
+
for (const pending of pendingCallControls.values()) {
|
|
717
|
+
clearTimeout(pending.timeout);
|
|
718
|
+
pending.reject(new Error(message));
|
|
719
|
+
}
|
|
720
|
+
pendingCallControls.clear();
|
|
721
|
+
};
|
|
707
722
|
const scheduleReconnect = () => {
|
|
708
723
|
state.reconnectAttempts += 1;
|
|
709
724
|
const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
|
|
@@ -718,7 +733,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
718
733
|
},
|
|
719
734
|
type: "connection"
|
|
720
735
|
});
|
|
721
|
-
state.reconnectTimeout = setTimeout(() => {
|
|
736
|
+
state.reconnectTimeout = setTimeout(async () => {
|
|
722
737
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
723
738
|
emitConnection({
|
|
724
739
|
reconnect: {
|
|
@@ -730,6 +745,28 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
730
745
|
});
|
|
731
746
|
return;
|
|
732
747
|
}
|
|
748
|
+
try {
|
|
749
|
+
await options.prepareReconnect?.({
|
|
750
|
+
attempt: state.reconnectAttempts,
|
|
751
|
+
path,
|
|
752
|
+
scenarioId: state.scenarioId,
|
|
753
|
+
sessionId: state.sessionId
|
|
754
|
+
});
|
|
755
|
+
} catch {
|
|
756
|
+
if (state.reconnectAttempts < maxReconnectAttempts) {
|
|
757
|
+
scheduleReconnect();
|
|
758
|
+
} else {
|
|
759
|
+
emitConnection({
|
|
760
|
+
reconnect: {
|
|
761
|
+
attempts: state.reconnectAttempts,
|
|
762
|
+
maxAttempts: maxReconnectAttempts,
|
|
763
|
+
status: "exhausted"
|
|
764
|
+
},
|
|
765
|
+
type: "connection"
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
733
770
|
connect();
|
|
734
771
|
}, delayMs);
|
|
735
772
|
};
|
|
@@ -784,6 +821,18 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
784
821
|
state.sessionId = parsed.sessionId;
|
|
785
822
|
state.scenarioId = parsed.scenarioId ?? state.scenarioId;
|
|
786
823
|
}
|
|
824
|
+
if (parsed.type === "call_control_ack") {
|
|
825
|
+
const pending = pendingCallControls.get(parsed.requestId);
|
|
826
|
+
if (pending) {
|
|
827
|
+
clearTimeout(pending.timeout);
|
|
828
|
+
pendingCallControls.delete(parsed.requestId);
|
|
829
|
+
if (parsed.ok) {
|
|
830
|
+
pending.resolve();
|
|
831
|
+
} else {
|
|
832
|
+
pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
787
836
|
listeners.forEach((listener) => listener(parsed));
|
|
788
837
|
};
|
|
789
838
|
ws.onclose = (event) => {
|
|
@@ -793,6 +842,14 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
793
842
|
state.isConnected = false;
|
|
794
843
|
clearTimers();
|
|
795
844
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
|
|
845
|
+
const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
|
|
846
|
+
if (authorizationFailure) {
|
|
847
|
+
listeners.forEach((listener) => listener({
|
|
848
|
+
message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
|
|
849
|
+
recoverable: false,
|
|
850
|
+
type: "error"
|
|
851
|
+
}));
|
|
852
|
+
}
|
|
796
853
|
if (reconnectable) {
|
|
797
854
|
scheduleReconnect();
|
|
798
855
|
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
@@ -814,6 +871,12 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
814
871
|
state.ws.send(value);
|
|
815
872
|
return;
|
|
816
873
|
}
|
|
874
|
+
if (typeof value !== "string") {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
|
|
878
|
+
state.pendingMessages.shift();
|
|
879
|
+
}
|
|
817
880
|
state.pendingMessages.push(value);
|
|
818
881
|
};
|
|
819
882
|
const send = (message) => {
|
|
@@ -839,13 +902,23 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
839
902
|
send({ type: "end_turn" });
|
|
840
903
|
};
|
|
841
904
|
const callControl = (message) => {
|
|
905
|
+
const requestId = crypto.randomUUID();
|
|
906
|
+
const { promise, reject, resolve } = Promise.withResolvers();
|
|
907
|
+
const timeout = setTimeout(() => {
|
|
908
|
+
pendingCallControls.delete(requestId);
|
|
909
|
+
reject(new Error(`${message.action} acknowledgement timed out`));
|
|
910
|
+
}, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
|
|
911
|
+
pendingCallControls.set(requestId, { reject, resolve, timeout });
|
|
842
912
|
send({
|
|
843
913
|
...message,
|
|
914
|
+
requestId,
|
|
844
915
|
type: "call_control"
|
|
845
916
|
});
|
|
917
|
+
return promise;
|
|
846
918
|
};
|
|
847
919
|
const close = (reason = "client-close") => {
|
|
848
920
|
clearTimers();
|
|
921
|
+
rejectPendingCallControls("Voice connection closed");
|
|
849
922
|
if (state.ws) {
|
|
850
923
|
const ws = state.ws;
|
|
851
924
|
state.ws = null;
|
|
@@ -859,6 +932,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
859
932
|
};
|
|
860
933
|
const disconnect = () => {
|
|
861
934
|
clearTimers();
|
|
935
|
+
rejectPendingCallControls("Voice connection disconnected");
|
|
862
936
|
if (state.ws) {
|
|
863
937
|
const ws = state.ws;
|
|
864
938
|
state.ws = null;
|
|
@@ -1021,7 +1095,8 @@ var createVoiceStreamStore = () => {
|
|
|
1021
1095
|
case "error":
|
|
1022
1096
|
state = {
|
|
1023
1097
|
...state,
|
|
1024
|
-
error: action.message
|
|
1098
|
+
error: action.message,
|
|
1099
|
+
...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
|
|
1025
1100
|
};
|
|
1026
1101
|
break;
|
|
1027
1102
|
case "final":
|
|
@@ -1161,7 +1236,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1161
1236
|
return store.getSnapshot().call;
|
|
1162
1237
|
},
|
|
1163
1238
|
callControl(message) {
|
|
1164
|
-
connection.callControl(message);
|
|
1239
|
+
return connection.callControl(message);
|
|
1165
1240
|
},
|
|
1166
1241
|
close(reason) {
|
|
1167
1242
|
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;
|
|
@@ -621,6 +629,13 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
621
629
|
}
|
|
622
630
|
}
|
|
623
631
|
};
|
|
632
|
+
const rejectPendingCallControls = (message) => {
|
|
633
|
+
for (const pending of pendingCallControls.values()) {
|
|
634
|
+
clearTimeout(pending.timeout);
|
|
635
|
+
pending.reject(new Error(message));
|
|
636
|
+
}
|
|
637
|
+
pendingCallControls.clear();
|
|
638
|
+
};
|
|
624
639
|
const scheduleReconnect = () => {
|
|
625
640
|
state.reconnectAttempts += 1;
|
|
626
641
|
const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
|
|
@@ -635,7 +650,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
635
650
|
},
|
|
636
651
|
type: "connection"
|
|
637
652
|
});
|
|
638
|
-
state.reconnectTimeout = setTimeout(() => {
|
|
653
|
+
state.reconnectTimeout = setTimeout(async () => {
|
|
639
654
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
640
655
|
emitConnection({
|
|
641
656
|
reconnect: {
|
|
@@ -647,6 +662,28 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
647
662
|
});
|
|
648
663
|
return;
|
|
649
664
|
}
|
|
665
|
+
try {
|
|
666
|
+
await options.prepareReconnect?.({
|
|
667
|
+
attempt: state.reconnectAttempts,
|
|
668
|
+
path,
|
|
669
|
+
scenarioId: state.scenarioId,
|
|
670
|
+
sessionId: state.sessionId
|
|
671
|
+
});
|
|
672
|
+
} catch {
|
|
673
|
+
if (state.reconnectAttempts < maxReconnectAttempts) {
|
|
674
|
+
scheduleReconnect();
|
|
675
|
+
} else {
|
|
676
|
+
emitConnection({
|
|
677
|
+
reconnect: {
|
|
678
|
+
attempts: state.reconnectAttempts,
|
|
679
|
+
maxAttempts: maxReconnectAttempts,
|
|
680
|
+
status: "exhausted"
|
|
681
|
+
},
|
|
682
|
+
type: "connection"
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
650
687
|
connect();
|
|
651
688
|
}, delayMs);
|
|
652
689
|
};
|
|
@@ -701,6 +738,18 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
701
738
|
state.sessionId = parsed.sessionId;
|
|
702
739
|
state.scenarioId = parsed.scenarioId ?? state.scenarioId;
|
|
703
740
|
}
|
|
741
|
+
if (parsed.type === "call_control_ack") {
|
|
742
|
+
const pending = pendingCallControls.get(parsed.requestId);
|
|
743
|
+
if (pending) {
|
|
744
|
+
clearTimeout(pending.timeout);
|
|
745
|
+
pendingCallControls.delete(parsed.requestId);
|
|
746
|
+
if (parsed.ok) {
|
|
747
|
+
pending.resolve();
|
|
748
|
+
} else {
|
|
749
|
+
pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
704
753
|
listeners.forEach((listener) => listener(parsed));
|
|
705
754
|
};
|
|
706
755
|
ws.onclose = (event) => {
|
|
@@ -710,6 +759,14 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
710
759
|
state.isConnected = false;
|
|
711
760
|
clearTimers();
|
|
712
761
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
|
|
762
|
+
const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
|
|
763
|
+
if (authorizationFailure) {
|
|
764
|
+
listeners.forEach((listener) => listener({
|
|
765
|
+
message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
|
|
766
|
+
recoverable: false,
|
|
767
|
+
type: "error"
|
|
768
|
+
}));
|
|
769
|
+
}
|
|
713
770
|
if (reconnectable) {
|
|
714
771
|
scheduleReconnect();
|
|
715
772
|
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
@@ -731,6 +788,12 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
731
788
|
state.ws.send(value);
|
|
732
789
|
return;
|
|
733
790
|
}
|
|
791
|
+
if (typeof value !== "string") {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
|
|
795
|
+
state.pendingMessages.shift();
|
|
796
|
+
}
|
|
734
797
|
state.pendingMessages.push(value);
|
|
735
798
|
};
|
|
736
799
|
const send = (message) => {
|
|
@@ -756,13 +819,23 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
756
819
|
send({ type: "end_turn" });
|
|
757
820
|
};
|
|
758
821
|
const callControl = (message) => {
|
|
822
|
+
const requestId = crypto.randomUUID();
|
|
823
|
+
const { promise, reject, resolve } = Promise.withResolvers();
|
|
824
|
+
const timeout = setTimeout(() => {
|
|
825
|
+
pendingCallControls.delete(requestId);
|
|
826
|
+
reject(new Error(`${message.action} acknowledgement timed out`));
|
|
827
|
+
}, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
|
|
828
|
+
pendingCallControls.set(requestId, { reject, resolve, timeout });
|
|
759
829
|
send({
|
|
760
830
|
...message,
|
|
831
|
+
requestId,
|
|
761
832
|
type: "call_control"
|
|
762
833
|
});
|
|
834
|
+
return promise;
|
|
763
835
|
};
|
|
764
836
|
const close = (reason = "client-close") => {
|
|
765
837
|
clearTimers();
|
|
838
|
+
rejectPendingCallControls("Voice connection closed");
|
|
766
839
|
if (state.ws) {
|
|
767
840
|
const ws = state.ws;
|
|
768
841
|
state.ws = null;
|
|
@@ -776,6 +849,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
776
849
|
};
|
|
777
850
|
const disconnect = () => {
|
|
778
851
|
clearTimers();
|
|
852
|
+
rejectPendingCallControls("Voice connection disconnected");
|
|
779
853
|
if (state.ws) {
|
|
780
854
|
const ws = state.ws;
|
|
781
855
|
state.ws = null;
|
|
@@ -938,7 +1012,8 @@ var createVoiceStreamStore = () => {
|
|
|
938
1012
|
case "error":
|
|
939
1013
|
state = {
|
|
940
1014
|
...state,
|
|
941
|
-
error: action.message
|
|
1015
|
+
error: action.message,
|
|
1016
|
+
...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
|
|
942
1017
|
};
|
|
943
1018
|
break;
|
|
944
1019
|
case "final":
|
|
@@ -1078,7 +1153,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1078
1153
|
return store.getSnapshot().call;
|
|
1079
1154
|
},
|
|
1080
1155
|
callControl(message) {
|
|
1081
|
-
connection.callControl(message);
|
|
1156
|
+
return connection.callControl(message);
|
|
1082
1157
|
},
|
|
1083
1158
|
close(reason) {
|
|
1084
1159
|
unsubscribeConnection();
|
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;
|
|
@@ -229,6 +234,13 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
229
234
|
}
|
|
230
235
|
}
|
|
231
236
|
};
|
|
237
|
+
const rejectPendingCallControls = (message) => {
|
|
238
|
+
for (const pending of pendingCallControls.values()) {
|
|
239
|
+
clearTimeout(pending.timeout);
|
|
240
|
+
pending.reject(new Error(message));
|
|
241
|
+
}
|
|
242
|
+
pendingCallControls.clear();
|
|
243
|
+
};
|
|
232
244
|
const scheduleReconnect = () => {
|
|
233
245
|
state.reconnectAttempts += 1;
|
|
234
246
|
const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
|
|
@@ -243,7 +255,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
243
255
|
},
|
|
244
256
|
type: "connection"
|
|
245
257
|
});
|
|
246
|
-
state.reconnectTimeout = setTimeout(() => {
|
|
258
|
+
state.reconnectTimeout = setTimeout(async () => {
|
|
247
259
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
248
260
|
emitConnection({
|
|
249
261
|
reconnect: {
|
|
@@ -255,6 +267,28 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
255
267
|
});
|
|
256
268
|
return;
|
|
257
269
|
}
|
|
270
|
+
try {
|
|
271
|
+
await options.prepareReconnect?.({
|
|
272
|
+
attempt: state.reconnectAttempts,
|
|
273
|
+
path,
|
|
274
|
+
scenarioId: state.scenarioId,
|
|
275
|
+
sessionId: state.sessionId
|
|
276
|
+
});
|
|
277
|
+
} catch {
|
|
278
|
+
if (state.reconnectAttempts < maxReconnectAttempts) {
|
|
279
|
+
scheduleReconnect();
|
|
280
|
+
} else {
|
|
281
|
+
emitConnection({
|
|
282
|
+
reconnect: {
|
|
283
|
+
attempts: state.reconnectAttempts,
|
|
284
|
+
maxAttempts: maxReconnectAttempts,
|
|
285
|
+
status: "exhausted"
|
|
286
|
+
},
|
|
287
|
+
type: "connection"
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
258
292
|
connect();
|
|
259
293
|
}, delayMs);
|
|
260
294
|
};
|
|
@@ -309,6 +343,18 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
309
343
|
state.sessionId = parsed.sessionId;
|
|
310
344
|
state.scenarioId = parsed.scenarioId ?? state.scenarioId;
|
|
311
345
|
}
|
|
346
|
+
if (parsed.type === "call_control_ack") {
|
|
347
|
+
const pending = pendingCallControls.get(parsed.requestId);
|
|
348
|
+
if (pending) {
|
|
349
|
+
clearTimeout(pending.timeout);
|
|
350
|
+
pendingCallControls.delete(parsed.requestId);
|
|
351
|
+
if (parsed.ok) {
|
|
352
|
+
pending.resolve();
|
|
353
|
+
} else {
|
|
354
|
+
pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
312
358
|
listeners.forEach((listener) => listener(parsed));
|
|
313
359
|
};
|
|
314
360
|
ws.onclose = (event) => {
|
|
@@ -318,6 +364,14 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
318
364
|
state.isConnected = false;
|
|
319
365
|
clearTimers();
|
|
320
366
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
|
|
367
|
+
const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
|
|
368
|
+
if (authorizationFailure) {
|
|
369
|
+
listeners.forEach((listener) => listener({
|
|
370
|
+
message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
|
|
371
|
+
recoverable: false,
|
|
372
|
+
type: "error"
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
321
375
|
if (reconnectable) {
|
|
322
376
|
scheduleReconnect();
|
|
323
377
|
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
@@ -339,6 +393,12 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
339
393
|
state.ws.send(value);
|
|
340
394
|
return;
|
|
341
395
|
}
|
|
396
|
+
if (typeof value !== "string") {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
|
|
400
|
+
state.pendingMessages.shift();
|
|
401
|
+
}
|
|
342
402
|
state.pendingMessages.push(value);
|
|
343
403
|
};
|
|
344
404
|
const send = (message) => {
|
|
@@ -364,13 +424,23 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
364
424
|
send({ type: "end_turn" });
|
|
365
425
|
};
|
|
366
426
|
const callControl = (message) => {
|
|
427
|
+
const requestId = crypto.randomUUID();
|
|
428
|
+
const { promise, reject, resolve } = Promise.withResolvers();
|
|
429
|
+
const timeout = setTimeout(() => {
|
|
430
|
+
pendingCallControls.delete(requestId);
|
|
431
|
+
reject(new Error(`${message.action} acknowledgement timed out`));
|
|
432
|
+
}, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
|
|
433
|
+
pendingCallControls.set(requestId, { reject, resolve, timeout });
|
|
367
434
|
send({
|
|
368
435
|
...message,
|
|
436
|
+
requestId,
|
|
369
437
|
type: "call_control"
|
|
370
438
|
});
|
|
439
|
+
return promise;
|
|
371
440
|
};
|
|
372
441
|
const close = (reason = "client-close") => {
|
|
373
442
|
clearTimers();
|
|
443
|
+
rejectPendingCallControls("Voice connection closed");
|
|
374
444
|
if (state.ws) {
|
|
375
445
|
const ws = state.ws;
|
|
376
446
|
state.ws = null;
|
|
@@ -384,6 +454,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
384
454
|
};
|
|
385
455
|
const disconnect = () => {
|
|
386
456
|
clearTimers();
|
|
457
|
+
rejectPendingCallControls("Voice connection disconnected");
|
|
387
458
|
if (state.ws) {
|
|
388
459
|
const ws = state.ws;
|
|
389
460
|
state.ws = null;
|
|
@@ -1219,8 +1290,11 @@ var serverMessageToAction = (message) => {
|
|
|
1219
1290
|
case "error":
|
|
1220
1291
|
return {
|
|
1221
1292
|
message: normalizeErrorMessage(message.message),
|
|
1293
|
+
recoverable: message.recoverable,
|
|
1222
1294
|
type: "error"
|
|
1223
1295
|
};
|
|
1296
|
+
case "call_control_ack":
|
|
1297
|
+
return null;
|
|
1224
1298
|
case "final":
|
|
1225
1299
|
return {
|
|
1226
1300
|
transcript: message.transcript,
|
|
@@ -1465,7 +1539,8 @@ var createVoiceStreamStore = () => {
|
|
|
1465
1539
|
case "error":
|
|
1466
1540
|
state = {
|
|
1467
1541
|
...state,
|
|
1468
|
-
error: action.message
|
|
1542
|
+
error: action.message,
|
|
1543
|
+
...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
|
|
1469
1544
|
};
|
|
1470
1545
|
break;
|
|
1471
1546
|
case "final":
|
|
@@ -1605,7 +1680,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1605
1680
|
return store.getSnapshot().call;
|
|
1606
1681
|
},
|
|
1607
1682
|
callControl(message) {
|
|
1608
|
-
connection.callControl(message);
|
|
1683
|
+
return connection.callControl(message);
|
|
1609
1684
|
},
|
|
1610
1685
|
close(reason) {
|
|
1611
1686
|
unsubscribeConnection();
|