assemblyai 4.34.5 → 4.35.0
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/CHANGELOG.md +4 -0
- package/README.md +0 -3
- package/dist/assemblyai.streaming.umd.js +146 -10
- package/dist/assemblyai.streaming.umd.min.js +1 -1
- package/dist/assemblyai.umd.js +146 -10
- package/dist/assemblyai.umd.min.js +1 -1
- package/dist/browser.mjs +144 -12
- package/dist/bun.mjs +144 -12
- package/dist/deno.mjs +144 -12
- package/dist/index.cjs +146 -10
- package/dist/index.mjs +146 -10
- package/dist/node.cjs +144 -12
- package/dist/node.mjs +144 -12
- package/dist/services/streaming/service.d.ts +22 -0
- package/dist/streaming.browser.mjs +144 -12
- package/dist/streaming.cjs +145 -9
- package/dist/streaming.mjs +145 -9
- package/dist/types/streaming/index.d.ts +21 -2
- package/dist/workerd.mjs +144 -12
- package/package.json +1 -1
- package/src/services/streaming/service.ts +167 -11
- package/src/types/streaming/index.ts +22 -0
|
@@ -531,6 +531,24 @@ function toInt16View(audio) {
|
|
|
531
531
|
}
|
|
532
532
|
const defaultStreamingUrl = "wss://streaming.assemblyai.com/v3/ws";
|
|
533
533
|
const terminateSessionMessage = `{"type":"Terminate"}`;
|
|
534
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1000;
|
|
535
|
+
const DEFAULT_MAX_CONNECTION_RETRIES = 2;
|
|
536
|
+
const DEFAULT_CONNECTION_RETRY_DELAY_MS = 500;
|
|
537
|
+
/**
|
|
538
|
+
* Close/error codes that signal a permanent client-side problem (auth,
|
|
539
|
+
* billing, malformed config). A retry would hit the same failure, so the
|
|
540
|
+
* connection is never retried on these.
|
|
541
|
+
*/
|
|
542
|
+
const NON_RETRYABLE_CLOSE_CODES = new Set([
|
|
543
|
+
StreamingErrorType.BadSampleRate,
|
|
544
|
+
StreamingErrorType.AuthFailed,
|
|
545
|
+
StreamingErrorType.InsufficientFunds,
|
|
546
|
+
StreamingErrorType.FreeTierUser,
|
|
547
|
+
StreamingErrorType.BadSchema,
|
|
548
|
+
]);
|
|
549
|
+
function isRetryableCloseCode(code) {
|
|
550
|
+
return code !== 1000 && !NON_RETRYABLE_CLOSE_CODES.has(code);
|
|
551
|
+
}
|
|
534
552
|
/**
|
|
535
553
|
* Per-send chunk cap in milliseconds for the dual-channel mixer. The streaming
|
|
536
554
|
* server rejects audio messages longer than 1000 ms (`Input Duration Error`).
|
|
@@ -739,12 +757,81 @@ class StreamingTranscriber {
|
|
|
739
757
|
on(event, listener) {
|
|
740
758
|
this.listeners[event] = listener;
|
|
741
759
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
760
|
+
/**
|
|
761
|
+
* Open the streaming session.
|
|
762
|
+
*
|
|
763
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
764
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
765
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
766
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
767
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
768
|
+
* funds, malformed config) are not retried.
|
|
769
|
+
*
|
|
770
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
771
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
772
|
+
* the retry loop) to observe the failure.
|
|
773
|
+
*/
|
|
774
|
+
async connect() {
|
|
775
|
+
if (this.socket) {
|
|
776
|
+
throw new Error("Already connected");
|
|
777
|
+
}
|
|
778
|
+
const maxRetries = this.params.maxConnectionRetries ?? DEFAULT_MAX_CONNECTION_RETRIES;
|
|
779
|
+
const retryDelay = this.params.connectionRetryDelay ?? DEFAULT_CONNECTION_RETRY_DELAY_MS;
|
|
780
|
+
let lastError;
|
|
781
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
782
|
+
try {
|
|
783
|
+
return await this.connectOnce();
|
|
784
|
+
}
|
|
785
|
+
catch (err) {
|
|
786
|
+
lastError = err;
|
|
787
|
+
const retryable = err.retryable === true;
|
|
788
|
+
if (!retryable || attempt === maxRetries) {
|
|
789
|
+
throw err;
|
|
790
|
+
}
|
|
791
|
+
console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
|
|
792
|
+
if (retryDelay > 0) {
|
|
793
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
794
|
+
}
|
|
746
795
|
}
|
|
796
|
+
}
|
|
797
|
+
// The loop above always returns or throws; this only satisfies the type
|
|
798
|
+
// checker that a value is produced on every path.
|
|
799
|
+
throw lastError ?? new Error("Failed to connect to streaming server");
|
|
800
|
+
}
|
|
801
|
+
connectOnce() {
|
|
802
|
+
return new Promise((resolve, reject) => {
|
|
747
803
|
const url = this.connectionUrl();
|
|
804
|
+
const timeoutMs = this.params.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
805
|
+
// `settled` flips once this attempt has resolved (`Begin`) or rejected
|
|
806
|
+
// (timeout / pre-`Begin` close / error). Before it flips the socket
|
|
807
|
+
// handlers drive this promise; after it flips they revert to normal
|
|
808
|
+
// runtime dispatch (close / error / message listeners).
|
|
809
|
+
let settled = false;
|
|
810
|
+
let timer;
|
|
811
|
+
const failAttempt = (error) => {
|
|
812
|
+
if (settled)
|
|
813
|
+
return;
|
|
814
|
+
settled = true;
|
|
815
|
+
if (timer)
|
|
816
|
+
clearTimeout(timer);
|
|
817
|
+
this.discardPendingSocket();
|
|
818
|
+
reject(error);
|
|
819
|
+
};
|
|
820
|
+
const succeed = (begin) => {
|
|
821
|
+
if (settled)
|
|
822
|
+
return;
|
|
823
|
+
settled = true;
|
|
824
|
+
if (timer)
|
|
825
|
+
clearTimeout(timer);
|
|
826
|
+
resolve(begin);
|
|
827
|
+
};
|
|
828
|
+
if (timeoutMs > 0) {
|
|
829
|
+
timer = setTimeout(() => {
|
|
830
|
+
const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
|
|
831
|
+
err.retryable = true;
|
|
832
|
+
failAttempt(err);
|
|
833
|
+
}, timeoutMs);
|
|
834
|
+
}
|
|
748
835
|
if (this.token) {
|
|
749
836
|
this.socket = factory(url.toString());
|
|
750
837
|
}
|
|
@@ -765,6 +852,15 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
|
|
|
765
852
|
reason = StreamingErrorMessages[code];
|
|
766
853
|
}
|
|
767
854
|
}
|
|
855
|
+
// A close before `Begin` is a failed connection attempt — reject so
|
|
856
|
+
// connect() can retry (or surface a permanent failure).
|
|
857
|
+
if (!settled) {
|
|
858
|
+
const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
|
|
859
|
+
err.code = code;
|
|
860
|
+
err.retryable = isRetryableCloseCode(code);
|
|
861
|
+
failAttempt(err);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
768
864
|
// Stop the flush timer when the socket is gone (server-initiated close,
|
|
769
865
|
// network drop, etc.) — otherwise subsequent ticks call send() on a
|
|
770
866
|
// closed socket and spam the error listener.
|
|
@@ -775,25 +871,37 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
|
|
|
775
871
|
this.listeners.close?.(code, reason);
|
|
776
872
|
};
|
|
777
873
|
this.socket.onerror = (event) => {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
874
|
+
const error = event.error ?? new Error(event.message);
|
|
875
|
+
// A socket error before `Begin` is a failed attempt → reject/retry.
|
|
876
|
+
if (!settled) {
|
|
877
|
+
error.retryable = true;
|
|
878
|
+
failAttempt(error);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
this.listeners.error?.(error);
|
|
782
882
|
};
|
|
783
883
|
this.socket.onmessage = ({ data }) => {
|
|
784
884
|
const message = JSON.parse(data.toString());
|
|
785
885
|
if ("error" in message) {
|
|
786
886
|
const err = new StreamingError(message.error);
|
|
787
887
|
if ("error_code" in message) {
|
|
788
|
-
err.code =
|
|
789
|
-
|
|
888
|
+
err.code = message.error_code;
|
|
889
|
+
}
|
|
890
|
+
// A server error frame before `Begin` fails the attempt; the code
|
|
891
|
+
// decides whether a retry is worthwhile.
|
|
892
|
+
if (!settled) {
|
|
893
|
+
const attemptErr = err;
|
|
894
|
+
attemptErr.retryable =
|
|
895
|
+
err.code === undefined ? true : isRetryableCloseCode(err.code);
|
|
896
|
+
failAttempt(attemptErr);
|
|
897
|
+
return;
|
|
790
898
|
}
|
|
791
899
|
this.listeners.error?.(err);
|
|
792
900
|
return;
|
|
793
901
|
}
|
|
794
902
|
switch (message.type) {
|
|
795
903
|
case "Begin": {
|
|
796
|
-
|
|
904
|
+
succeed(message);
|
|
797
905
|
this.listeners.open?.(message);
|
|
798
906
|
break;
|
|
799
907
|
}
|
|
@@ -840,6 +948,20 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
|
|
|
840
948
|
};
|
|
841
949
|
});
|
|
842
950
|
}
|
|
951
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
952
|
+
discardPendingSocket() {
|
|
953
|
+
if (!this.socket)
|
|
954
|
+
return;
|
|
955
|
+
try {
|
|
956
|
+
if (this.socket.removeAllListeners)
|
|
957
|
+
this.socket.removeAllListeners();
|
|
958
|
+
this.socket.close();
|
|
959
|
+
}
|
|
960
|
+
catch {
|
|
961
|
+
// Best-effort cleanup; a half-open socket may throw on close.
|
|
962
|
+
}
|
|
963
|
+
this.socket = undefined;
|
|
964
|
+
}
|
|
843
965
|
/**
|
|
844
966
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
845
967
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -1113,6 +1235,16 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
|
|
|
1113
1235
|
};
|
|
1114
1236
|
this.send(JSON.stringify(message));
|
|
1115
1237
|
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
1240
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
1241
|
+
*/
|
|
1242
|
+
keepAlive() {
|
|
1243
|
+
const message = {
|
|
1244
|
+
type: "KeepAlive",
|
|
1245
|
+
};
|
|
1246
|
+
this.send(JSON.stringify(message));
|
|
1247
|
+
}
|
|
1116
1248
|
send(data) {
|
|
1117
1249
|
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
|
|
1118
1250
|
throw new Error("Socket is not open for communication");
|
|
@@ -1168,7 +1300,7 @@ if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
|
1168
1300
|
defaultUserAgentString += navigator.userAgent;
|
|
1169
1301
|
}
|
|
1170
1302
|
const defaultUserAgent = {
|
|
1171
|
-
sdk: { name: "JavaScript", version: "4.
|
|
1303
|
+
sdk: { name: "JavaScript", version: "4.35.0" },
|
|
1172
1304
|
};
|
|
1173
1305
|
if (typeof process !== "undefined") {
|
|
1174
1306
|
if (process.versions.node && defaultUserAgentString.indexOf("Node") === -1) {
|
package/dist/streaming.cjs
CHANGED
|
@@ -579,6 +579,24 @@ function toInt16View(audio) {
|
|
|
579
579
|
}
|
|
580
580
|
const defaultStreamingUrl = "wss://streaming.assemblyai.com/v3/ws";
|
|
581
581
|
const terminateSessionMessage = `{"type":"Terminate"}`;
|
|
582
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1000;
|
|
583
|
+
const DEFAULT_MAX_CONNECTION_RETRIES = 2;
|
|
584
|
+
const DEFAULT_CONNECTION_RETRY_DELAY_MS = 500;
|
|
585
|
+
/**
|
|
586
|
+
* Close/error codes that signal a permanent client-side problem (auth,
|
|
587
|
+
* billing, malformed config). A retry would hit the same failure, so the
|
|
588
|
+
* connection is never retried on these.
|
|
589
|
+
*/
|
|
590
|
+
const NON_RETRYABLE_CLOSE_CODES = new Set([
|
|
591
|
+
StreamingErrorType.BadSampleRate,
|
|
592
|
+
StreamingErrorType.AuthFailed,
|
|
593
|
+
StreamingErrorType.InsufficientFunds,
|
|
594
|
+
StreamingErrorType.FreeTierUser,
|
|
595
|
+
StreamingErrorType.BadSchema,
|
|
596
|
+
]);
|
|
597
|
+
function isRetryableCloseCode(code) {
|
|
598
|
+
return code !== 1000 && !NON_RETRYABLE_CLOSE_CODES.has(code);
|
|
599
|
+
}
|
|
582
600
|
/**
|
|
583
601
|
* Per-send chunk cap in milliseconds for the dual-channel mixer. The streaming
|
|
584
602
|
* server rejects audio messages longer than 1000 ms (`Input Duration Error`).
|
|
@@ -785,12 +803,85 @@ class StreamingTranscriber {
|
|
|
785
803
|
on(event, listener) {
|
|
786
804
|
this.listeners[event] = listener;
|
|
787
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* Open the streaming session.
|
|
808
|
+
*
|
|
809
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
810
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
811
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
812
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
813
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
814
|
+
* funds, malformed config) are not retried.
|
|
815
|
+
*
|
|
816
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
817
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
818
|
+
* the retry loop) to observe the failure.
|
|
819
|
+
*/
|
|
788
820
|
connect() {
|
|
789
|
-
return
|
|
821
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
822
|
+
var _a, _b;
|
|
790
823
|
if (this.socket) {
|
|
791
824
|
throw new Error("Already connected");
|
|
792
825
|
}
|
|
826
|
+
const maxRetries = (_a = this.params.maxConnectionRetries) !== null && _a !== void 0 ? _a : DEFAULT_MAX_CONNECTION_RETRIES;
|
|
827
|
+
const retryDelay = (_b = this.params.connectionRetryDelay) !== null && _b !== void 0 ? _b : DEFAULT_CONNECTION_RETRY_DELAY_MS;
|
|
828
|
+
let lastError;
|
|
829
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
830
|
+
try {
|
|
831
|
+
return yield this.connectOnce();
|
|
832
|
+
}
|
|
833
|
+
catch (err) {
|
|
834
|
+
lastError = err;
|
|
835
|
+
const retryable = err.retryable === true;
|
|
836
|
+
if (!retryable || attempt === maxRetries) {
|
|
837
|
+
throw err;
|
|
838
|
+
}
|
|
839
|
+
console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
|
|
840
|
+
if (retryDelay > 0) {
|
|
841
|
+
yield new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// The loop above always returns or throws; this only satisfies the type
|
|
846
|
+
// checker that a value is produced on every path.
|
|
847
|
+
throw lastError !== null && lastError !== void 0 ? lastError : new Error("Failed to connect to streaming server");
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
connectOnce() {
|
|
851
|
+
return new Promise((resolve, reject) => {
|
|
852
|
+
var _a;
|
|
793
853
|
const url = this.connectionUrl();
|
|
854
|
+
const timeoutMs = (_a = this.params.connectTimeout) !== null && _a !== void 0 ? _a : DEFAULT_CONNECT_TIMEOUT_MS;
|
|
855
|
+
// `settled` flips once this attempt has resolved (`Begin`) or rejected
|
|
856
|
+
// (timeout / pre-`Begin` close / error). Before it flips the socket
|
|
857
|
+
// handlers drive this promise; after it flips they revert to normal
|
|
858
|
+
// runtime dispatch (close / error / message listeners).
|
|
859
|
+
let settled = false;
|
|
860
|
+
let timer;
|
|
861
|
+
const failAttempt = (error) => {
|
|
862
|
+
if (settled)
|
|
863
|
+
return;
|
|
864
|
+
settled = true;
|
|
865
|
+
if (timer)
|
|
866
|
+
clearTimeout(timer);
|
|
867
|
+
this.discardPendingSocket();
|
|
868
|
+
reject(error);
|
|
869
|
+
};
|
|
870
|
+
const succeed = (begin) => {
|
|
871
|
+
if (settled)
|
|
872
|
+
return;
|
|
873
|
+
settled = true;
|
|
874
|
+
if (timer)
|
|
875
|
+
clearTimeout(timer);
|
|
876
|
+
resolve(begin);
|
|
877
|
+
};
|
|
878
|
+
if (timeoutMs > 0) {
|
|
879
|
+
timer = setTimeout(() => {
|
|
880
|
+
const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
|
|
881
|
+
err.retryable = true;
|
|
882
|
+
failAttempt(err);
|
|
883
|
+
}, timeoutMs);
|
|
884
|
+
}
|
|
794
885
|
if (this.token) {
|
|
795
886
|
this.socket = factory(url.toString());
|
|
796
887
|
}
|
|
@@ -808,6 +899,15 @@ class StreamingTranscriber {
|
|
|
808
899
|
reason = StreamingErrorMessages[code];
|
|
809
900
|
}
|
|
810
901
|
}
|
|
902
|
+
// A close before `Begin` is a failed connection attempt — reject so
|
|
903
|
+
// connect() can retry (or surface a permanent failure).
|
|
904
|
+
if (!settled) {
|
|
905
|
+
const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
|
|
906
|
+
err.code = code;
|
|
907
|
+
err.retryable = isRetryableCloseCode(code);
|
|
908
|
+
failAttempt(err);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
811
911
|
// Stop the flush timer when the socket is gone (server-initiated close,
|
|
812
912
|
// network drop, etc.) — otherwise subsequent ticks call send() on a
|
|
813
913
|
// closed socket and spam the error listener.
|
|
@@ -818,11 +918,15 @@ class StreamingTranscriber {
|
|
|
818
918
|
(_b = (_a = this.listeners).close) === null || _b === void 0 ? void 0 : _b.call(_a, code, reason);
|
|
819
919
|
};
|
|
820
920
|
this.socket.onerror = (event) => {
|
|
821
|
-
var _a, _b, _c
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
921
|
+
var _a, _b, _c;
|
|
922
|
+
const error = (_a = event.error) !== null && _a !== void 0 ? _a : new Error(event.message);
|
|
923
|
+
// A socket error before `Begin` is a failed attempt → reject/retry.
|
|
924
|
+
if (!settled) {
|
|
925
|
+
error.retryable = true;
|
|
926
|
+
failAttempt(error);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
(_c = (_b = this.listeners).error) === null || _c === void 0 ? void 0 : _c.call(_b, error);
|
|
826
930
|
};
|
|
827
931
|
this.socket.onmessage = ({ data }) => {
|
|
828
932
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
@@ -830,15 +934,23 @@ class StreamingTranscriber {
|
|
|
830
934
|
if ("error" in message) {
|
|
831
935
|
const err = new StreamingError(message.error);
|
|
832
936
|
if ("error_code" in message) {
|
|
833
|
-
err.code =
|
|
834
|
-
|
|
937
|
+
err.code = message.error_code;
|
|
938
|
+
}
|
|
939
|
+
// A server error frame before `Begin` fails the attempt; the code
|
|
940
|
+
// decides whether a retry is worthwhile.
|
|
941
|
+
if (!settled) {
|
|
942
|
+
const attemptErr = err;
|
|
943
|
+
attemptErr.retryable =
|
|
944
|
+
err.code === undefined ? true : isRetryableCloseCode(err.code);
|
|
945
|
+
failAttempt(attemptErr);
|
|
946
|
+
return;
|
|
835
947
|
}
|
|
836
948
|
(_b = (_a = this.listeners).error) === null || _b === void 0 ? void 0 : _b.call(_a, err);
|
|
837
949
|
return;
|
|
838
950
|
}
|
|
839
951
|
switch (message.type) {
|
|
840
952
|
case "Begin": {
|
|
841
|
-
|
|
953
|
+
succeed(message);
|
|
842
954
|
(_d = (_c = this.listeners).open) === null || _d === void 0 ? void 0 : _d.call(_c, message);
|
|
843
955
|
break;
|
|
844
956
|
}
|
|
@@ -885,6 +997,20 @@ class StreamingTranscriber {
|
|
|
885
997
|
};
|
|
886
998
|
});
|
|
887
999
|
}
|
|
1000
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
1001
|
+
discardPendingSocket() {
|
|
1002
|
+
if (!this.socket)
|
|
1003
|
+
return;
|
|
1004
|
+
try {
|
|
1005
|
+
if (this.socket.removeAllListeners)
|
|
1006
|
+
this.socket.removeAllListeners();
|
|
1007
|
+
this.socket.close();
|
|
1008
|
+
}
|
|
1009
|
+
catch (_a) {
|
|
1010
|
+
// Best-effort cleanup; a half-open socket may throw on close.
|
|
1011
|
+
}
|
|
1012
|
+
this.socket = undefined;
|
|
1013
|
+
}
|
|
888
1014
|
/**
|
|
889
1015
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
890
1016
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -1158,6 +1284,16 @@ class StreamingTranscriber {
|
|
|
1158
1284
|
};
|
|
1159
1285
|
this.send(JSON.stringify(message));
|
|
1160
1286
|
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
1289
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
1290
|
+
*/
|
|
1291
|
+
keepAlive() {
|
|
1292
|
+
const message = {
|
|
1293
|
+
type: "KeepAlive",
|
|
1294
|
+
};
|
|
1295
|
+
this.send(JSON.stringify(message));
|
|
1296
|
+
}
|
|
1161
1297
|
send(data) {
|
|
1162
1298
|
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
|
|
1163
1299
|
throw new Error("Socket is not open for communication");
|
package/dist/streaming.mjs
CHANGED
|
@@ -577,6 +577,24 @@ function toInt16View(audio) {
|
|
|
577
577
|
}
|
|
578
578
|
const defaultStreamingUrl = "wss://streaming.assemblyai.com/v3/ws";
|
|
579
579
|
const terminateSessionMessage = `{"type":"Terminate"}`;
|
|
580
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1000;
|
|
581
|
+
const DEFAULT_MAX_CONNECTION_RETRIES = 2;
|
|
582
|
+
const DEFAULT_CONNECTION_RETRY_DELAY_MS = 500;
|
|
583
|
+
/**
|
|
584
|
+
* Close/error codes that signal a permanent client-side problem (auth,
|
|
585
|
+
* billing, malformed config). A retry would hit the same failure, so the
|
|
586
|
+
* connection is never retried on these.
|
|
587
|
+
*/
|
|
588
|
+
const NON_RETRYABLE_CLOSE_CODES = new Set([
|
|
589
|
+
StreamingErrorType.BadSampleRate,
|
|
590
|
+
StreamingErrorType.AuthFailed,
|
|
591
|
+
StreamingErrorType.InsufficientFunds,
|
|
592
|
+
StreamingErrorType.FreeTierUser,
|
|
593
|
+
StreamingErrorType.BadSchema,
|
|
594
|
+
]);
|
|
595
|
+
function isRetryableCloseCode(code) {
|
|
596
|
+
return code !== 1000 && !NON_RETRYABLE_CLOSE_CODES.has(code);
|
|
597
|
+
}
|
|
580
598
|
/**
|
|
581
599
|
* Per-send chunk cap in milliseconds for the dual-channel mixer. The streaming
|
|
582
600
|
* server rejects audio messages longer than 1000 ms (`Input Duration Error`).
|
|
@@ -783,12 +801,85 @@ class StreamingTranscriber {
|
|
|
783
801
|
on(event, listener) {
|
|
784
802
|
this.listeners[event] = listener;
|
|
785
803
|
}
|
|
804
|
+
/**
|
|
805
|
+
* Open the streaming session.
|
|
806
|
+
*
|
|
807
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
808
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
809
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
810
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
811
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
812
|
+
* funds, malformed config) are not retried.
|
|
813
|
+
*
|
|
814
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
815
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
816
|
+
* the retry loop) to observe the failure.
|
|
817
|
+
*/
|
|
786
818
|
connect() {
|
|
787
|
-
return
|
|
819
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
820
|
+
var _a, _b;
|
|
788
821
|
if (this.socket) {
|
|
789
822
|
throw new Error("Already connected");
|
|
790
823
|
}
|
|
824
|
+
const maxRetries = (_a = this.params.maxConnectionRetries) !== null && _a !== void 0 ? _a : DEFAULT_MAX_CONNECTION_RETRIES;
|
|
825
|
+
const retryDelay = (_b = this.params.connectionRetryDelay) !== null && _b !== void 0 ? _b : DEFAULT_CONNECTION_RETRY_DELAY_MS;
|
|
826
|
+
let lastError;
|
|
827
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
828
|
+
try {
|
|
829
|
+
return yield this.connectOnce();
|
|
830
|
+
}
|
|
831
|
+
catch (err) {
|
|
832
|
+
lastError = err;
|
|
833
|
+
const retryable = err.retryable === true;
|
|
834
|
+
if (!retryable || attempt === maxRetries) {
|
|
835
|
+
throw err;
|
|
836
|
+
}
|
|
837
|
+
console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
|
|
838
|
+
if (retryDelay > 0) {
|
|
839
|
+
yield new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
// The loop above always returns or throws; this only satisfies the type
|
|
844
|
+
// checker that a value is produced on every path.
|
|
845
|
+
throw lastError !== null && lastError !== void 0 ? lastError : new Error("Failed to connect to streaming server");
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
connectOnce() {
|
|
849
|
+
return new Promise((resolve, reject) => {
|
|
850
|
+
var _a;
|
|
791
851
|
const url = this.connectionUrl();
|
|
852
|
+
const timeoutMs = (_a = this.params.connectTimeout) !== null && _a !== void 0 ? _a : DEFAULT_CONNECT_TIMEOUT_MS;
|
|
853
|
+
// `settled` flips once this attempt has resolved (`Begin`) or rejected
|
|
854
|
+
// (timeout / pre-`Begin` close / error). Before it flips the socket
|
|
855
|
+
// handlers drive this promise; after it flips they revert to normal
|
|
856
|
+
// runtime dispatch (close / error / message listeners).
|
|
857
|
+
let settled = false;
|
|
858
|
+
let timer;
|
|
859
|
+
const failAttempt = (error) => {
|
|
860
|
+
if (settled)
|
|
861
|
+
return;
|
|
862
|
+
settled = true;
|
|
863
|
+
if (timer)
|
|
864
|
+
clearTimeout(timer);
|
|
865
|
+
this.discardPendingSocket();
|
|
866
|
+
reject(error);
|
|
867
|
+
};
|
|
868
|
+
const succeed = (begin) => {
|
|
869
|
+
if (settled)
|
|
870
|
+
return;
|
|
871
|
+
settled = true;
|
|
872
|
+
if (timer)
|
|
873
|
+
clearTimeout(timer);
|
|
874
|
+
resolve(begin);
|
|
875
|
+
};
|
|
876
|
+
if (timeoutMs > 0) {
|
|
877
|
+
timer = setTimeout(() => {
|
|
878
|
+
const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
|
|
879
|
+
err.retryable = true;
|
|
880
|
+
failAttempt(err);
|
|
881
|
+
}, timeoutMs);
|
|
882
|
+
}
|
|
792
883
|
if (this.token) {
|
|
793
884
|
this.socket = factory(url.toString());
|
|
794
885
|
}
|
|
@@ -806,6 +897,15 @@ class StreamingTranscriber {
|
|
|
806
897
|
reason = StreamingErrorMessages[code];
|
|
807
898
|
}
|
|
808
899
|
}
|
|
900
|
+
// A close before `Begin` is a failed connection attempt — reject so
|
|
901
|
+
// connect() can retry (or surface a permanent failure).
|
|
902
|
+
if (!settled) {
|
|
903
|
+
const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
|
|
904
|
+
err.code = code;
|
|
905
|
+
err.retryable = isRetryableCloseCode(code);
|
|
906
|
+
failAttempt(err);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
809
909
|
// Stop the flush timer when the socket is gone (server-initiated close,
|
|
810
910
|
// network drop, etc.) — otherwise subsequent ticks call send() on a
|
|
811
911
|
// closed socket and spam the error listener.
|
|
@@ -816,11 +916,15 @@ class StreamingTranscriber {
|
|
|
816
916
|
(_b = (_a = this.listeners).close) === null || _b === void 0 ? void 0 : _b.call(_a, code, reason);
|
|
817
917
|
};
|
|
818
918
|
this.socket.onerror = (event) => {
|
|
819
|
-
var _a, _b, _c
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
919
|
+
var _a, _b, _c;
|
|
920
|
+
const error = (_a = event.error) !== null && _a !== void 0 ? _a : new Error(event.message);
|
|
921
|
+
// A socket error before `Begin` is a failed attempt → reject/retry.
|
|
922
|
+
if (!settled) {
|
|
923
|
+
error.retryable = true;
|
|
924
|
+
failAttempt(error);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
(_c = (_b = this.listeners).error) === null || _c === void 0 ? void 0 : _c.call(_b, error);
|
|
824
928
|
};
|
|
825
929
|
this.socket.onmessage = ({ data }) => {
|
|
826
930
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
@@ -828,15 +932,23 @@ class StreamingTranscriber {
|
|
|
828
932
|
if ("error" in message) {
|
|
829
933
|
const err = new StreamingError(message.error);
|
|
830
934
|
if ("error_code" in message) {
|
|
831
|
-
err.code =
|
|
832
|
-
|
|
935
|
+
err.code = message.error_code;
|
|
936
|
+
}
|
|
937
|
+
// A server error frame before `Begin` fails the attempt; the code
|
|
938
|
+
// decides whether a retry is worthwhile.
|
|
939
|
+
if (!settled) {
|
|
940
|
+
const attemptErr = err;
|
|
941
|
+
attemptErr.retryable =
|
|
942
|
+
err.code === undefined ? true : isRetryableCloseCode(err.code);
|
|
943
|
+
failAttempt(attemptErr);
|
|
944
|
+
return;
|
|
833
945
|
}
|
|
834
946
|
(_b = (_a = this.listeners).error) === null || _b === void 0 ? void 0 : _b.call(_a, err);
|
|
835
947
|
return;
|
|
836
948
|
}
|
|
837
949
|
switch (message.type) {
|
|
838
950
|
case "Begin": {
|
|
839
|
-
|
|
951
|
+
succeed(message);
|
|
840
952
|
(_d = (_c = this.listeners).open) === null || _d === void 0 ? void 0 : _d.call(_c, message);
|
|
841
953
|
break;
|
|
842
954
|
}
|
|
@@ -883,6 +995,20 @@ class StreamingTranscriber {
|
|
|
883
995
|
};
|
|
884
996
|
});
|
|
885
997
|
}
|
|
998
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
999
|
+
discardPendingSocket() {
|
|
1000
|
+
if (!this.socket)
|
|
1001
|
+
return;
|
|
1002
|
+
try {
|
|
1003
|
+
if (this.socket.removeAllListeners)
|
|
1004
|
+
this.socket.removeAllListeners();
|
|
1005
|
+
this.socket.close();
|
|
1006
|
+
}
|
|
1007
|
+
catch (_a) {
|
|
1008
|
+
// Best-effort cleanup; a half-open socket may throw on close.
|
|
1009
|
+
}
|
|
1010
|
+
this.socket = undefined;
|
|
1011
|
+
}
|
|
886
1012
|
/**
|
|
887
1013
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
888
1014
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -1156,6 +1282,16 @@ class StreamingTranscriber {
|
|
|
1156
1282
|
};
|
|
1157
1283
|
this.send(JSON.stringify(message));
|
|
1158
1284
|
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
1287
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
1288
|
+
*/
|
|
1289
|
+
keepAlive() {
|
|
1290
|
+
const message = {
|
|
1291
|
+
type: "KeepAlive",
|
|
1292
|
+
};
|
|
1293
|
+
this.send(JSON.stringify(message));
|
|
1294
|
+
}
|
|
1159
1295
|
send(data) {
|
|
1160
1296
|
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
|
|
1161
1297
|
throw new Error("Socket is not open for communication");
|