assemblyai 4.34.6 → 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/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.34.6" },
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
- connect() {
1193
- return new Promise((resolve) => {
1194
- if (this.socket) {
1195
- throw new Error("Already connected");
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
- if (event.error)
1225
- this.listeners.error?.(event.error);
1226
- else
1227
- this.listeners.error?.(new Error(event.message));
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
- message.error_code;
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
- resolve(message);
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.34.6" },
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 new Promise((resolve) => {
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, _d;
1320
- if (event.error)
1321
- (_b = (_a = this.listeners).error) === null || _b === void 0 ? void 0 : _b.call(_a, event.error);
1322
- else
1323
- (_d = (_c = this.listeners).error) === null || _d === void 0 ? void 0 : _d.call(_c, new Error(event.message));
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
- message.error_code;
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
- resolve(message);
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.34.6" },
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 new Promise((resolve) => {
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, _d;
1318
- if (event.error)
1319
- (_b = (_a = this.listeners).error) === null || _b === void 0 ? void 0 : _b.call(_a, event.error);
1320
- else
1321
- (_d = (_c = this.listeners).error) === null || _d === void 0 ? void 0 : _d.call(_c, new Error(event.message));
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
- message.error_code;
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
- resolve(message);
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");