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
package/dist/deno.mjs
CHANGED
|
@@ -30,7 +30,7 @@ if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
|
30
30
|
defaultUserAgentString += navigator.userAgent;
|
|
31
31
|
}
|
|
32
32
|
const defaultUserAgent = {
|
|
33
|
-
sdk: { name: "JavaScript", version: "4.
|
|
33
|
+
sdk: { name: "JavaScript", version: "4.35.0" },
|
|
34
34
|
};
|
|
35
35
|
if (typeof process !== "undefined") {
|
|
36
36
|
if (process.versions.node && defaultUserAgentString.indexOf("Node") === -1) {
|
|
@@ -981,6 +981,24 @@ function toInt16View(audio) {
|
|
|
981
981
|
}
|
|
982
982
|
const defaultStreamingUrl$1 = "wss://streaming.assemblyai.com/v3/ws";
|
|
983
983
|
const terminateSessionMessage = `{"type":"Terminate"}`;
|
|
984
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1000;
|
|
985
|
+
const DEFAULT_MAX_CONNECTION_RETRIES = 2;
|
|
986
|
+
const DEFAULT_CONNECTION_RETRY_DELAY_MS = 500;
|
|
987
|
+
/**
|
|
988
|
+
* Close/error codes that signal a permanent client-side problem (auth,
|
|
989
|
+
* billing, malformed config). A retry would hit the same failure, so the
|
|
990
|
+
* connection is never retried on these.
|
|
991
|
+
*/
|
|
992
|
+
const NON_RETRYABLE_CLOSE_CODES = new Set([
|
|
993
|
+
StreamingErrorType.BadSampleRate,
|
|
994
|
+
StreamingErrorType.AuthFailed,
|
|
995
|
+
StreamingErrorType.InsufficientFunds,
|
|
996
|
+
StreamingErrorType.FreeTierUser,
|
|
997
|
+
StreamingErrorType.BadSchema,
|
|
998
|
+
]);
|
|
999
|
+
function isRetryableCloseCode(code) {
|
|
1000
|
+
return code !== 1000 && !NON_RETRYABLE_CLOSE_CODES.has(code);
|
|
1001
|
+
}
|
|
984
1002
|
/**
|
|
985
1003
|
* Per-send chunk cap in milliseconds for the dual-channel mixer. The streaming
|
|
986
1004
|
* server rejects audio messages longer than 1000 ms (`Input Duration Error`).
|
|
@@ -1189,12 +1207,81 @@ class StreamingTranscriber {
|
|
|
1189
1207
|
on(event, listener) {
|
|
1190
1208
|
this.listeners[event] = listener;
|
|
1191
1209
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1210
|
+
/**
|
|
1211
|
+
* Open the streaming session.
|
|
1212
|
+
*
|
|
1213
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
1214
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
1215
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
1216
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
1217
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
1218
|
+
* funds, malformed config) are not retried.
|
|
1219
|
+
*
|
|
1220
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
1221
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
1222
|
+
* the retry loop) to observe the failure.
|
|
1223
|
+
*/
|
|
1224
|
+
async connect() {
|
|
1225
|
+
if (this.socket) {
|
|
1226
|
+
throw new Error("Already connected");
|
|
1227
|
+
}
|
|
1228
|
+
const maxRetries = this.params.maxConnectionRetries ?? DEFAULT_MAX_CONNECTION_RETRIES;
|
|
1229
|
+
const retryDelay = this.params.connectionRetryDelay ?? DEFAULT_CONNECTION_RETRY_DELAY_MS;
|
|
1230
|
+
let lastError;
|
|
1231
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1232
|
+
try {
|
|
1233
|
+
return await this.connectOnce();
|
|
1234
|
+
}
|
|
1235
|
+
catch (err) {
|
|
1236
|
+
lastError = err;
|
|
1237
|
+
const retryable = err.retryable === true;
|
|
1238
|
+
if (!retryable || attempt === maxRetries) {
|
|
1239
|
+
throw err;
|
|
1240
|
+
}
|
|
1241
|
+
console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
|
|
1242
|
+
if (retryDelay > 0) {
|
|
1243
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1244
|
+
}
|
|
1196
1245
|
}
|
|
1246
|
+
}
|
|
1247
|
+
// The loop above always returns or throws; this only satisfies the type
|
|
1248
|
+
// checker that a value is produced on every path.
|
|
1249
|
+
throw lastError ?? new Error("Failed to connect to streaming server");
|
|
1250
|
+
}
|
|
1251
|
+
connectOnce() {
|
|
1252
|
+
return new Promise((resolve, reject) => {
|
|
1197
1253
|
const url = this.connectionUrl();
|
|
1254
|
+
const timeoutMs = this.params.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
1255
|
+
// `settled` flips once this attempt has resolved (`Begin`) or rejected
|
|
1256
|
+
// (timeout / pre-`Begin` close / error). Before it flips the socket
|
|
1257
|
+
// handlers drive this promise; after it flips they revert to normal
|
|
1258
|
+
// runtime dispatch (close / error / message listeners).
|
|
1259
|
+
let settled = false;
|
|
1260
|
+
let timer;
|
|
1261
|
+
const failAttempt = (error) => {
|
|
1262
|
+
if (settled)
|
|
1263
|
+
return;
|
|
1264
|
+
settled = true;
|
|
1265
|
+
if (timer)
|
|
1266
|
+
clearTimeout(timer);
|
|
1267
|
+
this.discardPendingSocket();
|
|
1268
|
+
reject(error);
|
|
1269
|
+
};
|
|
1270
|
+
const succeed = (begin) => {
|
|
1271
|
+
if (settled)
|
|
1272
|
+
return;
|
|
1273
|
+
settled = true;
|
|
1274
|
+
if (timer)
|
|
1275
|
+
clearTimeout(timer);
|
|
1276
|
+
resolve(begin);
|
|
1277
|
+
};
|
|
1278
|
+
if (timeoutMs > 0) {
|
|
1279
|
+
timer = setTimeout(() => {
|
|
1280
|
+
const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
|
|
1281
|
+
err.retryable = true;
|
|
1282
|
+
failAttempt(err);
|
|
1283
|
+
}, timeoutMs);
|
|
1284
|
+
}
|
|
1198
1285
|
if (this.token) {
|
|
1199
1286
|
this.socket = factory(url.toString());
|
|
1200
1287
|
}
|
|
@@ -1211,6 +1298,15 @@ class StreamingTranscriber {
|
|
|
1211
1298
|
reason = StreamingErrorMessages[code];
|
|
1212
1299
|
}
|
|
1213
1300
|
}
|
|
1301
|
+
// A close before `Begin` is a failed connection attempt — reject so
|
|
1302
|
+
// connect() can retry (or surface a permanent failure).
|
|
1303
|
+
if (!settled) {
|
|
1304
|
+
const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
|
|
1305
|
+
err.code = code;
|
|
1306
|
+
err.retryable = isRetryableCloseCode(code);
|
|
1307
|
+
failAttempt(err);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1214
1310
|
// Stop the flush timer when the socket is gone (server-initiated close,
|
|
1215
1311
|
// network drop, etc.) — otherwise subsequent ticks call send() on a
|
|
1216
1312
|
// closed socket and spam the error listener.
|
|
@@ -1221,25 +1317,37 @@ class StreamingTranscriber {
|
|
|
1221
1317
|
this.listeners.close?.(code, reason);
|
|
1222
1318
|
};
|
|
1223
1319
|
this.socket.onerror = (event) => {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1320
|
+
const error = event.error ?? new Error(event.message);
|
|
1321
|
+
// A socket error before `Begin` is a failed attempt → reject/retry.
|
|
1322
|
+
if (!settled) {
|
|
1323
|
+
error.retryable = true;
|
|
1324
|
+
failAttempt(error);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
this.listeners.error?.(error);
|
|
1228
1328
|
};
|
|
1229
1329
|
this.socket.onmessage = ({ data }) => {
|
|
1230
1330
|
const message = JSON.parse(data.toString());
|
|
1231
1331
|
if ("error" in message) {
|
|
1232
1332
|
const err = new StreamingError(message.error);
|
|
1233
1333
|
if ("error_code" in message) {
|
|
1234
|
-
err.code =
|
|
1235
|
-
|
|
1334
|
+
err.code = message.error_code;
|
|
1335
|
+
}
|
|
1336
|
+
// A server error frame before `Begin` fails the attempt; the code
|
|
1337
|
+
// decides whether a retry is worthwhile.
|
|
1338
|
+
if (!settled) {
|
|
1339
|
+
const attemptErr = err;
|
|
1340
|
+
attemptErr.retryable =
|
|
1341
|
+
err.code === undefined ? true : isRetryableCloseCode(err.code);
|
|
1342
|
+
failAttempt(attemptErr);
|
|
1343
|
+
return;
|
|
1236
1344
|
}
|
|
1237
1345
|
this.listeners.error?.(err);
|
|
1238
1346
|
return;
|
|
1239
1347
|
}
|
|
1240
1348
|
switch (message.type) {
|
|
1241
1349
|
case "Begin": {
|
|
1242
|
-
|
|
1350
|
+
succeed(message);
|
|
1243
1351
|
this.listeners.open?.(message);
|
|
1244
1352
|
break;
|
|
1245
1353
|
}
|
|
@@ -1286,6 +1394,20 @@ class StreamingTranscriber {
|
|
|
1286
1394
|
};
|
|
1287
1395
|
});
|
|
1288
1396
|
}
|
|
1397
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
1398
|
+
discardPendingSocket() {
|
|
1399
|
+
if (!this.socket)
|
|
1400
|
+
return;
|
|
1401
|
+
try {
|
|
1402
|
+
if (this.socket.removeAllListeners)
|
|
1403
|
+
this.socket.removeAllListeners();
|
|
1404
|
+
this.socket.close();
|
|
1405
|
+
}
|
|
1406
|
+
catch {
|
|
1407
|
+
// Best-effort cleanup; a half-open socket may throw on close.
|
|
1408
|
+
}
|
|
1409
|
+
this.socket = undefined;
|
|
1410
|
+
}
|
|
1289
1411
|
/**
|
|
1290
1412
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
1291
1413
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -1559,6 +1681,16 @@ class StreamingTranscriber {
|
|
|
1559
1681
|
};
|
|
1560
1682
|
this.send(JSON.stringify(message));
|
|
1561
1683
|
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
1686
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
1687
|
+
*/
|
|
1688
|
+
keepAlive() {
|
|
1689
|
+
const message = {
|
|
1690
|
+
type: "KeepAlive",
|
|
1691
|
+
};
|
|
1692
|
+
this.send(JSON.stringify(message));
|
|
1693
|
+
}
|
|
1562
1694
|
send(data) {
|
|
1563
1695
|
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
|
|
1564
1696
|
throw new Error("Socket is not open for communication");
|
package/dist/index.cjs
CHANGED
|
@@ -76,7 +76,7 @@ if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
|
76
76
|
defaultUserAgentString += navigator.userAgent;
|
|
77
77
|
}
|
|
78
78
|
const defaultUserAgent = {
|
|
79
|
-
sdk: { name: "JavaScript", version: "4.
|
|
79
|
+
sdk: { name: "JavaScript", version: "4.35.0" },
|
|
80
80
|
};
|
|
81
81
|
if (typeof process !== "undefined") {
|
|
82
82
|
if (process.versions.node && defaultUserAgentString.indexOf("Node") === -1) {
|
|
@@ -1077,6 +1077,24 @@ function toInt16View(audio) {
|
|
|
1077
1077
|
}
|
|
1078
1078
|
const defaultStreamingUrl$1 = "wss://streaming.assemblyai.com/v3/ws";
|
|
1079
1079
|
const terminateSessionMessage = `{"type":"Terminate"}`;
|
|
1080
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1000;
|
|
1081
|
+
const DEFAULT_MAX_CONNECTION_RETRIES = 2;
|
|
1082
|
+
const DEFAULT_CONNECTION_RETRY_DELAY_MS = 500;
|
|
1083
|
+
/**
|
|
1084
|
+
* Close/error codes that signal a permanent client-side problem (auth,
|
|
1085
|
+
* billing, malformed config). A retry would hit the same failure, so the
|
|
1086
|
+
* connection is never retried on these.
|
|
1087
|
+
*/
|
|
1088
|
+
const NON_RETRYABLE_CLOSE_CODES = new Set([
|
|
1089
|
+
StreamingErrorType.BadSampleRate,
|
|
1090
|
+
StreamingErrorType.AuthFailed,
|
|
1091
|
+
StreamingErrorType.InsufficientFunds,
|
|
1092
|
+
StreamingErrorType.FreeTierUser,
|
|
1093
|
+
StreamingErrorType.BadSchema,
|
|
1094
|
+
]);
|
|
1095
|
+
function isRetryableCloseCode(code) {
|
|
1096
|
+
return code !== 1000 && !NON_RETRYABLE_CLOSE_CODES.has(code);
|
|
1097
|
+
}
|
|
1080
1098
|
/**
|
|
1081
1099
|
* Per-send chunk cap in milliseconds for the dual-channel mixer. The streaming
|
|
1082
1100
|
* server rejects audio messages longer than 1000 ms (`Input Duration Error`).
|
|
@@ -1283,12 +1301,85 @@ class StreamingTranscriber {
|
|
|
1283
1301
|
on(event, listener) {
|
|
1284
1302
|
this.listeners[event] = listener;
|
|
1285
1303
|
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Open the streaming session.
|
|
1306
|
+
*
|
|
1307
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
1308
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
1309
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
1310
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
1311
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
1312
|
+
* funds, malformed config) are not retried.
|
|
1313
|
+
*
|
|
1314
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
1315
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
1316
|
+
* the retry loop) to observe the failure.
|
|
1317
|
+
*/
|
|
1286
1318
|
connect() {
|
|
1287
|
-
return
|
|
1319
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1320
|
+
var _a, _b;
|
|
1288
1321
|
if (this.socket) {
|
|
1289
1322
|
throw new Error("Already connected");
|
|
1290
1323
|
}
|
|
1324
|
+
const maxRetries = (_a = this.params.maxConnectionRetries) !== null && _a !== void 0 ? _a : DEFAULT_MAX_CONNECTION_RETRIES;
|
|
1325
|
+
const retryDelay = (_b = this.params.connectionRetryDelay) !== null && _b !== void 0 ? _b : DEFAULT_CONNECTION_RETRY_DELAY_MS;
|
|
1326
|
+
let lastError;
|
|
1327
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1328
|
+
try {
|
|
1329
|
+
return yield this.connectOnce();
|
|
1330
|
+
}
|
|
1331
|
+
catch (err) {
|
|
1332
|
+
lastError = err;
|
|
1333
|
+
const retryable = err.retryable === true;
|
|
1334
|
+
if (!retryable || attempt === maxRetries) {
|
|
1335
|
+
throw err;
|
|
1336
|
+
}
|
|
1337
|
+
console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
|
|
1338
|
+
if (retryDelay > 0) {
|
|
1339
|
+
yield new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
// The loop above always returns or throws; this only satisfies the type
|
|
1344
|
+
// checker that a value is produced on every path.
|
|
1345
|
+
throw lastError !== null && lastError !== void 0 ? lastError : new Error("Failed to connect to streaming server");
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
connectOnce() {
|
|
1349
|
+
return new Promise((resolve, reject) => {
|
|
1350
|
+
var _a;
|
|
1291
1351
|
const url = this.connectionUrl();
|
|
1352
|
+
const timeoutMs = (_a = this.params.connectTimeout) !== null && _a !== void 0 ? _a : DEFAULT_CONNECT_TIMEOUT_MS;
|
|
1353
|
+
// `settled` flips once this attempt has resolved (`Begin`) or rejected
|
|
1354
|
+
// (timeout / pre-`Begin` close / error). Before it flips the socket
|
|
1355
|
+
// handlers drive this promise; after it flips they revert to normal
|
|
1356
|
+
// runtime dispatch (close / error / message listeners).
|
|
1357
|
+
let settled = false;
|
|
1358
|
+
let timer;
|
|
1359
|
+
const failAttempt = (error) => {
|
|
1360
|
+
if (settled)
|
|
1361
|
+
return;
|
|
1362
|
+
settled = true;
|
|
1363
|
+
if (timer)
|
|
1364
|
+
clearTimeout(timer);
|
|
1365
|
+
this.discardPendingSocket();
|
|
1366
|
+
reject(error);
|
|
1367
|
+
};
|
|
1368
|
+
const succeed = (begin) => {
|
|
1369
|
+
if (settled)
|
|
1370
|
+
return;
|
|
1371
|
+
settled = true;
|
|
1372
|
+
if (timer)
|
|
1373
|
+
clearTimeout(timer);
|
|
1374
|
+
resolve(begin);
|
|
1375
|
+
};
|
|
1376
|
+
if (timeoutMs > 0) {
|
|
1377
|
+
timer = setTimeout(() => {
|
|
1378
|
+
const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
|
|
1379
|
+
err.retryable = true;
|
|
1380
|
+
failAttempt(err);
|
|
1381
|
+
}, timeoutMs);
|
|
1382
|
+
}
|
|
1292
1383
|
if (this.token) {
|
|
1293
1384
|
this.socket = factory(url.toString());
|
|
1294
1385
|
}
|
|
@@ -1306,6 +1397,15 @@ class StreamingTranscriber {
|
|
|
1306
1397
|
reason = StreamingErrorMessages[code];
|
|
1307
1398
|
}
|
|
1308
1399
|
}
|
|
1400
|
+
// A close before `Begin` is a failed connection attempt — reject so
|
|
1401
|
+
// connect() can retry (or surface a permanent failure).
|
|
1402
|
+
if (!settled) {
|
|
1403
|
+
const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
|
|
1404
|
+
err.code = code;
|
|
1405
|
+
err.retryable = isRetryableCloseCode(code);
|
|
1406
|
+
failAttempt(err);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1309
1409
|
// Stop the flush timer when the socket is gone (server-initiated close,
|
|
1310
1410
|
// network drop, etc.) — otherwise subsequent ticks call send() on a
|
|
1311
1411
|
// closed socket and spam the error listener.
|
|
@@ -1316,11 +1416,15 @@ class StreamingTranscriber {
|
|
|
1316
1416
|
(_b = (_a = this.listeners).close) === null || _b === void 0 ? void 0 : _b.call(_a, code, reason);
|
|
1317
1417
|
};
|
|
1318
1418
|
this.socket.onerror = (event) => {
|
|
1319
|
-
var _a, _b, _c
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1419
|
+
var _a, _b, _c;
|
|
1420
|
+
const error = (_a = event.error) !== null && _a !== void 0 ? _a : new Error(event.message);
|
|
1421
|
+
// A socket error before `Begin` is a failed attempt → reject/retry.
|
|
1422
|
+
if (!settled) {
|
|
1423
|
+
error.retryable = true;
|
|
1424
|
+
failAttempt(error);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
(_c = (_b = this.listeners).error) === null || _c === void 0 ? void 0 : _c.call(_b, error);
|
|
1324
1428
|
};
|
|
1325
1429
|
this.socket.onmessage = ({ data }) => {
|
|
1326
1430
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
@@ -1328,15 +1432,23 @@ class StreamingTranscriber {
|
|
|
1328
1432
|
if ("error" in message) {
|
|
1329
1433
|
const err = new StreamingError(message.error);
|
|
1330
1434
|
if ("error_code" in message) {
|
|
1331
|
-
err.code =
|
|
1332
|
-
|
|
1435
|
+
err.code = message.error_code;
|
|
1436
|
+
}
|
|
1437
|
+
// A server error frame before `Begin` fails the attempt; the code
|
|
1438
|
+
// decides whether a retry is worthwhile.
|
|
1439
|
+
if (!settled) {
|
|
1440
|
+
const attemptErr = err;
|
|
1441
|
+
attemptErr.retryable =
|
|
1442
|
+
err.code === undefined ? true : isRetryableCloseCode(err.code);
|
|
1443
|
+
failAttempt(attemptErr);
|
|
1444
|
+
return;
|
|
1333
1445
|
}
|
|
1334
1446
|
(_b = (_a = this.listeners).error) === null || _b === void 0 ? void 0 : _b.call(_a, err);
|
|
1335
1447
|
return;
|
|
1336
1448
|
}
|
|
1337
1449
|
switch (message.type) {
|
|
1338
1450
|
case "Begin": {
|
|
1339
|
-
|
|
1451
|
+
succeed(message);
|
|
1340
1452
|
(_d = (_c = this.listeners).open) === null || _d === void 0 ? void 0 : _d.call(_c, message);
|
|
1341
1453
|
break;
|
|
1342
1454
|
}
|
|
@@ -1383,6 +1495,20 @@ class StreamingTranscriber {
|
|
|
1383
1495
|
};
|
|
1384
1496
|
});
|
|
1385
1497
|
}
|
|
1498
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
1499
|
+
discardPendingSocket() {
|
|
1500
|
+
if (!this.socket)
|
|
1501
|
+
return;
|
|
1502
|
+
try {
|
|
1503
|
+
if (this.socket.removeAllListeners)
|
|
1504
|
+
this.socket.removeAllListeners();
|
|
1505
|
+
this.socket.close();
|
|
1506
|
+
}
|
|
1507
|
+
catch (_a) {
|
|
1508
|
+
// Best-effort cleanup; a half-open socket may throw on close.
|
|
1509
|
+
}
|
|
1510
|
+
this.socket = undefined;
|
|
1511
|
+
}
|
|
1386
1512
|
/**
|
|
1387
1513
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
1388
1514
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -1656,6 +1782,16 @@ class StreamingTranscriber {
|
|
|
1656
1782
|
};
|
|
1657
1783
|
this.send(JSON.stringify(message));
|
|
1658
1784
|
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
1787
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
1788
|
+
*/
|
|
1789
|
+
keepAlive() {
|
|
1790
|
+
const message = {
|
|
1791
|
+
type: "KeepAlive",
|
|
1792
|
+
};
|
|
1793
|
+
this.send(JSON.stringify(message));
|
|
1794
|
+
}
|
|
1659
1795
|
send(data) {
|
|
1660
1796
|
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
|
|
1661
1797
|
throw new Error("Socket is not open for communication");
|
package/dist/index.mjs
CHANGED
|
@@ -74,7 +74,7 @@ if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
|
74
74
|
defaultUserAgentString += navigator.userAgent;
|
|
75
75
|
}
|
|
76
76
|
const defaultUserAgent = {
|
|
77
|
-
sdk: { name: "JavaScript", version: "4.
|
|
77
|
+
sdk: { name: "JavaScript", version: "4.35.0" },
|
|
78
78
|
};
|
|
79
79
|
if (typeof process !== "undefined") {
|
|
80
80
|
if (process.versions.node && defaultUserAgentString.indexOf("Node") === -1) {
|
|
@@ -1075,6 +1075,24 @@ function toInt16View(audio) {
|
|
|
1075
1075
|
}
|
|
1076
1076
|
const defaultStreamingUrl$1 = "wss://streaming.assemblyai.com/v3/ws";
|
|
1077
1077
|
const terminateSessionMessage = `{"type":"Terminate"}`;
|
|
1078
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1000;
|
|
1079
|
+
const DEFAULT_MAX_CONNECTION_RETRIES = 2;
|
|
1080
|
+
const DEFAULT_CONNECTION_RETRY_DELAY_MS = 500;
|
|
1081
|
+
/**
|
|
1082
|
+
* Close/error codes that signal a permanent client-side problem (auth,
|
|
1083
|
+
* billing, malformed config). A retry would hit the same failure, so the
|
|
1084
|
+
* connection is never retried on these.
|
|
1085
|
+
*/
|
|
1086
|
+
const NON_RETRYABLE_CLOSE_CODES = new Set([
|
|
1087
|
+
StreamingErrorType.BadSampleRate,
|
|
1088
|
+
StreamingErrorType.AuthFailed,
|
|
1089
|
+
StreamingErrorType.InsufficientFunds,
|
|
1090
|
+
StreamingErrorType.FreeTierUser,
|
|
1091
|
+
StreamingErrorType.BadSchema,
|
|
1092
|
+
]);
|
|
1093
|
+
function isRetryableCloseCode(code) {
|
|
1094
|
+
return code !== 1000 && !NON_RETRYABLE_CLOSE_CODES.has(code);
|
|
1095
|
+
}
|
|
1078
1096
|
/**
|
|
1079
1097
|
* Per-send chunk cap in milliseconds for the dual-channel mixer. The streaming
|
|
1080
1098
|
* server rejects audio messages longer than 1000 ms (`Input Duration Error`).
|
|
@@ -1281,12 +1299,85 @@ class StreamingTranscriber {
|
|
|
1281
1299
|
on(event, listener) {
|
|
1282
1300
|
this.listeners[event] = listener;
|
|
1283
1301
|
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Open the streaming session.
|
|
1304
|
+
*
|
|
1305
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
1306
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
1307
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
1308
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
1309
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
1310
|
+
* funds, malformed config) are not retried.
|
|
1311
|
+
*
|
|
1312
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
1313
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
1314
|
+
* the retry loop) to observe the failure.
|
|
1315
|
+
*/
|
|
1284
1316
|
connect() {
|
|
1285
|
-
return
|
|
1317
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1318
|
+
var _a, _b;
|
|
1286
1319
|
if (this.socket) {
|
|
1287
1320
|
throw new Error("Already connected");
|
|
1288
1321
|
}
|
|
1322
|
+
const maxRetries = (_a = this.params.maxConnectionRetries) !== null && _a !== void 0 ? _a : DEFAULT_MAX_CONNECTION_RETRIES;
|
|
1323
|
+
const retryDelay = (_b = this.params.connectionRetryDelay) !== null && _b !== void 0 ? _b : DEFAULT_CONNECTION_RETRY_DELAY_MS;
|
|
1324
|
+
let lastError;
|
|
1325
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1326
|
+
try {
|
|
1327
|
+
return yield this.connectOnce();
|
|
1328
|
+
}
|
|
1329
|
+
catch (err) {
|
|
1330
|
+
lastError = err;
|
|
1331
|
+
const retryable = err.retryable === true;
|
|
1332
|
+
if (!retryable || attempt === maxRetries) {
|
|
1333
|
+
throw err;
|
|
1334
|
+
}
|
|
1335
|
+
console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
|
|
1336
|
+
if (retryDelay > 0) {
|
|
1337
|
+
yield new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
// The loop above always returns or throws; this only satisfies the type
|
|
1342
|
+
// checker that a value is produced on every path.
|
|
1343
|
+
throw lastError !== null && lastError !== void 0 ? lastError : new Error("Failed to connect to streaming server");
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
connectOnce() {
|
|
1347
|
+
return new Promise((resolve, reject) => {
|
|
1348
|
+
var _a;
|
|
1289
1349
|
const url = this.connectionUrl();
|
|
1350
|
+
const timeoutMs = (_a = this.params.connectTimeout) !== null && _a !== void 0 ? _a : DEFAULT_CONNECT_TIMEOUT_MS;
|
|
1351
|
+
// `settled` flips once this attempt has resolved (`Begin`) or rejected
|
|
1352
|
+
// (timeout / pre-`Begin` close / error). Before it flips the socket
|
|
1353
|
+
// handlers drive this promise; after it flips they revert to normal
|
|
1354
|
+
// runtime dispatch (close / error / message listeners).
|
|
1355
|
+
let settled = false;
|
|
1356
|
+
let timer;
|
|
1357
|
+
const failAttempt = (error) => {
|
|
1358
|
+
if (settled)
|
|
1359
|
+
return;
|
|
1360
|
+
settled = true;
|
|
1361
|
+
if (timer)
|
|
1362
|
+
clearTimeout(timer);
|
|
1363
|
+
this.discardPendingSocket();
|
|
1364
|
+
reject(error);
|
|
1365
|
+
};
|
|
1366
|
+
const succeed = (begin) => {
|
|
1367
|
+
if (settled)
|
|
1368
|
+
return;
|
|
1369
|
+
settled = true;
|
|
1370
|
+
if (timer)
|
|
1371
|
+
clearTimeout(timer);
|
|
1372
|
+
resolve(begin);
|
|
1373
|
+
};
|
|
1374
|
+
if (timeoutMs > 0) {
|
|
1375
|
+
timer = setTimeout(() => {
|
|
1376
|
+
const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
|
|
1377
|
+
err.retryable = true;
|
|
1378
|
+
failAttempt(err);
|
|
1379
|
+
}, timeoutMs);
|
|
1380
|
+
}
|
|
1290
1381
|
if (this.token) {
|
|
1291
1382
|
this.socket = factory(url.toString());
|
|
1292
1383
|
}
|
|
@@ -1304,6 +1395,15 @@ class StreamingTranscriber {
|
|
|
1304
1395
|
reason = StreamingErrorMessages[code];
|
|
1305
1396
|
}
|
|
1306
1397
|
}
|
|
1398
|
+
// A close before `Begin` is a failed connection attempt — reject so
|
|
1399
|
+
// connect() can retry (or surface a permanent failure).
|
|
1400
|
+
if (!settled) {
|
|
1401
|
+
const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
|
|
1402
|
+
err.code = code;
|
|
1403
|
+
err.retryable = isRetryableCloseCode(code);
|
|
1404
|
+
failAttempt(err);
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1307
1407
|
// Stop the flush timer when the socket is gone (server-initiated close,
|
|
1308
1408
|
// network drop, etc.) — otherwise subsequent ticks call send() on a
|
|
1309
1409
|
// closed socket and spam the error listener.
|
|
@@ -1314,11 +1414,15 @@ class StreamingTranscriber {
|
|
|
1314
1414
|
(_b = (_a = this.listeners).close) === null || _b === void 0 ? void 0 : _b.call(_a, code, reason);
|
|
1315
1415
|
};
|
|
1316
1416
|
this.socket.onerror = (event) => {
|
|
1317
|
-
var _a, _b, _c
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1417
|
+
var _a, _b, _c;
|
|
1418
|
+
const error = (_a = event.error) !== null && _a !== void 0 ? _a : new Error(event.message);
|
|
1419
|
+
// A socket error before `Begin` is a failed attempt → reject/retry.
|
|
1420
|
+
if (!settled) {
|
|
1421
|
+
error.retryable = true;
|
|
1422
|
+
failAttempt(error);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
(_c = (_b = this.listeners).error) === null || _c === void 0 ? void 0 : _c.call(_b, error);
|
|
1322
1426
|
};
|
|
1323
1427
|
this.socket.onmessage = ({ data }) => {
|
|
1324
1428
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
@@ -1326,15 +1430,23 @@ class StreamingTranscriber {
|
|
|
1326
1430
|
if ("error" in message) {
|
|
1327
1431
|
const err = new StreamingError(message.error);
|
|
1328
1432
|
if ("error_code" in message) {
|
|
1329
|
-
err.code =
|
|
1330
|
-
|
|
1433
|
+
err.code = message.error_code;
|
|
1434
|
+
}
|
|
1435
|
+
// A server error frame before `Begin` fails the attempt; the code
|
|
1436
|
+
// decides whether a retry is worthwhile.
|
|
1437
|
+
if (!settled) {
|
|
1438
|
+
const attemptErr = err;
|
|
1439
|
+
attemptErr.retryable =
|
|
1440
|
+
err.code === undefined ? true : isRetryableCloseCode(err.code);
|
|
1441
|
+
failAttempt(attemptErr);
|
|
1442
|
+
return;
|
|
1331
1443
|
}
|
|
1332
1444
|
(_b = (_a = this.listeners).error) === null || _b === void 0 ? void 0 : _b.call(_a, err);
|
|
1333
1445
|
return;
|
|
1334
1446
|
}
|
|
1335
1447
|
switch (message.type) {
|
|
1336
1448
|
case "Begin": {
|
|
1337
|
-
|
|
1449
|
+
succeed(message);
|
|
1338
1450
|
(_d = (_c = this.listeners).open) === null || _d === void 0 ? void 0 : _d.call(_c, message);
|
|
1339
1451
|
break;
|
|
1340
1452
|
}
|
|
@@ -1381,6 +1493,20 @@ class StreamingTranscriber {
|
|
|
1381
1493
|
};
|
|
1382
1494
|
});
|
|
1383
1495
|
}
|
|
1496
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
1497
|
+
discardPendingSocket() {
|
|
1498
|
+
if (!this.socket)
|
|
1499
|
+
return;
|
|
1500
|
+
try {
|
|
1501
|
+
if (this.socket.removeAllListeners)
|
|
1502
|
+
this.socket.removeAllListeners();
|
|
1503
|
+
this.socket.close();
|
|
1504
|
+
}
|
|
1505
|
+
catch (_a) {
|
|
1506
|
+
// Best-effort cleanup; a half-open socket may throw on close.
|
|
1507
|
+
}
|
|
1508
|
+
this.socket = undefined;
|
|
1509
|
+
}
|
|
1384
1510
|
/**
|
|
1385
1511
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
1386
1512
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -1654,6 +1780,16 @@ class StreamingTranscriber {
|
|
|
1654
1780
|
};
|
|
1655
1781
|
this.send(JSON.stringify(message));
|
|
1656
1782
|
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
1785
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
1786
|
+
*/
|
|
1787
|
+
keepAlive() {
|
|
1788
|
+
const message = {
|
|
1789
|
+
type: "KeepAlive",
|
|
1790
|
+
};
|
|
1791
|
+
this.send(JSON.stringify(message));
|
|
1792
|
+
}
|
|
1657
1793
|
send(data) {
|
|
1658
1794
|
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
|
|
1659
1795
|
throw new Error("Socket is not open for communication");
|