@absolutejs/voice 0.0.22-beta.655 → 0.0.22-beta.657
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 +204 -70
- package/dist/client/connection.d.ts +3 -0
- package/dist/client/htmxBootstrap.js +204 -70
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +206 -71
- package/dist/core/types.d.ts +4 -0
- package/dist/embed/index.js +204 -70
- package/dist/embed/voice-widget.js +8 -8
- package/dist/react/index.js +204 -70
- package/dist/svelte/index.js +204 -70
- package/dist/testing/index.js +205 -71
- package/dist/vue/index.js +204 -70
- package/package.json +1 -1
package/dist/svelte/index.js
CHANGED
|
@@ -517,48 +517,89 @@ var createMicrophoneCapture = (options) => {
|
|
|
517
517
|
let sourceNode = null;
|
|
518
518
|
let processorNode = null;
|
|
519
519
|
let mediaStream = null;
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
520
|
+
let lifecycleGeneration = 0;
|
|
521
|
+
let startPromise = null;
|
|
522
|
+
const release = (resources) => {
|
|
523
|
+
resources.processorNode?.disconnect();
|
|
524
|
+
resources.sourceNode?.disconnect();
|
|
525
|
+
resources.mediaStream?.getTracks().forEach((track) => track.stop());
|
|
526
|
+
resources.audioContext?.close();
|
|
527
|
+
};
|
|
528
|
+
const start = () => {
|
|
529
|
+
if (mediaStream)
|
|
530
|
+
return Promise.resolve();
|
|
531
|
+
if (startPromise)
|
|
532
|
+
return startPromise;
|
|
533
|
+
const generation = ++lifecycleGeneration;
|
|
534
|
+
const operation = async () => {
|
|
535
|
+
if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
|
|
536
|
+
throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
|
|
537
|
+
}
|
|
538
|
+
const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
|
|
539
|
+
if (!AudioContextCtor) {
|
|
540
|
+
throw new Error("Browser microphone capture requires AudioContext support.");
|
|
541
|
+
}
|
|
542
|
+
let pendingAudioContext = null;
|
|
543
|
+
let pendingMediaStream = null;
|
|
544
|
+
let pendingProcessorNode = null;
|
|
545
|
+
let pendingSourceNode = null;
|
|
546
|
+
try {
|
|
547
|
+
pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
|
|
548
|
+
audio: {
|
|
549
|
+
autoGainControl: true,
|
|
550
|
+
channelCount: options.channelCount ?? 1,
|
|
551
|
+
echoCancellation: true,
|
|
552
|
+
noiseSuppression: true
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
if (generation !== lifecycleGeneration) {
|
|
556
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
535
557
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
558
|
+
pendingAudioContext = new AudioContextCtor;
|
|
559
|
+
if (pendingAudioContext.state === "suspended") {
|
|
560
|
+
await pendingAudioContext.resume();
|
|
561
|
+
}
|
|
562
|
+
if (generation !== lifecycleGeneration) {
|
|
563
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
564
|
+
}
|
|
565
|
+
pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
|
|
566
|
+
pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
|
|
567
|
+
pendingProcessorNode.onaudioprocess = (event) => {
|
|
568
|
+
const channel = event.inputBuffer.getChannelData(0);
|
|
569
|
+
const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
|
|
570
|
+
const pcm = floatTo16BitPCM(downsampled);
|
|
571
|
+
options.onLevel?.(getPcmLevel(pcm));
|
|
572
|
+
options.onAudio(pcm);
|
|
573
|
+
};
|
|
574
|
+
pendingSourceNode.connect(pendingProcessorNode);
|
|
575
|
+
pendingProcessorNode.connect(pendingAudioContext.destination);
|
|
576
|
+
if (generation !== lifecycleGeneration) {
|
|
577
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
578
|
+
}
|
|
579
|
+
audioContext = pendingAudioContext;
|
|
580
|
+
mediaStream = pendingMediaStream;
|
|
581
|
+
processorNode = pendingProcessorNode;
|
|
582
|
+
sourceNode = pendingSourceNode;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
release({
|
|
585
|
+
audioContext: pendingAudioContext,
|
|
586
|
+
mediaStream: pendingMediaStream,
|
|
587
|
+
processorNode: pendingProcessorNode,
|
|
588
|
+
sourceNode: pendingSourceNode
|
|
589
|
+
});
|
|
590
|
+
throw error;
|
|
540
591
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
options.onAudio(pcm);
|
|
549
|
-
};
|
|
550
|
-
sourceNode.connect(processorNode);
|
|
551
|
-
processorNode.connect(audioContext.destination);
|
|
552
|
-
} catch (error) {
|
|
553
|
-
stop();
|
|
554
|
-
throw error;
|
|
555
|
-
}
|
|
592
|
+
};
|
|
593
|
+
const pending = operation().finally(() => {
|
|
594
|
+
if (startPromise === pending)
|
|
595
|
+
startPromise = null;
|
|
596
|
+
});
|
|
597
|
+
startPromise = pending;
|
|
598
|
+
return pending;
|
|
556
599
|
};
|
|
557
600
|
const stop = () => {
|
|
558
|
-
|
|
559
|
-
sourceNode
|
|
560
|
-
mediaStream?.getTracks().forEach((track) => track.stop());
|
|
561
|
-
audioContext?.close();
|
|
601
|
+
lifecycleGeneration += 1;
|
|
602
|
+
release({ audioContext, mediaStream, processorNode, sourceNode });
|
|
562
603
|
options.onLevel?.(0);
|
|
563
604
|
audioContext = null;
|
|
564
605
|
mediaStream = null;
|
|
@@ -770,11 +811,19 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
|
770
811
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
771
812
|
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
772
813
|
var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
|
|
814
|
+
var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
|
|
773
815
|
var MAX_PENDING_CONTROL_MESSAGES = 100;
|
|
774
816
|
var RECONNECT_BASE_DELAY_MS = 500;
|
|
775
817
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
776
818
|
var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
|
|
777
819
|
var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
|
|
820
|
+
|
|
821
|
+
class VoiceReconnectRejectedError extends Error {
|
|
822
|
+
constructor(message = "Voice authorization could not be renewed.") {
|
|
823
|
+
super(message);
|
|
824
|
+
this.name = "VoiceReconnectRejectedError";
|
|
825
|
+
}
|
|
826
|
+
}
|
|
778
827
|
var noop = () => {};
|
|
779
828
|
var noopAsync = () => Promise.resolve();
|
|
780
829
|
var noopUnsubscribe = () => noop;
|
|
@@ -854,6 +903,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
854
903
|
const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
855
904
|
const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
|
|
856
905
|
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
906
|
+
let lifecycleGeneration = 0;
|
|
907
|
+
let disposed = false;
|
|
908
|
+
let reconnectPreparationAbort = null;
|
|
857
909
|
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
858
910
|
const state = {
|
|
859
911
|
isConnected: false,
|
|
@@ -917,6 +969,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
917
969
|
pendingCallControls.clear();
|
|
918
970
|
};
|
|
919
971
|
const scheduleReconnect = () => {
|
|
972
|
+
if (disposed)
|
|
973
|
+
return;
|
|
974
|
+
const scheduledGeneration = lifecycleGeneration;
|
|
920
975
|
state.reconnectAttempts += 1;
|
|
921
976
|
const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
|
|
922
977
|
const nextAttemptAt = Date.now() + delayMs;
|
|
@@ -931,18 +986,44 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
931
986
|
type: "connection"
|
|
932
987
|
});
|
|
933
988
|
state.reconnectTimeout = setTimeout(async () => {
|
|
989
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
990
|
+
return;
|
|
934
991
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
935
992
|
emitTerminalFailure("Voice connection could not be restored.");
|
|
936
993
|
return;
|
|
937
994
|
}
|
|
938
995
|
try {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
996
|
+
if (options.prepareReconnect) {
|
|
997
|
+
const preparationAbort = new AbortController;
|
|
998
|
+
reconnectPreparationAbort = preparationAbort;
|
|
999
|
+
const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
|
|
1000
|
+
try {
|
|
1001
|
+
await Promise.race([
|
|
1002
|
+
options.prepareReconnect({
|
|
1003
|
+
attempt: state.reconnectAttempts,
|
|
1004
|
+
path,
|
|
1005
|
+
scenarioId: state.scenarioId,
|
|
1006
|
+
sessionId: state.sessionId,
|
|
1007
|
+
signal: preparationAbort.signal
|
|
1008
|
+
}),
|
|
1009
|
+
new Promise((_, reject) => {
|
|
1010
|
+
preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
|
|
1011
|
+
})
|
|
1012
|
+
]);
|
|
1013
|
+
} finally {
|
|
1014
|
+
clearTimeout(timeout);
|
|
1015
|
+
if (reconnectPreparationAbort === preparationAbort) {
|
|
1016
|
+
reconnectPreparationAbort = null;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
1022
|
+
return;
|
|
1023
|
+
if (error instanceof VoiceReconnectRejectedError) {
|
|
1024
|
+
emitTerminalFailure(error.message);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
946
1027
|
if (state.reconnectAttempts < maxReconnectAttempts) {
|
|
947
1028
|
scheduleReconnect();
|
|
948
1029
|
} else {
|
|
@@ -950,10 +1031,14 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
950
1031
|
}
|
|
951
1032
|
return;
|
|
952
1033
|
}
|
|
1034
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
1035
|
+
return;
|
|
953
1036
|
connect();
|
|
954
1037
|
}, delayMs);
|
|
955
1038
|
};
|
|
956
1039
|
const connect = () => {
|
|
1040
|
+
if (disposed)
|
|
1041
|
+
return;
|
|
957
1042
|
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
|
|
958
1043
|
ws.binaryType = "arraybuffer";
|
|
959
1044
|
ws.onopen = () => {
|
|
@@ -1024,6 +1109,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
1024
1109
|
state.ws = null;
|
|
1025
1110
|
state.isConnected = false;
|
|
1026
1111
|
clearTimers();
|
|
1112
|
+
rejectPendingCallControls("Voice connection interrupted");
|
|
1027
1113
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
|
|
1028
1114
|
const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
|
|
1029
1115
|
if (authorizationFailure) {
|
|
@@ -1054,6 +1140,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
1054
1140
|
state.ws = ws;
|
|
1055
1141
|
};
|
|
1056
1142
|
const sendSerialized = (value) => {
|
|
1143
|
+
if (disposed)
|
|
1144
|
+
return;
|
|
1057
1145
|
if (state.ws?.readyState === WS_OPEN) {
|
|
1058
1146
|
state.ws.send(value);
|
|
1059
1147
|
return;
|
|
@@ -1089,6 +1177,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
1089
1177
|
send({ type: "end_turn" });
|
|
1090
1178
|
};
|
|
1091
1179
|
const callControl = (message) => {
|
|
1180
|
+
if (state.ws?.readyState !== WS_OPEN) {
|
|
1181
|
+
return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
|
|
1182
|
+
}
|
|
1092
1183
|
const requestId = crypto.randomUUID();
|
|
1093
1184
|
const { promise, reject, resolve } = Promise.withResolvers();
|
|
1094
1185
|
const timeout = setTimeout(() => {
|
|
@@ -1096,16 +1187,21 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
1096
1187
|
reject(new Error(`${message.action} acknowledgement timed out`));
|
|
1097
1188
|
}, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
|
|
1098
1189
|
pendingCallControls.set(requestId, { reject, resolve, timeout });
|
|
1099
|
-
send({
|
|
1190
|
+
state.ws.send(JSON.stringify({
|
|
1100
1191
|
...message,
|
|
1101
1192
|
requestId,
|
|
1102
1193
|
type: "call_control"
|
|
1103
|
-
});
|
|
1194
|
+
}));
|
|
1104
1195
|
return promise;
|
|
1105
1196
|
};
|
|
1106
1197
|
const close = (reason = "client-close") => {
|
|
1198
|
+
disposed = true;
|
|
1199
|
+
reconnectPreparationAbort?.abort();
|
|
1200
|
+
reconnectPreparationAbort = null;
|
|
1201
|
+
lifecycleGeneration += 1;
|
|
1107
1202
|
clearTimers();
|
|
1108
1203
|
rejectPendingCallControls("Voice connection closed");
|
|
1204
|
+
state.pendingMessages.length = 0;
|
|
1109
1205
|
if (state.ws) {
|
|
1110
1206
|
const ws = state.ws;
|
|
1111
1207
|
state.ws = null;
|
|
@@ -1118,8 +1214,13 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
1118
1214
|
listeners.clear();
|
|
1119
1215
|
};
|
|
1120
1216
|
const disconnect = () => {
|
|
1217
|
+
disposed = true;
|
|
1218
|
+
reconnectPreparationAbort?.abort();
|
|
1219
|
+
reconnectPreparationAbort = null;
|
|
1220
|
+
lifecycleGeneration += 1;
|
|
1121
1221
|
clearTimers();
|
|
1122
1222
|
rejectPendingCallControls("Voice connection disconnected");
|
|
1223
|
+
state.pendingMessages.length = 0;
|
|
1123
1224
|
if (state.ws) {
|
|
1124
1225
|
const ws = state.ws;
|
|
1125
1226
|
state.ws = null;
|
|
@@ -1367,7 +1468,10 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1367
1468
|
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
1368
1469
|
}) : null;
|
|
1369
1470
|
const subscribers = new Set;
|
|
1471
|
+
let disposed = false;
|
|
1370
1472
|
const start = (input) => Promise.resolve().then(() => {
|
|
1473
|
+
if (disposed)
|
|
1474
|
+
return;
|
|
1371
1475
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
1372
1476
|
return;
|
|
1373
1477
|
}
|
|
@@ -1426,6 +1530,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1426
1530
|
return connection.callControl(message);
|
|
1427
1531
|
},
|
|
1428
1532
|
close(reason) {
|
|
1533
|
+
disposed = true;
|
|
1429
1534
|
unsubscribeConnection();
|
|
1430
1535
|
browserMediaReporter?.close();
|
|
1431
1536
|
connection.close(reason);
|
|
@@ -1433,6 +1538,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1433
1538
|
notify();
|
|
1434
1539
|
},
|
|
1435
1540
|
disconnect() {
|
|
1541
|
+
disposed = true;
|
|
1436
1542
|
unsubscribeConnection();
|
|
1437
1543
|
browserMediaReporter?.close();
|
|
1438
1544
|
connection.disconnect();
|
|
@@ -1836,6 +1942,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1836
1942
|
...options.connection
|
|
1837
1943
|
});
|
|
1838
1944
|
let capture = null;
|
|
1945
|
+
let captureGeneration = 0;
|
|
1946
|
+
let disposed = false;
|
|
1947
|
+
let startRecordingPromise = null;
|
|
1839
1948
|
let state = createInitialState2(stream);
|
|
1840
1949
|
const subscribers = new Set;
|
|
1841
1950
|
const notify = () => {
|
|
@@ -1892,6 +2001,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1892
2001
|
return capture;
|
|
1893
2002
|
};
|
|
1894
2003
|
const stopRecording = () => {
|
|
2004
|
+
captureGeneration += 1;
|
|
1895
2005
|
capture?.stop();
|
|
1896
2006
|
capture = null;
|
|
1897
2007
|
state = {
|
|
@@ -1901,39 +2011,63 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1901
2011
|
notify();
|
|
1902
2012
|
};
|
|
1903
2013
|
const startRecording = async () => {
|
|
2014
|
+
if (disposed) {
|
|
2015
|
+
throw new Error("Voice controller is closed.");
|
|
2016
|
+
}
|
|
1904
2017
|
if (state.isRecording) {
|
|
1905
2018
|
return;
|
|
1906
2019
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2020
|
+
if (startRecordingPromise)
|
|
2021
|
+
return startRecordingPromise;
|
|
2022
|
+
const generation = ++captureGeneration;
|
|
2023
|
+
const activeCapture = ensureCapture();
|
|
2024
|
+
const operation = (async () => {
|
|
2025
|
+
try {
|
|
2026
|
+
state = {
|
|
2027
|
+
...state,
|
|
2028
|
+
recordingError: null
|
|
2029
|
+
};
|
|
2030
|
+
notify();
|
|
2031
|
+
await activeCapture.start();
|
|
2032
|
+
if (disposed || generation !== captureGeneration || capture !== activeCapture) {
|
|
2033
|
+
activeCapture.stop();
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
state = {
|
|
2037
|
+
...state,
|
|
2038
|
+
isRecording: true
|
|
2039
|
+
};
|
|
2040
|
+
notify();
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
activeCapture.stop();
|
|
2043
|
+
if (generation !== captureGeneration || capture !== activeCapture) {
|
|
2044
|
+
throw error;
|
|
2045
|
+
}
|
|
2046
|
+
capture = null;
|
|
2047
|
+
state = {
|
|
2048
|
+
...state,
|
|
2049
|
+
isRecording: false,
|
|
2050
|
+
recordingError: error instanceof Error ? error.message : String(error)
|
|
2051
|
+
};
|
|
2052
|
+
notify();
|
|
2053
|
+
throw error;
|
|
2054
|
+
}
|
|
2055
|
+
})();
|
|
2056
|
+
const pending = operation.finally(() => {
|
|
2057
|
+
if (startRecordingPromise === pending)
|
|
2058
|
+
startRecordingPromise = null;
|
|
2059
|
+
});
|
|
2060
|
+
startRecordingPromise = pending;
|
|
2061
|
+
return startRecordingPromise;
|
|
1930
2062
|
};
|
|
1931
2063
|
const close = (reason) => {
|
|
2064
|
+
disposed = true;
|
|
1932
2065
|
unsubscribeStream();
|
|
1933
2066
|
stopRecording();
|
|
1934
2067
|
stream.close(reason);
|
|
1935
2068
|
};
|
|
1936
2069
|
const disconnect = () => {
|
|
2070
|
+
disposed = true;
|
|
1937
2071
|
unsubscribeStream();
|
|
1938
2072
|
stopRecording();
|
|
1939
2073
|
stream.disconnect();
|