@absolutejs/voice 0.0.22-beta.654 → 0.0.22-beta.656
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 +67 -28
- package/dist/client/connection.d.ts +3 -0
- package/dist/client/htmxBootstrap.js +67 -28
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +69 -29
- package/dist/embed/index.js +67 -28
- package/dist/embed/voice-widget.js +8 -8
- package/dist/react/index.js +67 -28
- package/dist/svelte/index.js +67 -28
- package/dist/testing/index.js +67 -28
- package/dist/vue/index.js +67 -28
- package/package.json +1 -1
package/dist/angular/index.js
CHANGED
|
@@ -593,6 +593,13 @@ var RECONNECT_BASE_DELAY_MS = 500;
|
|
|
593
593
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
594
594
|
var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
|
|
595
595
|
var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
|
|
596
|
+
|
|
597
|
+
class VoiceReconnectRejectedError extends Error {
|
|
598
|
+
constructor(message = "Voice authorization could not be renewed.") {
|
|
599
|
+
super(message);
|
|
600
|
+
this.name = "VoiceReconnectRejectedError";
|
|
601
|
+
}
|
|
602
|
+
}
|
|
596
603
|
var noop = () => {};
|
|
597
604
|
var noopAsync = () => Promise.resolve();
|
|
598
605
|
var noopUnsubscribe = () => noop;
|
|
@@ -672,6 +679,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
672
679
|
const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
673
680
|
const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
|
|
674
681
|
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
682
|
+
let lifecycleGeneration = 0;
|
|
683
|
+
let disposed = false;
|
|
675
684
|
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
676
685
|
const state = {
|
|
677
686
|
isConnected: false,
|
|
@@ -687,6 +696,21 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
687
696
|
const emitConnection = (reconnect) => {
|
|
688
697
|
listeners.forEach((listener) => listener(reconnect));
|
|
689
698
|
};
|
|
699
|
+
const emitTerminalFailure = (message) => {
|
|
700
|
+
listeners.forEach((listener) => listener({
|
|
701
|
+
message,
|
|
702
|
+
recoverable: false,
|
|
703
|
+
type: "error"
|
|
704
|
+
}));
|
|
705
|
+
emitConnection({
|
|
706
|
+
reconnect: {
|
|
707
|
+
attempts: state.reconnectAttempts,
|
|
708
|
+
maxAttempts: maxReconnectAttempts,
|
|
709
|
+
status: "exhausted"
|
|
710
|
+
},
|
|
711
|
+
type: "connection"
|
|
712
|
+
});
|
|
713
|
+
};
|
|
690
714
|
const clearTimers = () => {
|
|
691
715
|
if (state.pingInterval) {
|
|
692
716
|
clearInterval(state.pingInterval);
|
|
@@ -720,6 +744,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
720
744
|
pendingCallControls.clear();
|
|
721
745
|
};
|
|
722
746
|
const scheduleReconnect = () => {
|
|
747
|
+
if (disposed)
|
|
748
|
+
return;
|
|
749
|
+
const scheduledGeneration = lifecycleGeneration;
|
|
723
750
|
state.reconnectAttempts += 1;
|
|
724
751
|
const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
|
|
725
752
|
const nextAttemptAt = Date.now() + delayMs;
|
|
@@ -734,15 +761,10 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
734
761
|
type: "connection"
|
|
735
762
|
});
|
|
736
763
|
state.reconnectTimeout = setTimeout(async () => {
|
|
764
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
765
|
+
return;
|
|
737
766
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
738
|
-
|
|
739
|
-
reconnect: {
|
|
740
|
-
attempts: state.reconnectAttempts,
|
|
741
|
-
maxAttempts: maxReconnectAttempts,
|
|
742
|
-
status: "exhausted"
|
|
743
|
-
},
|
|
744
|
-
type: "connection"
|
|
745
|
-
});
|
|
767
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
746
768
|
return;
|
|
747
769
|
}
|
|
748
770
|
try {
|
|
@@ -752,25 +774,28 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
752
774
|
scenarioId: state.scenarioId,
|
|
753
775
|
sessionId: state.sessionId
|
|
754
776
|
});
|
|
755
|
-
} catch {
|
|
777
|
+
} catch (error) {
|
|
778
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
779
|
+
return;
|
|
780
|
+
if (error instanceof VoiceReconnectRejectedError) {
|
|
781
|
+
emitTerminalFailure(error.message);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
756
784
|
if (state.reconnectAttempts < maxReconnectAttempts) {
|
|
757
785
|
scheduleReconnect();
|
|
758
786
|
} else {
|
|
759
|
-
|
|
760
|
-
reconnect: {
|
|
761
|
-
attempts: state.reconnectAttempts,
|
|
762
|
-
maxAttempts: maxReconnectAttempts,
|
|
763
|
-
status: "exhausted"
|
|
764
|
-
},
|
|
765
|
-
type: "connection"
|
|
766
|
-
});
|
|
787
|
+
emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
|
|
767
788
|
}
|
|
768
789
|
return;
|
|
769
790
|
}
|
|
791
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
792
|
+
return;
|
|
770
793
|
connect();
|
|
771
794
|
}, delayMs);
|
|
772
795
|
};
|
|
773
796
|
const connect = () => {
|
|
797
|
+
if (disposed)
|
|
798
|
+
return;
|
|
774
799
|
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
|
|
775
800
|
ws.binaryType = "arraybuffer";
|
|
776
801
|
ws.onopen = () => {
|
|
@@ -841,6 +866,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
841
866
|
state.ws = null;
|
|
842
867
|
state.isConnected = false;
|
|
843
868
|
clearTimers();
|
|
869
|
+
rejectPendingCallControls("Voice connection interrupted");
|
|
844
870
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
|
|
845
871
|
const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
|
|
846
872
|
if (authorizationFailure) {
|
|
@@ -853,15 +879,19 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
853
879
|
if (reconnectable) {
|
|
854
880
|
scheduleReconnect();
|
|
855
881
|
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
882
|
+
if (authorizationFailure) {
|
|
883
|
+
emitConnection({
|
|
884
|
+
reconnect: {
|
|
885
|
+
attempts: state.reconnectAttempts,
|
|
886
|
+
lastDisconnectAt: Date.now(),
|
|
887
|
+
maxAttempts: maxReconnectAttempts,
|
|
888
|
+
status: "exhausted"
|
|
889
|
+
},
|
|
890
|
+
type: "connection"
|
|
891
|
+
});
|
|
892
|
+
} else {
|
|
893
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
894
|
+
}
|
|
865
895
|
}
|
|
866
896
|
};
|
|
867
897
|
state.ws = ws;
|
|
@@ -902,6 +932,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
902
932
|
send({ type: "end_turn" });
|
|
903
933
|
};
|
|
904
934
|
const callControl = (message) => {
|
|
935
|
+
if (state.ws?.readyState !== WS_OPEN) {
|
|
936
|
+
return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
|
|
937
|
+
}
|
|
905
938
|
const requestId = crypto.randomUUID();
|
|
906
939
|
const { promise, reject, resolve } = Promise.withResolvers();
|
|
907
940
|
const timeout = setTimeout(() => {
|
|
@@ -909,16 +942,19 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
909
942
|
reject(new Error(`${message.action} acknowledgement timed out`));
|
|
910
943
|
}, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
|
|
911
944
|
pendingCallControls.set(requestId, { reject, resolve, timeout });
|
|
912
|
-
send({
|
|
945
|
+
state.ws.send(JSON.stringify({
|
|
913
946
|
...message,
|
|
914
947
|
requestId,
|
|
915
948
|
type: "call_control"
|
|
916
|
-
});
|
|
949
|
+
}));
|
|
917
950
|
return promise;
|
|
918
951
|
};
|
|
919
952
|
const close = (reason = "client-close") => {
|
|
953
|
+
disposed = true;
|
|
954
|
+
lifecycleGeneration += 1;
|
|
920
955
|
clearTimers();
|
|
921
956
|
rejectPendingCallControls("Voice connection closed");
|
|
957
|
+
state.pendingMessages.length = 0;
|
|
922
958
|
if (state.ws) {
|
|
923
959
|
const ws = state.ws;
|
|
924
960
|
state.ws = null;
|
|
@@ -931,8 +967,11 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
931
967
|
listeners.clear();
|
|
932
968
|
};
|
|
933
969
|
const disconnect = () => {
|
|
970
|
+
disposed = true;
|
|
971
|
+
lifecycleGeneration += 1;
|
|
934
972
|
clearTimers();
|
|
935
973
|
rejectPendingCallControls("Voice connection disconnected");
|
|
974
|
+
state.pendingMessages.length = 0;
|
|
936
975
|
if (state.ws) {
|
|
937
976
|
const ws = state.ws;
|
|
938
977
|
state.ws = null;
|
|
@@ -22,5 +22,8 @@ type VoiceConnectionHandle = {
|
|
|
22
22
|
simulateDisconnect: () => void;
|
|
23
23
|
subscribe: (callback: (message: VoiceServerMessage) => void) => () => void;
|
|
24
24
|
};
|
|
25
|
+
export declare class VoiceReconnectRejectedError extends Error {
|
|
26
|
+
constructor(message?: string);
|
|
27
|
+
}
|
|
25
28
|
export declare const createVoiceConnection: (path: string, options?: VoiceConnectionOptions) => VoiceConnectionHandle;
|
|
26
29
|
export {};
|
|
@@ -510,6 +510,13 @@ var RECONNECT_BASE_DELAY_MS = 500;
|
|
|
510
510
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
511
511
|
var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
|
|
512
512
|
var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
|
|
513
|
+
|
|
514
|
+
class VoiceReconnectRejectedError extends Error {
|
|
515
|
+
constructor(message = "Voice authorization could not be renewed.") {
|
|
516
|
+
super(message);
|
|
517
|
+
this.name = "VoiceReconnectRejectedError";
|
|
518
|
+
}
|
|
519
|
+
}
|
|
513
520
|
var noop = () => {};
|
|
514
521
|
var noopAsync = () => Promise.resolve();
|
|
515
522
|
var noopUnsubscribe = () => noop;
|
|
@@ -589,6 +596,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
589
596
|
const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
590
597
|
const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
|
|
591
598
|
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
599
|
+
let lifecycleGeneration = 0;
|
|
600
|
+
let disposed = false;
|
|
592
601
|
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
593
602
|
const state = {
|
|
594
603
|
isConnected: false,
|
|
@@ -604,6 +613,21 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
604
613
|
const emitConnection = (reconnect) => {
|
|
605
614
|
listeners.forEach((listener) => listener(reconnect));
|
|
606
615
|
};
|
|
616
|
+
const emitTerminalFailure = (message) => {
|
|
617
|
+
listeners.forEach((listener) => listener({
|
|
618
|
+
message,
|
|
619
|
+
recoverable: false,
|
|
620
|
+
type: "error"
|
|
621
|
+
}));
|
|
622
|
+
emitConnection({
|
|
623
|
+
reconnect: {
|
|
624
|
+
attempts: state.reconnectAttempts,
|
|
625
|
+
maxAttempts: maxReconnectAttempts,
|
|
626
|
+
status: "exhausted"
|
|
627
|
+
},
|
|
628
|
+
type: "connection"
|
|
629
|
+
});
|
|
630
|
+
};
|
|
607
631
|
const clearTimers = () => {
|
|
608
632
|
if (state.pingInterval) {
|
|
609
633
|
clearInterval(state.pingInterval);
|
|
@@ -637,6 +661,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
637
661
|
pendingCallControls.clear();
|
|
638
662
|
};
|
|
639
663
|
const scheduleReconnect = () => {
|
|
664
|
+
if (disposed)
|
|
665
|
+
return;
|
|
666
|
+
const scheduledGeneration = lifecycleGeneration;
|
|
640
667
|
state.reconnectAttempts += 1;
|
|
641
668
|
const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
|
|
642
669
|
const nextAttemptAt = Date.now() + delayMs;
|
|
@@ -651,15 +678,10 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
651
678
|
type: "connection"
|
|
652
679
|
});
|
|
653
680
|
state.reconnectTimeout = setTimeout(async () => {
|
|
681
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
682
|
+
return;
|
|
654
683
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
655
|
-
|
|
656
|
-
reconnect: {
|
|
657
|
-
attempts: state.reconnectAttempts,
|
|
658
|
-
maxAttempts: maxReconnectAttempts,
|
|
659
|
-
status: "exhausted"
|
|
660
|
-
},
|
|
661
|
-
type: "connection"
|
|
662
|
-
});
|
|
684
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
663
685
|
return;
|
|
664
686
|
}
|
|
665
687
|
try {
|
|
@@ -669,25 +691,28 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
669
691
|
scenarioId: state.scenarioId,
|
|
670
692
|
sessionId: state.sessionId
|
|
671
693
|
});
|
|
672
|
-
} catch {
|
|
694
|
+
} catch (error) {
|
|
695
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
696
|
+
return;
|
|
697
|
+
if (error instanceof VoiceReconnectRejectedError) {
|
|
698
|
+
emitTerminalFailure(error.message);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
673
701
|
if (state.reconnectAttempts < maxReconnectAttempts) {
|
|
674
702
|
scheduleReconnect();
|
|
675
703
|
} else {
|
|
676
|
-
|
|
677
|
-
reconnect: {
|
|
678
|
-
attempts: state.reconnectAttempts,
|
|
679
|
-
maxAttempts: maxReconnectAttempts,
|
|
680
|
-
status: "exhausted"
|
|
681
|
-
},
|
|
682
|
-
type: "connection"
|
|
683
|
-
});
|
|
704
|
+
emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
|
|
684
705
|
}
|
|
685
706
|
return;
|
|
686
707
|
}
|
|
708
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
709
|
+
return;
|
|
687
710
|
connect();
|
|
688
711
|
}, delayMs);
|
|
689
712
|
};
|
|
690
713
|
const connect = () => {
|
|
714
|
+
if (disposed)
|
|
715
|
+
return;
|
|
691
716
|
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
|
|
692
717
|
ws.binaryType = "arraybuffer";
|
|
693
718
|
ws.onopen = () => {
|
|
@@ -758,6 +783,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
758
783
|
state.ws = null;
|
|
759
784
|
state.isConnected = false;
|
|
760
785
|
clearTimers();
|
|
786
|
+
rejectPendingCallControls("Voice connection interrupted");
|
|
761
787
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
|
|
762
788
|
const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
|
|
763
789
|
if (authorizationFailure) {
|
|
@@ -770,15 +796,19 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
770
796
|
if (reconnectable) {
|
|
771
797
|
scheduleReconnect();
|
|
772
798
|
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
799
|
+
if (authorizationFailure) {
|
|
800
|
+
emitConnection({
|
|
801
|
+
reconnect: {
|
|
802
|
+
attempts: state.reconnectAttempts,
|
|
803
|
+
lastDisconnectAt: Date.now(),
|
|
804
|
+
maxAttempts: maxReconnectAttempts,
|
|
805
|
+
status: "exhausted"
|
|
806
|
+
},
|
|
807
|
+
type: "connection"
|
|
808
|
+
});
|
|
809
|
+
} else {
|
|
810
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
811
|
+
}
|
|
782
812
|
}
|
|
783
813
|
};
|
|
784
814
|
state.ws = ws;
|
|
@@ -819,6 +849,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
819
849
|
send({ type: "end_turn" });
|
|
820
850
|
};
|
|
821
851
|
const callControl = (message) => {
|
|
852
|
+
if (state.ws?.readyState !== WS_OPEN) {
|
|
853
|
+
return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
|
|
854
|
+
}
|
|
822
855
|
const requestId = crypto.randomUUID();
|
|
823
856
|
const { promise, reject, resolve } = Promise.withResolvers();
|
|
824
857
|
const timeout = setTimeout(() => {
|
|
@@ -826,16 +859,19 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
826
859
|
reject(new Error(`${message.action} acknowledgement timed out`));
|
|
827
860
|
}, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
|
|
828
861
|
pendingCallControls.set(requestId, { reject, resolve, timeout });
|
|
829
|
-
send({
|
|
862
|
+
state.ws.send(JSON.stringify({
|
|
830
863
|
...message,
|
|
831
864
|
requestId,
|
|
832
865
|
type: "call_control"
|
|
833
|
-
});
|
|
866
|
+
}));
|
|
834
867
|
return promise;
|
|
835
868
|
};
|
|
836
869
|
const close = (reason = "client-close") => {
|
|
870
|
+
disposed = true;
|
|
871
|
+
lifecycleGeneration += 1;
|
|
837
872
|
clearTimers();
|
|
838
873
|
rejectPendingCallControls("Voice connection closed");
|
|
874
|
+
state.pendingMessages.length = 0;
|
|
839
875
|
if (state.ws) {
|
|
840
876
|
const ws = state.ws;
|
|
841
877
|
state.ws = null;
|
|
@@ -848,8 +884,11 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
848
884
|
listeners.clear();
|
|
849
885
|
};
|
|
850
886
|
const disconnect = () => {
|
|
887
|
+
disposed = true;
|
|
888
|
+
lifecycleGeneration += 1;
|
|
851
889
|
clearTimers();
|
|
852
890
|
rejectPendingCallControls("Voice connection disconnected");
|
|
891
|
+
state.pendingMessages.length = 0;
|
|
853
892
|
if (state.ws) {
|
|
854
893
|
const ws = state.ws;
|
|
855
894
|
state.ws = null;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { bindVoiceReactiveSource, voiceSseReactiveSource, } from "./reactiveSource";
|
|
2
2
|
export type { VoiceReactiveSource, VoiceSseReactiveSourceOptions, } from "./reactiveSource";
|
|
3
|
-
export { createVoiceConnection } from "./connection";
|
|
3
|
+
export { createVoiceConnection, VoiceReconnectRejectedError, } from "./connection";
|
|
4
4
|
export { checkBrowserVoiceSupport, probeBrowserVoiceSupport, } from "./browserVoiceSupport";
|
|
5
5
|
export type { BrowserVoiceSupportProbe, BrowserVoiceSupportReport, } from "./browserVoiceSupport";
|
|
6
6
|
export { createVoiceAudioPlayer, decodeVoiceAudioChunk } from "./audioPlayer";
|
package/dist/client/index.js
CHANGED
|
@@ -115,6 +115,13 @@ var RECONNECT_BASE_DELAY_MS = 500;
|
|
|
115
115
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
116
116
|
var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
|
|
117
117
|
var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
|
|
118
|
+
|
|
119
|
+
class VoiceReconnectRejectedError extends Error {
|
|
120
|
+
constructor(message = "Voice authorization could not be renewed.") {
|
|
121
|
+
super(message);
|
|
122
|
+
this.name = "VoiceReconnectRejectedError";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
118
125
|
var noop = () => {};
|
|
119
126
|
var noopAsync = () => Promise.resolve();
|
|
120
127
|
var noopUnsubscribe = () => noop;
|
|
@@ -194,6 +201,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
194
201
|
const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
195
202
|
const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
|
|
196
203
|
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
204
|
+
let lifecycleGeneration = 0;
|
|
205
|
+
let disposed = false;
|
|
197
206
|
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
198
207
|
const state = {
|
|
199
208
|
isConnected: false,
|
|
@@ -209,6 +218,21 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
209
218
|
const emitConnection = (reconnect) => {
|
|
210
219
|
listeners.forEach((listener) => listener(reconnect));
|
|
211
220
|
};
|
|
221
|
+
const emitTerminalFailure = (message) => {
|
|
222
|
+
listeners.forEach((listener) => listener({
|
|
223
|
+
message,
|
|
224
|
+
recoverable: false,
|
|
225
|
+
type: "error"
|
|
226
|
+
}));
|
|
227
|
+
emitConnection({
|
|
228
|
+
reconnect: {
|
|
229
|
+
attempts: state.reconnectAttempts,
|
|
230
|
+
maxAttempts: maxReconnectAttempts,
|
|
231
|
+
status: "exhausted"
|
|
232
|
+
},
|
|
233
|
+
type: "connection"
|
|
234
|
+
});
|
|
235
|
+
};
|
|
212
236
|
const clearTimers = () => {
|
|
213
237
|
if (state.pingInterval) {
|
|
214
238
|
clearInterval(state.pingInterval);
|
|
@@ -242,6 +266,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
242
266
|
pendingCallControls.clear();
|
|
243
267
|
};
|
|
244
268
|
const scheduleReconnect = () => {
|
|
269
|
+
if (disposed)
|
|
270
|
+
return;
|
|
271
|
+
const scheduledGeneration = lifecycleGeneration;
|
|
245
272
|
state.reconnectAttempts += 1;
|
|
246
273
|
const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
|
|
247
274
|
const nextAttemptAt = Date.now() + delayMs;
|
|
@@ -256,15 +283,10 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
256
283
|
type: "connection"
|
|
257
284
|
});
|
|
258
285
|
state.reconnectTimeout = setTimeout(async () => {
|
|
286
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
287
|
+
return;
|
|
259
288
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
260
|
-
|
|
261
|
-
reconnect: {
|
|
262
|
-
attempts: state.reconnectAttempts,
|
|
263
|
-
maxAttempts: maxReconnectAttempts,
|
|
264
|
-
status: "exhausted"
|
|
265
|
-
},
|
|
266
|
-
type: "connection"
|
|
267
|
-
});
|
|
289
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
268
290
|
return;
|
|
269
291
|
}
|
|
270
292
|
try {
|
|
@@ -274,25 +296,28 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
274
296
|
scenarioId: state.scenarioId,
|
|
275
297
|
sessionId: state.sessionId
|
|
276
298
|
});
|
|
277
|
-
} catch {
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
301
|
+
return;
|
|
302
|
+
if (error instanceof VoiceReconnectRejectedError) {
|
|
303
|
+
emitTerminalFailure(error.message);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
278
306
|
if (state.reconnectAttempts < maxReconnectAttempts) {
|
|
279
307
|
scheduleReconnect();
|
|
280
308
|
} else {
|
|
281
|
-
|
|
282
|
-
reconnect: {
|
|
283
|
-
attempts: state.reconnectAttempts,
|
|
284
|
-
maxAttempts: maxReconnectAttempts,
|
|
285
|
-
status: "exhausted"
|
|
286
|
-
},
|
|
287
|
-
type: "connection"
|
|
288
|
-
});
|
|
309
|
+
emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
|
|
289
310
|
}
|
|
290
311
|
return;
|
|
291
312
|
}
|
|
313
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
314
|
+
return;
|
|
292
315
|
connect();
|
|
293
316
|
}, delayMs);
|
|
294
317
|
};
|
|
295
318
|
const connect = () => {
|
|
319
|
+
if (disposed)
|
|
320
|
+
return;
|
|
296
321
|
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
|
|
297
322
|
ws.binaryType = "arraybuffer";
|
|
298
323
|
ws.onopen = () => {
|
|
@@ -363,6 +388,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
363
388
|
state.ws = null;
|
|
364
389
|
state.isConnected = false;
|
|
365
390
|
clearTimers();
|
|
391
|
+
rejectPendingCallControls("Voice connection interrupted");
|
|
366
392
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
|
|
367
393
|
const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
|
|
368
394
|
if (authorizationFailure) {
|
|
@@ -375,15 +401,19 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
375
401
|
if (reconnectable) {
|
|
376
402
|
scheduleReconnect();
|
|
377
403
|
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
404
|
+
if (authorizationFailure) {
|
|
405
|
+
emitConnection({
|
|
406
|
+
reconnect: {
|
|
407
|
+
attempts: state.reconnectAttempts,
|
|
408
|
+
lastDisconnectAt: Date.now(),
|
|
409
|
+
maxAttempts: maxReconnectAttempts,
|
|
410
|
+
status: "exhausted"
|
|
411
|
+
},
|
|
412
|
+
type: "connection"
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
emitTerminalFailure("Voice connection could not be restored.");
|
|
416
|
+
}
|
|
387
417
|
}
|
|
388
418
|
};
|
|
389
419
|
state.ws = ws;
|
|
@@ -424,6 +454,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
424
454
|
send({ type: "end_turn" });
|
|
425
455
|
};
|
|
426
456
|
const callControl = (message) => {
|
|
457
|
+
if (state.ws?.readyState !== WS_OPEN) {
|
|
458
|
+
return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
|
|
459
|
+
}
|
|
427
460
|
const requestId = crypto.randomUUID();
|
|
428
461
|
const { promise, reject, resolve } = Promise.withResolvers();
|
|
429
462
|
const timeout = setTimeout(() => {
|
|
@@ -431,16 +464,19 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
431
464
|
reject(new Error(`${message.action} acknowledgement timed out`));
|
|
432
465
|
}, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
|
|
433
466
|
pendingCallControls.set(requestId, { reject, resolve, timeout });
|
|
434
|
-
send({
|
|
467
|
+
state.ws.send(JSON.stringify({
|
|
435
468
|
...message,
|
|
436
469
|
requestId,
|
|
437
470
|
type: "call_control"
|
|
438
|
-
});
|
|
471
|
+
}));
|
|
439
472
|
return promise;
|
|
440
473
|
};
|
|
441
474
|
const close = (reason = "client-close") => {
|
|
475
|
+
disposed = true;
|
|
476
|
+
lifecycleGeneration += 1;
|
|
442
477
|
clearTimers();
|
|
443
478
|
rejectPendingCallControls("Voice connection closed");
|
|
479
|
+
state.pendingMessages.length = 0;
|
|
444
480
|
if (state.ws) {
|
|
445
481
|
const ws = state.ws;
|
|
446
482
|
state.ws = null;
|
|
@@ -453,8 +489,11 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
453
489
|
listeners.clear();
|
|
454
490
|
};
|
|
455
491
|
const disconnect = () => {
|
|
492
|
+
disposed = true;
|
|
493
|
+
lifecycleGeneration += 1;
|
|
456
494
|
clearTimers();
|
|
457
495
|
rejectPendingCallControls("Voice connection disconnected");
|
|
496
|
+
state.pendingMessages.length = 0;
|
|
458
497
|
if (state.ws) {
|
|
459
498
|
const ws = state.ws;
|
|
460
499
|
state.ws = null;
|
|
@@ -12835,5 +12874,6 @@ export {
|
|
|
12835
12874
|
bindVoiceReactiveSource,
|
|
12836
12875
|
bindVoiceProviderSimulationControls,
|
|
12837
12876
|
bindVoiceHTMX,
|
|
12838
|
-
bindVoiceBargeIn
|
|
12877
|
+
bindVoiceBargeIn,
|
|
12878
|
+
VoiceReconnectRejectedError
|
|
12839
12879
|
};
|