assemblyai 4.34.6 → 4.35.3
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 +16 -0
- package/README.md +7 -8
- package/dist/assemblyai.streaming.umd.js +150 -10
- package/dist/assemblyai.streaming.umd.min.js +1 -1
- package/dist/assemblyai.umd.js +150 -10
- package/dist/assemblyai.umd.min.js +1 -1
- package/dist/browser.mjs +148 -12
- package/dist/bun.mjs +148 -12
- package/dist/deno.mjs +148 -12
- package/dist/exports/streaming.d.ts +1 -1
- package/dist/index.cjs +150 -10
- package/dist/index.mjs +150 -10
- package/dist/node.cjs +148 -12
- package/dist/node.mjs +148 -12
- package/dist/services/streaming/service.d.ts +22 -0
- package/dist/streaming.browser.mjs +148 -12
- package/dist/streaming.cjs +149 -9
- package/dist/streaming.mjs +149 -9
- package/dist/types/asyncapi.generated.d.ts +1 -1
- package/dist/types/streaming/index.d.ts +38 -1
- package/dist/workerd.mjs +148 -12
- package/package.json +1 -1
- package/src/exports/streaming.ts +1 -1
- package/src/services/streaming/service.ts +177 -11
- package/src/types/asyncapi.generated.ts +1 -1
- package/src/types/streaming/index.ts +39 -0
package/dist/node.cjs
CHANGED
|
@@ -35,7 +35,7 @@ if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
|
35
35
|
defaultUserAgentString += navigator.userAgent;
|
|
36
36
|
}
|
|
37
37
|
const defaultUserAgent = {
|
|
38
|
-
sdk: { name: "JavaScript", version: "4.
|
|
38
|
+
sdk: { name: "JavaScript", version: "4.35.3" },
|
|
39
39
|
};
|
|
40
40
|
if (typeof process !== "undefined") {
|
|
41
41
|
if (process.versions.node && defaultUserAgentString.indexOf("Node") === -1) {
|
|
@@ -980,6 +980,24 @@ function toInt16View(audio) {
|
|
|
980
980
|
}
|
|
981
981
|
const defaultStreamingUrl$1 = "wss://streaming.assemblyai.com/v3/ws";
|
|
982
982
|
const terminateSessionMessage = `{"type":"Terminate"}`;
|
|
983
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1000;
|
|
984
|
+
const DEFAULT_MAX_CONNECTION_RETRIES = 2;
|
|
985
|
+
const DEFAULT_CONNECTION_RETRY_DELAY_MS = 500;
|
|
986
|
+
/**
|
|
987
|
+
* Close/error codes that signal a permanent client-side problem (auth,
|
|
988
|
+
* billing, malformed config). A retry would hit the same failure, so the
|
|
989
|
+
* connection is never retried on these.
|
|
990
|
+
*/
|
|
991
|
+
const NON_RETRYABLE_CLOSE_CODES = new Set([
|
|
992
|
+
StreamingErrorType.BadSampleRate,
|
|
993
|
+
StreamingErrorType.AuthFailed,
|
|
994
|
+
StreamingErrorType.InsufficientFunds,
|
|
995
|
+
StreamingErrorType.FreeTierUser,
|
|
996
|
+
StreamingErrorType.BadSchema,
|
|
997
|
+
]);
|
|
998
|
+
function isRetryableCloseCode(code) {
|
|
999
|
+
return code !== 1000 && !NON_RETRYABLE_CLOSE_CODES.has(code);
|
|
1000
|
+
}
|
|
983
1001
|
/**
|
|
984
1002
|
* Per-send chunk cap in milliseconds for the dual-channel mixer. The streaming
|
|
985
1003
|
* server rejects audio messages longer than 1000 ms (`Input Duration Error`).
|
|
@@ -1113,8 +1131,12 @@ class StreamingTranscriber {
|
|
|
1113
1131
|
searchParams.set("speech_model", this.params.speechModel.toString());
|
|
1114
1132
|
}
|
|
1115
1133
|
if (this.params.languageCode !== undefined) {
|
|
1134
|
+
console.warn("[Deprecation Warning] `languageCode` is deprecated and will be removed in a future release. Please use `languageCodes` instead.");
|
|
1116
1135
|
searchParams.set("language_code", this.params.languageCode);
|
|
1117
1136
|
}
|
|
1137
|
+
if (this.params.languageCodes !== undefined) {
|
|
1138
|
+
searchParams.set("language_codes", JSON.stringify(this.params.languageCodes));
|
|
1139
|
+
}
|
|
1118
1140
|
if (this.params.languageDetection !== undefined) {
|
|
1119
1141
|
searchParams.set("language_detection", this.params.languageDetection.toString());
|
|
1120
1142
|
}
|
|
@@ -1188,12 +1210,81 @@ class StreamingTranscriber {
|
|
|
1188
1210
|
on(event, listener) {
|
|
1189
1211
|
this.listeners[event] = listener;
|
|
1190
1212
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1213
|
+
/**
|
|
1214
|
+
* Open the streaming session.
|
|
1215
|
+
*
|
|
1216
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
1217
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
1218
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
1219
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
1220
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
1221
|
+
* funds, malformed config) are not retried.
|
|
1222
|
+
*
|
|
1223
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
1224
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
1225
|
+
* the retry loop) to observe the failure.
|
|
1226
|
+
*/
|
|
1227
|
+
async connect() {
|
|
1228
|
+
if (this.socket) {
|
|
1229
|
+
throw new Error("Already connected");
|
|
1230
|
+
}
|
|
1231
|
+
const maxRetries = this.params.maxConnectionRetries ?? DEFAULT_MAX_CONNECTION_RETRIES;
|
|
1232
|
+
const retryDelay = this.params.connectionRetryDelay ?? DEFAULT_CONNECTION_RETRY_DELAY_MS;
|
|
1233
|
+
let lastError;
|
|
1234
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1235
|
+
try {
|
|
1236
|
+
return await this.connectOnce();
|
|
1237
|
+
}
|
|
1238
|
+
catch (err) {
|
|
1239
|
+
lastError = err;
|
|
1240
|
+
const retryable = err.retryable === true;
|
|
1241
|
+
if (!retryable || attempt === maxRetries) {
|
|
1242
|
+
throw err;
|
|
1243
|
+
}
|
|
1244
|
+
console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
|
|
1245
|
+
if (retryDelay > 0) {
|
|
1246
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1247
|
+
}
|
|
1195
1248
|
}
|
|
1249
|
+
}
|
|
1250
|
+
// The loop above always returns or throws; this only satisfies the type
|
|
1251
|
+
// checker that a value is produced on every path.
|
|
1252
|
+
throw lastError ?? new Error("Failed to connect to streaming server");
|
|
1253
|
+
}
|
|
1254
|
+
connectOnce() {
|
|
1255
|
+
return new Promise((resolve, reject) => {
|
|
1196
1256
|
const url = this.connectionUrl();
|
|
1257
|
+
const timeoutMs = this.params.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
1258
|
+
// `settled` flips once this attempt has resolved (`Begin`) or rejected
|
|
1259
|
+
// (timeout / pre-`Begin` close / error). Before it flips the socket
|
|
1260
|
+
// handlers drive this promise; after it flips they revert to normal
|
|
1261
|
+
// runtime dispatch (close / error / message listeners).
|
|
1262
|
+
let settled = false;
|
|
1263
|
+
let timer;
|
|
1264
|
+
const failAttempt = (error) => {
|
|
1265
|
+
if (settled)
|
|
1266
|
+
return;
|
|
1267
|
+
settled = true;
|
|
1268
|
+
if (timer)
|
|
1269
|
+
clearTimeout(timer);
|
|
1270
|
+
this.discardPendingSocket();
|
|
1271
|
+
reject(error);
|
|
1272
|
+
};
|
|
1273
|
+
const succeed = (begin) => {
|
|
1274
|
+
if (settled)
|
|
1275
|
+
return;
|
|
1276
|
+
settled = true;
|
|
1277
|
+
if (timer)
|
|
1278
|
+
clearTimeout(timer);
|
|
1279
|
+
resolve(begin);
|
|
1280
|
+
};
|
|
1281
|
+
if (timeoutMs > 0) {
|
|
1282
|
+
timer = setTimeout(() => {
|
|
1283
|
+
const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
|
|
1284
|
+
err.retryable = true;
|
|
1285
|
+
failAttempt(err);
|
|
1286
|
+
}, timeoutMs);
|
|
1287
|
+
}
|
|
1197
1288
|
if (this.token) {
|
|
1198
1289
|
this.socket = factory(url.toString());
|
|
1199
1290
|
}
|
|
@@ -1210,6 +1301,15 @@ class StreamingTranscriber {
|
|
|
1210
1301
|
reason = StreamingErrorMessages[code];
|
|
1211
1302
|
}
|
|
1212
1303
|
}
|
|
1304
|
+
// A close before `Begin` is a failed connection attempt — reject so
|
|
1305
|
+
// connect() can retry (or surface a permanent failure).
|
|
1306
|
+
if (!settled) {
|
|
1307
|
+
const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
|
|
1308
|
+
err.code = code;
|
|
1309
|
+
err.retryable = isRetryableCloseCode(code);
|
|
1310
|
+
failAttempt(err);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1213
1313
|
// Stop the flush timer when the socket is gone (server-initiated close,
|
|
1214
1314
|
// network drop, etc.) — otherwise subsequent ticks call send() on a
|
|
1215
1315
|
// closed socket and spam the error listener.
|
|
@@ -1220,25 +1320,37 @@ class StreamingTranscriber {
|
|
|
1220
1320
|
this.listeners.close?.(code, reason);
|
|
1221
1321
|
};
|
|
1222
1322
|
this.socket.onerror = (event) => {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1323
|
+
const error = event.error ?? new Error(event.message);
|
|
1324
|
+
// A socket error before `Begin` is a failed attempt → reject/retry.
|
|
1325
|
+
if (!settled) {
|
|
1326
|
+
error.retryable = true;
|
|
1327
|
+
failAttempt(error);
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
this.listeners.error?.(error);
|
|
1227
1331
|
};
|
|
1228
1332
|
this.socket.onmessage = ({ data }) => {
|
|
1229
1333
|
const message = JSON.parse(data.toString());
|
|
1230
1334
|
if ("error" in message) {
|
|
1231
1335
|
const err = new StreamingError(message.error);
|
|
1232
1336
|
if ("error_code" in message) {
|
|
1233
|
-
err.code =
|
|
1234
|
-
|
|
1337
|
+
err.code = message.error_code;
|
|
1338
|
+
}
|
|
1339
|
+
// A server error frame before `Begin` fails the attempt; the code
|
|
1340
|
+
// decides whether a retry is worthwhile.
|
|
1341
|
+
if (!settled) {
|
|
1342
|
+
const attemptErr = err;
|
|
1343
|
+
attemptErr.retryable =
|
|
1344
|
+
err.code === undefined ? true : isRetryableCloseCode(err.code);
|
|
1345
|
+
failAttempt(attemptErr);
|
|
1346
|
+
return;
|
|
1235
1347
|
}
|
|
1236
1348
|
this.listeners.error?.(err);
|
|
1237
1349
|
return;
|
|
1238
1350
|
}
|
|
1239
1351
|
switch (message.type) {
|
|
1240
1352
|
case "Begin": {
|
|
1241
|
-
|
|
1353
|
+
succeed(message);
|
|
1242
1354
|
this.listeners.open?.(message);
|
|
1243
1355
|
break;
|
|
1244
1356
|
}
|
|
@@ -1285,6 +1397,20 @@ class StreamingTranscriber {
|
|
|
1285
1397
|
};
|
|
1286
1398
|
});
|
|
1287
1399
|
}
|
|
1400
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
1401
|
+
discardPendingSocket() {
|
|
1402
|
+
if (!this.socket)
|
|
1403
|
+
return;
|
|
1404
|
+
try {
|
|
1405
|
+
if (this.socket.removeAllListeners)
|
|
1406
|
+
this.socket.removeAllListeners();
|
|
1407
|
+
this.socket.close();
|
|
1408
|
+
}
|
|
1409
|
+
catch {
|
|
1410
|
+
// Best-effort cleanup; a half-open socket may throw on close.
|
|
1411
|
+
}
|
|
1412
|
+
this.socket = undefined;
|
|
1413
|
+
}
|
|
1288
1414
|
/**
|
|
1289
1415
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
1290
1416
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -1558,6 +1684,16 @@ class StreamingTranscriber {
|
|
|
1558
1684
|
};
|
|
1559
1685
|
this.send(JSON.stringify(message));
|
|
1560
1686
|
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
1689
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
1690
|
+
*/
|
|
1691
|
+
keepAlive() {
|
|
1692
|
+
const message = {
|
|
1693
|
+
type: "KeepAlive",
|
|
1694
|
+
};
|
|
1695
|
+
this.send(JSON.stringify(message));
|
|
1696
|
+
}
|
|
1561
1697
|
send(data) {
|
|
1562
1698
|
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
|
|
1563
1699
|
throw new Error("Socket is not open for communication");
|
package/dist/node.mjs
CHANGED
|
@@ -33,7 +33,7 @@ if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
|
33
33
|
defaultUserAgentString += navigator.userAgent;
|
|
34
34
|
}
|
|
35
35
|
const defaultUserAgent = {
|
|
36
|
-
sdk: { name: "JavaScript", version: "4.
|
|
36
|
+
sdk: { name: "JavaScript", version: "4.35.3" },
|
|
37
37
|
};
|
|
38
38
|
if (typeof process !== "undefined") {
|
|
39
39
|
if (process.versions.node && defaultUserAgentString.indexOf("Node") === -1) {
|
|
@@ -978,6 +978,24 @@ function toInt16View(audio) {
|
|
|
978
978
|
}
|
|
979
979
|
const defaultStreamingUrl$1 = "wss://streaming.assemblyai.com/v3/ws";
|
|
980
980
|
const terminateSessionMessage = `{"type":"Terminate"}`;
|
|
981
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1000;
|
|
982
|
+
const DEFAULT_MAX_CONNECTION_RETRIES = 2;
|
|
983
|
+
const DEFAULT_CONNECTION_RETRY_DELAY_MS = 500;
|
|
984
|
+
/**
|
|
985
|
+
* Close/error codes that signal a permanent client-side problem (auth,
|
|
986
|
+
* billing, malformed config). A retry would hit the same failure, so the
|
|
987
|
+
* connection is never retried on these.
|
|
988
|
+
*/
|
|
989
|
+
const NON_RETRYABLE_CLOSE_CODES = new Set([
|
|
990
|
+
StreamingErrorType.BadSampleRate,
|
|
991
|
+
StreamingErrorType.AuthFailed,
|
|
992
|
+
StreamingErrorType.InsufficientFunds,
|
|
993
|
+
StreamingErrorType.FreeTierUser,
|
|
994
|
+
StreamingErrorType.BadSchema,
|
|
995
|
+
]);
|
|
996
|
+
function isRetryableCloseCode(code) {
|
|
997
|
+
return code !== 1000 && !NON_RETRYABLE_CLOSE_CODES.has(code);
|
|
998
|
+
}
|
|
981
999
|
/**
|
|
982
1000
|
* Per-send chunk cap in milliseconds for the dual-channel mixer. The streaming
|
|
983
1001
|
* server rejects audio messages longer than 1000 ms (`Input Duration Error`).
|
|
@@ -1111,8 +1129,12 @@ class StreamingTranscriber {
|
|
|
1111
1129
|
searchParams.set("speech_model", this.params.speechModel.toString());
|
|
1112
1130
|
}
|
|
1113
1131
|
if (this.params.languageCode !== undefined) {
|
|
1132
|
+
console.warn("[Deprecation Warning] `languageCode` is deprecated and will be removed in a future release. Please use `languageCodes` instead.");
|
|
1114
1133
|
searchParams.set("language_code", this.params.languageCode);
|
|
1115
1134
|
}
|
|
1135
|
+
if (this.params.languageCodes !== undefined) {
|
|
1136
|
+
searchParams.set("language_codes", JSON.stringify(this.params.languageCodes));
|
|
1137
|
+
}
|
|
1116
1138
|
if (this.params.languageDetection !== undefined) {
|
|
1117
1139
|
searchParams.set("language_detection", this.params.languageDetection.toString());
|
|
1118
1140
|
}
|
|
@@ -1186,12 +1208,81 @@ class StreamingTranscriber {
|
|
|
1186
1208
|
on(event, listener) {
|
|
1187
1209
|
this.listeners[event] = listener;
|
|
1188
1210
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1211
|
+
/**
|
|
1212
|
+
* Open the streaming session.
|
|
1213
|
+
*
|
|
1214
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
1215
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
1216
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
1217
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
1218
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
1219
|
+
* funds, malformed config) are not retried.
|
|
1220
|
+
*
|
|
1221
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
1222
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
1223
|
+
* the retry loop) to observe the failure.
|
|
1224
|
+
*/
|
|
1225
|
+
async connect() {
|
|
1226
|
+
if (this.socket) {
|
|
1227
|
+
throw new Error("Already connected");
|
|
1228
|
+
}
|
|
1229
|
+
const maxRetries = this.params.maxConnectionRetries ?? DEFAULT_MAX_CONNECTION_RETRIES;
|
|
1230
|
+
const retryDelay = this.params.connectionRetryDelay ?? DEFAULT_CONNECTION_RETRY_DELAY_MS;
|
|
1231
|
+
let lastError;
|
|
1232
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1233
|
+
try {
|
|
1234
|
+
return await this.connectOnce();
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
lastError = err;
|
|
1238
|
+
const retryable = err.retryable === true;
|
|
1239
|
+
if (!retryable || attempt === maxRetries) {
|
|
1240
|
+
throw err;
|
|
1241
|
+
}
|
|
1242
|
+
console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
|
|
1243
|
+
if (retryDelay > 0) {
|
|
1244
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1245
|
+
}
|
|
1193
1246
|
}
|
|
1247
|
+
}
|
|
1248
|
+
// The loop above always returns or throws; this only satisfies the type
|
|
1249
|
+
// checker that a value is produced on every path.
|
|
1250
|
+
throw lastError ?? new Error("Failed to connect to streaming server");
|
|
1251
|
+
}
|
|
1252
|
+
connectOnce() {
|
|
1253
|
+
return new Promise((resolve, reject) => {
|
|
1194
1254
|
const url = this.connectionUrl();
|
|
1255
|
+
const timeoutMs = this.params.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
1256
|
+
// `settled` flips once this attempt has resolved (`Begin`) or rejected
|
|
1257
|
+
// (timeout / pre-`Begin` close / error). Before it flips the socket
|
|
1258
|
+
// handlers drive this promise; after it flips they revert to normal
|
|
1259
|
+
// runtime dispatch (close / error / message listeners).
|
|
1260
|
+
let settled = false;
|
|
1261
|
+
let timer;
|
|
1262
|
+
const failAttempt = (error) => {
|
|
1263
|
+
if (settled)
|
|
1264
|
+
return;
|
|
1265
|
+
settled = true;
|
|
1266
|
+
if (timer)
|
|
1267
|
+
clearTimeout(timer);
|
|
1268
|
+
this.discardPendingSocket();
|
|
1269
|
+
reject(error);
|
|
1270
|
+
};
|
|
1271
|
+
const succeed = (begin) => {
|
|
1272
|
+
if (settled)
|
|
1273
|
+
return;
|
|
1274
|
+
settled = true;
|
|
1275
|
+
if (timer)
|
|
1276
|
+
clearTimeout(timer);
|
|
1277
|
+
resolve(begin);
|
|
1278
|
+
};
|
|
1279
|
+
if (timeoutMs > 0) {
|
|
1280
|
+
timer = setTimeout(() => {
|
|
1281
|
+
const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
|
|
1282
|
+
err.retryable = true;
|
|
1283
|
+
failAttempt(err);
|
|
1284
|
+
}, timeoutMs);
|
|
1285
|
+
}
|
|
1195
1286
|
if (this.token) {
|
|
1196
1287
|
this.socket = factory(url.toString());
|
|
1197
1288
|
}
|
|
@@ -1208,6 +1299,15 @@ class StreamingTranscriber {
|
|
|
1208
1299
|
reason = StreamingErrorMessages[code];
|
|
1209
1300
|
}
|
|
1210
1301
|
}
|
|
1302
|
+
// A close before `Begin` is a failed connection attempt — reject so
|
|
1303
|
+
// connect() can retry (or surface a permanent failure).
|
|
1304
|
+
if (!settled) {
|
|
1305
|
+
const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
|
|
1306
|
+
err.code = code;
|
|
1307
|
+
err.retryable = isRetryableCloseCode(code);
|
|
1308
|
+
failAttempt(err);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1211
1311
|
// Stop the flush timer when the socket is gone (server-initiated close,
|
|
1212
1312
|
// network drop, etc.) — otherwise subsequent ticks call send() on a
|
|
1213
1313
|
// closed socket and spam the error listener.
|
|
@@ -1218,25 +1318,37 @@ class StreamingTranscriber {
|
|
|
1218
1318
|
this.listeners.close?.(code, reason);
|
|
1219
1319
|
};
|
|
1220
1320
|
this.socket.onerror = (event) => {
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1321
|
+
const error = event.error ?? new Error(event.message);
|
|
1322
|
+
// A socket error before `Begin` is a failed attempt → reject/retry.
|
|
1323
|
+
if (!settled) {
|
|
1324
|
+
error.retryable = true;
|
|
1325
|
+
failAttempt(error);
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
this.listeners.error?.(error);
|
|
1225
1329
|
};
|
|
1226
1330
|
this.socket.onmessage = ({ data }) => {
|
|
1227
1331
|
const message = JSON.parse(data.toString());
|
|
1228
1332
|
if ("error" in message) {
|
|
1229
1333
|
const err = new StreamingError(message.error);
|
|
1230
1334
|
if ("error_code" in message) {
|
|
1231
|
-
err.code =
|
|
1232
|
-
|
|
1335
|
+
err.code = message.error_code;
|
|
1336
|
+
}
|
|
1337
|
+
// A server error frame before `Begin` fails the attempt; the code
|
|
1338
|
+
// decides whether a retry is worthwhile.
|
|
1339
|
+
if (!settled) {
|
|
1340
|
+
const attemptErr = err;
|
|
1341
|
+
attemptErr.retryable =
|
|
1342
|
+
err.code === undefined ? true : isRetryableCloseCode(err.code);
|
|
1343
|
+
failAttempt(attemptErr);
|
|
1344
|
+
return;
|
|
1233
1345
|
}
|
|
1234
1346
|
this.listeners.error?.(err);
|
|
1235
1347
|
return;
|
|
1236
1348
|
}
|
|
1237
1349
|
switch (message.type) {
|
|
1238
1350
|
case "Begin": {
|
|
1239
|
-
|
|
1351
|
+
succeed(message);
|
|
1240
1352
|
this.listeners.open?.(message);
|
|
1241
1353
|
break;
|
|
1242
1354
|
}
|
|
@@ -1283,6 +1395,20 @@ class StreamingTranscriber {
|
|
|
1283
1395
|
};
|
|
1284
1396
|
});
|
|
1285
1397
|
}
|
|
1398
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
1399
|
+
discardPendingSocket() {
|
|
1400
|
+
if (!this.socket)
|
|
1401
|
+
return;
|
|
1402
|
+
try {
|
|
1403
|
+
if (this.socket.removeAllListeners)
|
|
1404
|
+
this.socket.removeAllListeners();
|
|
1405
|
+
this.socket.close();
|
|
1406
|
+
}
|
|
1407
|
+
catch {
|
|
1408
|
+
// Best-effort cleanup; a half-open socket may throw on close.
|
|
1409
|
+
}
|
|
1410
|
+
this.socket = undefined;
|
|
1411
|
+
}
|
|
1286
1412
|
/**
|
|
1287
1413
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
1288
1414
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -1556,6 +1682,16 @@ class StreamingTranscriber {
|
|
|
1556
1682
|
};
|
|
1557
1683
|
this.send(JSON.stringify(message));
|
|
1558
1684
|
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
1687
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
1688
|
+
*/
|
|
1689
|
+
keepAlive() {
|
|
1690
|
+
const message = {
|
|
1691
|
+
type: "KeepAlive",
|
|
1692
|
+
};
|
|
1693
|
+
this.send(JSON.stringify(message));
|
|
1694
|
+
}
|
|
1559
1695
|
send(data) {
|
|
1560
1696
|
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
|
|
1561
1697
|
throw new Error("Socket is not open for communication");
|
|
@@ -39,7 +39,24 @@ export declare class StreamingTranscriber {
|
|
|
39
39
|
on(event: "vad", listener: (event: VadFrame) => void): void;
|
|
40
40
|
on(event: "error", listener: (error: Error) => void): void;
|
|
41
41
|
on(event: "close", listener: (code: number, reason: string) => void): void;
|
|
42
|
+
/**
|
|
43
|
+
* Open the streaming session.
|
|
44
|
+
*
|
|
45
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
46
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
47
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
48
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
49
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
50
|
+
* funds, malformed config) are not retried.
|
|
51
|
+
*
|
|
52
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
53
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
54
|
+
* the retry loop) to observe the failure.
|
|
55
|
+
*/
|
|
42
56
|
connect(): Promise<BeginEvent>;
|
|
57
|
+
private connectOnce;
|
|
58
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
59
|
+
private discardPendingSocket;
|
|
43
60
|
/**
|
|
44
61
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
45
62
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -95,6 +112,11 @@ export declare class StreamingTranscriber {
|
|
|
95
112
|
* Force the current turn to end immediately.
|
|
96
113
|
*/
|
|
97
114
|
forceEndpoint(): void;
|
|
115
|
+
/**
|
|
116
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
117
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
118
|
+
*/
|
|
119
|
+
keepAlive(): void;
|
|
98
120
|
private send;
|
|
99
121
|
close(waitForSessionTermination?: boolean): Promise<void>;
|
|
100
122
|
}
|