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/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.34.5" },
38
+ sdk: { name: "JavaScript", version: "4.35.0" },
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`).
@@ -1188,12 +1206,81 @@ class StreamingTranscriber {
1188
1206
  on(event, listener) {
1189
1207
  this.listeners[event] = listener;
1190
1208
  }
1191
- connect() {
1192
- return new Promise((resolve) => {
1193
- if (this.socket) {
1194
- throw new Error("Already connected");
1209
+ /**
1210
+ * Open the streaming session.
1211
+ *
1212
+ * Resolves with the server's `Begin` event once the handshake completes. A
1213
+ * single attempt is bounded by `connectTimeout` (default 1000ms); transient
1214
+ * failures (timeout, network drop, unexpected close) are retried up to
1215
+ * `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
1216
+ * (default 500ms) between attempts. Permanent failures (auth, insufficient
1217
+ * funds, malformed config) are not retried.
1218
+ *
1219
+ * Unlike previously, a failed connection now rejects this promise rather
1220
+ * than only invoking the `error` listener — necessary for the caller (and
1221
+ * the retry loop) to observe the failure.
1222
+ */
1223
+ async connect() {
1224
+ if (this.socket) {
1225
+ throw new Error("Already connected");
1226
+ }
1227
+ const maxRetries = this.params.maxConnectionRetries ?? DEFAULT_MAX_CONNECTION_RETRIES;
1228
+ const retryDelay = this.params.connectionRetryDelay ?? DEFAULT_CONNECTION_RETRY_DELAY_MS;
1229
+ let lastError;
1230
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1231
+ try {
1232
+ return await this.connectOnce();
1233
+ }
1234
+ catch (err) {
1235
+ lastError = err;
1236
+ const retryable = err.retryable === true;
1237
+ if (!retryable || attempt === maxRetries) {
1238
+ throw err;
1239
+ }
1240
+ console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
1241
+ if (retryDelay > 0) {
1242
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
1243
+ }
1195
1244
  }
1245
+ }
1246
+ // The loop above always returns or throws; this only satisfies the type
1247
+ // checker that a value is produced on every path.
1248
+ throw lastError ?? new Error("Failed to connect to streaming server");
1249
+ }
1250
+ connectOnce() {
1251
+ return new Promise((resolve, reject) => {
1196
1252
  const url = this.connectionUrl();
1253
+ const timeoutMs = this.params.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
1254
+ // `settled` flips once this attempt has resolved (`Begin`) or rejected
1255
+ // (timeout / pre-`Begin` close / error). Before it flips the socket
1256
+ // handlers drive this promise; after it flips they revert to normal
1257
+ // runtime dispatch (close / error / message listeners).
1258
+ let settled = false;
1259
+ let timer;
1260
+ const failAttempt = (error) => {
1261
+ if (settled)
1262
+ return;
1263
+ settled = true;
1264
+ if (timer)
1265
+ clearTimeout(timer);
1266
+ this.discardPendingSocket();
1267
+ reject(error);
1268
+ };
1269
+ const succeed = (begin) => {
1270
+ if (settled)
1271
+ return;
1272
+ settled = true;
1273
+ if (timer)
1274
+ clearTimeout(timer);
1275
+ resolve(begin);
1276
+ };
1277
+ if (timeoutMs > 0) {
1278
+ timer = setTimeout(() => {
1279
+ const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
1280
+ err.retryable = true;
1281
+ failAttempt(err);
1282
+ }, timeoutMs);
1283
+ }
1197
1284
  if (this.token) {
1198
1285
  this.socket = factory(url.toString());
1199
1286
  }
@@ -1210,6 +1297,15 @@ class StreamingTranscriber {
1210
1297
  reason = StreamingErrorMessages[code];
1211
1298
  }
1212
1299
  }
1300
+ // A close before `Begin` is a failed connection attempt — reject so
1301
+ // connect() can retry (or surface a permanent failure).
1302
+ if (!settled) {
1303
+ const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
1304
+ err.code = code;
1305
+ err.retryable = isRetryableCloseCode(code);
1306
+ failAttempt(err);
1307
+ return;
1308
+ }
1213
1309
  // Stop the flush timer when the socket is gone (server-initiated close,
1214
1310
  // network drop, etc.) — otherwise subsequent ticks call send() on a
1215
1311
  // closed socket and spam the error listener.
@@ -1220,25 +1316,37 @@ class StreamingTranscriber {
1220
1316
  this.listeners.close?.(code, reason);
1221
1317
  };
1222
1318
  this.socket.onerror = (event) => {
1223
- if (event.error)
1224
- this.listeners.error?.(event.error);
1225
- else
1226
- this.listeners.error?.(new Error(event.message));
1319
+ const error = event.error ?? new Error(event.message);
1320
+ // A socket error before `Begin` is a failed attempt → reject/retry.
1321
+ if (!settled) {
1322
+ error.retryable = true;
1323
+ failAttempt(error);
1324
+ return;
1325
+ }
1326
+ this.listeners.error?.(error);
1227
1327
  };
1228
1328
  this.socket.onmessage = ({ data }) => {
1229
1329
  const message = JSON.parse(data.toString());
1230
1330
  if ("error" in message) {
1231
1331
  const err = new StreamingError(message.error);
1232
1332
  if ("error_code" in message) {
1233
- err.code =
1234
- message.error_code;
1333
+ err.code = message.error_code;
1334
+ }
1335
+ // A server error frame before `Begin` fails the attempt; the code
1336
+ // decides whether a retry is worthwhile.
1337
+ if (!settled) {
1338
+ const attemptErr = err;
1339
+ attemptErr.retryable =
1340
+ err.code === undefined ? true : isRetryableCloseCode(err.code);
1341
+ failAttempt(attemptErr);
1342
+ return;
1235
1343
  }
1236
1344
  this.listeners.error?.(err);
1237
1345
  return;
1238
1346
  }
1239
1347
  switch (message.type) {
1240
1348
  case "Begin": {
1241
- resolve(message);
1349
+ succeed(message);
1242
1350
  this.listeners.open?.(message);
1243
1351
  break;
1244
1352
  }
@@ -1285,6 +1393,20 @@ class StreamingTranscriber {
1285
1393
  };
1286
1394
  });
1287
1395
  }
1396
+ /** Tear down a half-open socket from a failed connection attempt. */
1397
+ discardPendingSocket() {
1398
+ if (!this.socket)
1399
+ return;
1400
+ try {
1401
+ if (this.socket.removeAllListeners)
1402
+ this.socket.removeAllListeners();
1403
+ this.socket.close();
1404
+ }
1405
+ catch {
1406
+ // Best-effort cleanup; a half-open socket may throw on close.
1407
+ }
1408
+ this.socket = undefined;
1409
+ }
1288
1410
  /**
1289
1411
  * Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
1290
1412
  * only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
@@ -1558,6 +1680,16 @@ class StreamingTranscriber {
1558
1680
  };
1559
1681
  this.send(JSON.stringify(message));
1560
1682
  }
1683
+ /**
1684
+ * Reset the server's inactivity timer. Only needed when the session was
1685
+ * created with `inactivityTimeout` and no audio is being sent.
1686
+ */
1687
+ keepAlive() {
1688
+ const message = {
1689
+ type: "KeepAlive",
1690
+ };
1691
+ this.send(JSON.stringify(message));
1692
+ }
1561
1693
  send(data) {
1562
1694
  if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
1563
1695
  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.34.5" },
36
+ sdk: { name: "JavaScript", version: "4.35.0" },
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`).
@@ -1186,12 +1204,81 @@ class StreamingTranscriber {
1186
1204
  on(event, listener) {
1187
1205
  this.listeners[event] = listener;
1188
1206
  }
1189
- connect() {
1190
- return new Promise((resolve) => {
1191
- if (this.socket) {
1192
- throw new Error("Already connected");
1207
+ /**
1208
+ * Open the streaming session.
1209
+ *
1210
+ * Resolves with the server's `Begin` event once the handshake completes. A
1211
+ * single attempt is bounded by `connectTimeout` (default 1000ms); transient
1212
+ * failures (timeout, network drop, unexpected close) are retried up to
1213
+ * `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
1214
+ * (default 500ms) between attempts. Permanent failures (auth, insufficient
1215
+ * funds, malformed config) are not retried.
1216
+ *
1217
+ * Unlike previously, a failed connection now rejects this promise rather
1218
+ * than only invoking the `error` listener — necessary for the caller (and
1219
+ * the retry loop) to observe the failure.
1220
+ */
1221
+ async connect() {
1222
+ if (this.socket) {
1223
+ throw new Error("Already connected");
1224
+ }
1225
+ const maxRetries = this.params.maxConnectionRetries ?? DEFAULT_MAX_CONNECTION_RETRIES;
1226
+ const retryDelay = this.params.connectionRetryDelay ?? DEFAULT_CONNECTION_RETRY_DELAY_MS;
1227
+ let lastError;
1228
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1229
+ try {
1230
+ return await this.connectOnce();
1231
+ }
1232
+ catch (err) {
1233
+ lastError = err;
1234
+ const retryable = err.retryable === true;
1235
+ if (!retryable || attempt === maxRetries) {
1236
+ throw err;
1237
+ }
1238
+ console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
1239
+ if (retryDelay > 0) {
1240
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
1241
+ }
1193
1242
  }
1243
+ }
1244
+ // The loop above always returns or throws; this only satisfies the type
1245
+ // checker that a value is produced on every path.
1246
+ throw lastError ?? new Error("Failed to connect to streaming server");
1247
+ }
1248
+ connectOnce() {
1249
+ return new Promise((resolve, reject) => {
1194
1250
  const url = this.connectionUrl();
1251
+ const timeoutMs = this.params.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
1252
+ // `settled` flips once this attempt has resolved (`Begin`) or rejected
1253
+ // (timeout / pre-`Begin` close / error). Before it flips the socket
1254
+ // handlers drive this promise; after it flips they revert to normal
1255
+ // runtime dispatch (close / error / message listeners).
1256
+ let settled = false;
1257
+ let timer;
1258
+ const failAttempt = (error) => {
1259
+ if (settled)
1260
+ return;
1261
+ settled = true;
1262
+ if (timer)
1263
+ clearTimeout(timer);
1264
+ this.discardPendingSocket();
1265
+ reject(error);
1266
+ };
1267
+ const succeed = (begin) => {
1268
+ if (settled)
1269
+ return;
1270
+ settled = true;
1271
+ if (timer)
1272
+ clearTimeout(timer);
1273
+ resolve(begin);
1274
+ };
1275
+ if (timeoutMs > 0) {
1276
+ timer = setTimeout(() => {
1277
+ const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
1278
+ err.retryable = true;
1279
+ failAttempt(err);
1280
+ }, timeoutMs);
1281
+ }
1195
1282
  if (this.token) {
1196
1283
  this.socket = factory(url.toString());
1197
1284
  }
@@ -1208,6 +1295,15 @@ class StreamingTranscriber {
1208
1295
  reason = StreamingErrorMessages[code];
1209
1296
  }
1210
1297
  }
1298
+ // A close before `Begin` is a failed connection attempt — reject so
1299
+ // connect() can retry (or surface a permanent failure).
1300
+ if (!settled) {
1301
+ const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
1302
+ err.code = code;
1303
+ err.retryable = isRetryableCloseCode(code);
1304
+ failAttempt(err);
1305
+ return;
1306
+ }
1211
1307
  // Stop the flush timer when the socket is gone (server-initiated close,
1212
1308
  // network drop, etc.) — otherwise subsequent ticks call send() on a
1213
1309
  // closed socket and spam the error listener.
@@ -1218,25 +1314,37 @@ class StreamingTranscriber {
1218
1314
  this.listeners.close?.(code, reason);
1219
1315
  };
1220
1316
  this.socket.onerror = (event) => {
1221
- if (event.error)
1222
- this.listeners.error?.(event.error);
1223
- else
1224
- this.listeners.error?.(new Error(event.message));
1317
+ const error = event.error ?? new Error(event.message);
1318
+ // A socket error before `Begin` is a failed attempt → reject/retry.
1319
+ if (!settled) {
1320
+ error.retryable = true;
1321
+ failAttempt(error);
1322
+ return;
1323
+ }
1324
+ this.listeners.error?.(error);
1225
1325
  };
1226
1326
  this.socket.onmessage = ({ data }) => {
1227
1327
  const message = JSON.parse(data.toString());
1228
1328
  if ("error" in message) {
1229
1329
  const err = new StreamingError(message.error);
1230
1330
  if ("error_code" in message) {
1231
- err.code =
1232
- message.error_code;
1331
+ err.code = message.error_code;
1332
+ }
1333
+ // A server error frame before `Begin` fails the attempt; the code
1334
+ // decides whether a retry is worthwhile.
1335
+ if (!settled) {
1336
+ const attemptErr = err;
1337
+ attemptErr.retryable =
1338
+ err.code === undefined ? true : isRetryableCloseCode(err.code);
1339
+ failAttempt(attemptErr);
1340
+ return;
1233
1341
  }
1234
1342
  this.listeners.error?.(err);
1235
1343
  return;
1236
1344
  }
1237
1345
  switch (message.type) {
1238
1346
  case "Begin": {
1239
- resolve(message);
1347
+ succeed(message);
1240
1348
  this.listeners.open?.(message);
1241
1349
  break;
1242
1350
  }
@@ -1283,6 +1391,20 @@ class StreamingTranscriber {
1283
1391
  };
1284
1392
  });
1285
1393
  }
1394
+ /** Tear down a half-open socket from a failed connection attempt. */
1395
+ discardPendingSocket() {
1396
+ if (!this.socket)
1397
+ return;
1398
+ try {
1399
+ if (this.socket.removeAllListeners)
1400
+ this.socket.removeAllListeners();
1401
+ this.socket.close();
1402
+ }
1403
+ catch {
1404
+ // Best-effort cleanup; a half-open socket may throw on close.
1405
+ }
1406
+ this.socket = undefined;
1407
+ }
1286
1408
  /**
1287
1409
  * Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
1288
1410
  * only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
@@ -1556,6 +1678,16 @@ class StreamingTranscriber {
1556
1678
  };
1557
1679
  this.send(JSON.stringify(message));
1558
1680
  }
1681
+ /**
1682
+ * Reset the server's inactivity timer. Only needed when the session was
1683
+ * created with `inactivityTimeout` and no audio is being sent.
1684
+ */
1685
+ keepAlive() {
1686
+ const message = {
1687
+ type: "KeepAlive",
1688
+ };
1689
+ this.send(JSON.stringify(message));
1690
+ }
1559
1691
  send(data) {
1560
1692
  if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
1561
1693
  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
  }