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/dist/bun.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.3" },
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`).
@@ -1114,8 +1132,12 @@ class StreamingTranscriber {
1114
1132
  searchParams.set("speech_model", this.params.speechModel.toString());
1115
1133
  }
1116
1134
  if (this.params.languageCode !== undefined) {
1135
+ console.warn("[Deprecation Warning] `languageCode` is deprecated and will be removed in a future release. Please use `languageCodes` instead.");
1117
1136
  searchParams.set("language_code", this.params.languageCode);
1118
1137
  }
1138
+ if (this.params.languageCodes !== undefined) {
1139
+ searchParams.set("language_codes", JSON.stringify(this.params.languageCodes));
1140
+ }
1119
1141
  if (this.params.languageDetection !== undefined) {
1120
1142
  searchParams.set("language_detection", this.params.languageDetection.toString());
1121
1143
  }
@@ -1189,12 +1211,81 @@ class StreamingTranscriber {
1189
1211
  on(event, listener) {
1190
1212
  this.listeners[event] = listener;
1191
1213
  }
1192
- connect() {
1193
- return new Promise((resolve) => {
1194
- if (this.socket) {
1195
- throw new Error("Already connected");
1214
+ /**
1215
+ * Open the streaming session.
1216
+ *
1217
+ * Resolves with the server's `Begin` event once the handshake completes. A
1218
+ * single attempt is bounded by `connectTimeout` (default 1000ms); transient
1219
+ * failures (timeout, network drop, unexpected close) are retried up to
1220
+ * `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
1221
+ * (default 500ms) between attempts. Permanent failures (auth, insufficient
1222
+ * funds, malformed config) are not retried.
1223
+ *
1224
+ * Unlike previously, a failed connection now rejects this promise rather
1225
+ * than only invoking the `error` listener — necessary for the caller (and
1226
+ * the retry loop) to observe the failure.
1227
+ */
1228
+ async connect() {
1229
+ if (this.socket) {
1230
+ throw new Error("Already connected");
1231
+ }
1232
+ const maxRetries = this.params.maxConnectionRetries ?? DEFAULT_MAX_CONNECTION_RETRIES;
1233
+ const retryDelay = this.params.connectionRetryDelay ?? DEFAULT_CONNECTION_RETRY_DELAY_MS;
1234
+ let lastError;
1235
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1236
+ try {
1237
+ return await this.connectOnce();
1238
+ }
1239
+ catch (err) {
1240
+ lastError = err;
1241
+ const retryable = err.retryable === true;
1242
+ if (!retryable || attempt === maxRetries) {
1243
+ throw err;
1244
+ }
1245
+ console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
1246
+ if (retryDelay > 0) {
1247
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
1248
+ }
1196
1249
  }
1250
+ }
1251
+ // The loop above always returns or throws; this only satisfies the type
1252
+ // checker that a value is produced on every path.
1253
+ throw lastError ?? new Error("Failed to connect to streaming server");
1254
+ }
1255
+ connectOnce() {
1256
+ return new Promise((resolve, reject) => {
1197
1257
  const url = this.connectionUrl();
1258
+ const timeoutMs = this.params.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
1259
+ // `settled` flips once this attempt has resolved (`Begin`) or rejected
1260
+ // (timeout / pre-`Begin` close / error). Before it flips the socket
1261
+ // handlers drive this promise; after it flips they revert to normal
1262
+ // runtime dispatch (close / error / message listeners).
1263
+ let settled = false;
1264
+ let timer;
1265
+ const failAttempt = (error) => {
1266
+ if (settled)
1267
+ return;
1268
+ settled = true;
1269
+ if (timer)
1270
+ clearTimeout(timer);
1271
+ this.discardPendingSocket();
1272
+ reject(error);
1273
+ };
1274
+ const succeed = (begin) => {
1275
+ if (settled)
1276
+ return;
1277
+ settled = true;
1278
+ if (timer)
1279
+ clearTimeout(timer);
1280
+ resolve(begin);
1281
+ };
1282
+ if (timeoutMs > 0) {
1283
+ timer = setTimeout(() => {
1284
+ const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
1285
+ err.retryable = true;
1286
+ failAttempt(err);
1287
+ }, timeoutMs);
1288
+ }
1198
1289
  if (this.token) {
1199
1290
  this.socket = factory(url.toString());
1200
1291
  }
@@ -1211,6 +1302,15 @@ class StreamingTranscriber {
1211
1302
  reason = StreamingErrorMessages[code];
1212
1303
  }
1213
1304
  }
1305
+ // A close before `Begin` is a failed connection attempt — reject so
1306
+ // connect() can retry (or surface a permanent failure).
1307
+ if (!settled) {
1308
+ const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
1309
+ err.code = code;
1310
+ err.retryable = isRetryableCloseCode(code);
1311
+ failAttempt(err);
1312
+ return;
1313
+ }
1214
1314
  // Stop the flush timer when the socket is gone (server-initiated close,
1215
1315
  // network drop, etc.) — otherwise subsequent ticks call send() on a
1216
1316
  // closed socket and spam the error listener.
@@ -1221,25 +1321,37 @@ class StreamingTranscriber {
1221
1321
  this.listeners.close?.(code, reason);
1222
1322
  };
1223
1323
  this.socket.onerror = (event) => {
1224
- if (event.error)
1225
- this.listeners.error?.(event.error);
1226
- else
1227
- this.listeners.error?.(new Error(event.message));
1324
+ const error = event.error ?? new Error(event.message);
1325
+ // A socket error before `Begin` is a failed attempt → reject/retry.
1326
+ if (!settled) {
1327
+ error.retryable = true;
1328
+ failAttempt(error);
1329
+ return;
1330
+ }
1331
+ this.listeners.error?.(error);
1228
1332
  };
1229
1333
  this.socket.onmessage = ({ data }) => {
1230
1334
  const message = JSON.parse(data.toString());
1231
1335
  if ("error" in message) {
1232
1336
  const err = new StreamingError(message.error);
1233
1337
  if ("error_code" in message) {
1234
- err.code =
1235
- message.error_code;
1338
+ err.code = message.error_code;
1339
+ }
1340
+ // A server error frame before `Begin` fails the attempt; the code
1341
+ // decides whether a retry is worthwhile.
1342
+ if (!settled) {
1343
+ const attemptErr = err;
1344
+ attemptErr.retryable =
1345
+ err.code === undefined ? true : isRetryableCloseCode(err.code);
1346
+ failAttempt(attemptErr);
1347
+ return;
1236
1348
  }
1237
1349
  this.listeners.error?.(err);
1238
1350
  return;
1239
1351
  }
1240
1352
  switch (message.type) {
1241
1353
  case "Begin": {
1242
- resolve(message);
1354
+ succeed(message);
1243
1355
  this.listeners.open?.(message);
1244
1356
  break;
1245
1357
  }
@@ -1286,6 +1398,20 @@ class StreamingTranscriber {
1286
1398
  };
1287
1399
  });
1288
1400
  }
1401
+ /** Tear down a half-open socket from a failed connection attempt. */
1402
+ discardPendingSocket() {
1403
+ if (!this.socket)
1404
+ return;
1405
+ try {
1406
+ if (this.socket.removeAllListeners)
1407
+ this.socket.removeAllListeners();
1408
+ this.socket.close();
1409
+ }
1410
+ catch {
1411
+ // Best-effort cleanup; a half-open socket may throw on close.
1412
+ }
1413
+ this.socket = undefined;
1414
+ }
1289
1415
  /**
1290
1416
  * Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
1291
1417
  * only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
@@ -1559,6 +1685,16 @@ class StreamingTranscriber {
1559
1685
  };
1560
1686
  this.send(JSON.stringify(message));
1561
1687
  }
1688
+ /**
1689
+ * Reset the server's inactivity timer. Only needed when the session was
1690
+ * created with `inactivityTimeout` and no audio is being sent.
1691
+ */
1692
+ keepAlive() {
1693
+ const message = {
1694
+ type: "KeepAlive",
1695
+ };
1696
+ this.send(JSON.stringify(message));
1697
+ }
1562
1698
  send(data) {
1563
1699
  if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
1564
1700
  throw new Error("Socket is not open for communication");
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.3" },
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`).
@@ -1114,8 +1132,12 @@ class StreamingTranscriber {
1114
1132
  searchParams.set("speech_model", this.params.speechModel.toString());
1115
1133
  }
1116
1134
  if (this.params.languageCode !== undefined) {
1135
+ console.warn("[Deprecation Warning] `languageCode` is deprecated and will be removed in a future release. Please use `languageCodes` instead.");
1117
1136
  searchParams.set("language_code", this.params.languageCode);
1118
1137
  }
1138
+ if (this.params.languageCodes !== undefined) {
1139
+ searchParams.set("language_codes", JSON.stringify(this.params.languageCodes));
1140
+ }
1119
1141
  if (this.params.languageDetection !== undefined) {
1120
1142
  searchParams.set("language_detection", this.params.languageDetection.toString());
1121
1143
  }
@@ -1189,12 +1211,81 @@ class StreamingTranscriber {
1189
1211
  on(event, listener) {
1190
1212
  this.listeners[event] = listener;
1191
1213
  }
1192
- connect() {
1193
- return new Promise((resolve) => {
1194
- if (this.socket) {
1195
- throw new Error("Already connected");
1214
+ /**
1215
+ * Open the streaming session.
1216
+ *
1217
+ * Resolves with the server's `Begin` event once the handshake completes. A
1218
+ * single attempt is bounded by `connectTimeout` (default 1000ms); transient
1219
+ * failures (timeout, network drop, unexpected close) are retried up to
1220
+ * `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
1221
+ * (default 500ms) between attempts. Permanent failures (auth, insufficient
1222
+ * funds, malformed config) are not retried.
1223
+ *
1224
+ * Unlike previously, a failed connection now rejects this promise rather
1225
+ * than only invoking the `error` listener — necessary for the caller (and
1226
+ * the retry loop) to observe the failure.
1227
+ */
1228
+ async connect() {
1229
+ if (this.socket) {
1230
+ throw new Error("Already connected");
1231
+ }
1232
+ const maxRetries = this.params.maxConnectionRetries ?? DEFAULT_MAX_CONNECTION_RETRIES;
1233
+ const retryDelay = this.params.connectionRetryDelay ?? DEFAULT_CONNECTION_RETRY_DELAY_MS;
1234
+ let lastError;
1235
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1236
+ try {
1237
+ return await this.connectOnce();
1238
+ }
1239
+ catch (err) {
1240
+ lastError = err;
1241
+ const retryable = err.retryable === true;
1242
+ if (!retryable || attempt === maxRetries) {
1243
+ throw err;
1244
+ }
1245
+ console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
1246
+ if (retryDelay > 0) {
1247
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
1248
+ }
1196
1249
  }
1250
+ }
1251
+ // The loop above always returns or throws; this only satisfies the type
1252
+ // checker that a value is produced on every path.
1253
+ throw lastError ?? new Error("Failed to connect to streaming server");
1254
+ }
1255
+ connectOnce() {
1256
+ return new Promise((resolve, reject) => {
1197
1257
  const url = this.connectionUrl();
1258
+ const timeoutMs = this.params.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
1259
+ // `settled` flips once this attempt has resolved (`Begin`) or rejected
1260
+ // (timeout / pre-`Begin` close / error). Before it flips the socket
1261
+ // handlers drive this promise; after it flips they revert to normal
1262
+ // runtime dispatch (close / error / message listeners).
1263
+ let settled = false;
1264
+ let timer;
1265
+ const failAttempt = (error) => {
1266
+ if (settled)
1267
+ return;
1268
+ settled = true;
1269
+ if (timer)
1270
+ clearTimeout(timer);
1271
+ this.discardPendingSocket();
1272
+ reject(error);
1273
+ };
1274
+ const succeed = (begin) => {
1275
+ if (settled)
1276
+ return;
1277
+ settled = true;
1278
+ if (timer)
1279
+ clearTimeout(timer);
1280
+ resolve(begin);
1281
+ };
1282
+ if (timeoutMs > 0) {
1283
+ timer = setTimeout(() => {
1284
+ const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
1285
+ err.retryable = true;
1286
+ failAttempt(err);
1287
+ }, timeoutMs);
1288
+ }
1198
1289
  if (this.token) {
1199
1290
  this.socket = factory(url.toString());
1200
1291
  }
@@ -1211,6 +1302,15 @@ class StreamingTranscriber {
1211
1302
  reason = StreamingErrorMessages[code];
1212
1303
  }
1213
1304
  }
1305
+ // A close before `Begin` is a failed connection attempt — reject so
1306
+ // connect() can retry (or surface a permanent failure).
1307
+ if (!settled) {
1308
+ const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
1309
+ err.code = code;
1310
+ err.retryable = isRetryableCloseCode(code);
1311
+ failAttempt(err);
1312
+ return;
1313
+ }
1214
1314
  // Stop the flush timer when the socket is gone (server-initiated close,
1215
1315
  // network drop, etc.) — otherwise subsequent ticks call send() on a
1216
1316
  // closed socket and spam the error listener.
@@ -1221,25 +1321,37 @@ class StreamingTranscriber {
1221
1321
  this.listeners.close?.(code, reason);
1222
1322
  };
1223
1323
  this.socket.onerror = (event) => {
1224
- if (event.error)
1225
- this.listeners.error?.(event.error);
1226
- else
1227
- this.listeners.error?.(new Error(event.message));
1324
+ const error = event.error ?? new Error(event.message);
1325
+ // A socket error before `Begin` is a failed attempt → reject/retry.
1326
+ if (!settled) {
1327
+ error.retryable = true;
1328
+ failAttempt(error);
1329
+ return;
1330
+ }
1331
+ this.listeners.error?.(error);
1228
1332
  };
1229
1333
  this.socket.onmessage = ({ data }) => {
1230
1334
  const message = JSON.parse(data.toString());
1231
1335
  if ("error" in message) {
1232
1336
  const err = new StreamingError(message.error);
1233
1337
  if ("error_code" in message) {
1234
- err.code =
1235
- message.error_code;
1338
+ err.code = message.error_code;
1339
+ }
1340
+ // A server error frame before `Begin` fails the attempt; the code
1341
+ // decides whether a retry is worthwhile.
1342
+ if (!settled) {
1343
+ const attemptErr = err;
1344
+ attemptErr.retryable =
1345
+ err.code === undefined ? true : isRetryableCloseCode(err.code);
1346
+ failAttempt(attemptErr);
1347
+ return;
1236
1348
  }
1237
1349
  this.listeners.error?.(err);
1238
1350
  return;
1239
1351
  }
1240
1352
  switch (message.type) {
1241
1353
  case "Begin": {
1242
- resolve(message);
1354
+ succeed(message);
1243
1355
  this.listeners.open?.(message);
1244
1356
  break;
1245
1357
  }
@@ -1286,6 +1398,20 @@ class StreamingTranscriber {
1286
1398
  };
1287
1399
  });
1288
1400
  }
1401
+ /** Tear down a half-open socket from a failed connection attempt. */
1402
+ discardPendingSocket() {
1403
+ if (!this.socket)
1404
+ return;
1405
+ try {
1406
+ if (this.socket.removeAllListeners)
1407
+ this.socket.removeAllListeners();
1408
+ this.socket.close();
1409
+ }
1410
+ catch {
1411
+ // Best-effort cleanup; a half-open socket may throw on close.
1412
+ }
1413
+ this.socket = undefined;
1414
+ }
1289
1415
  /**
1290
1416
  * Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
1291
1417
  * only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
@@ -1559,6 +1685,16 @@ class StreamingTranscriber {
1559
1685
  };
1560
1686
  this.send(JSON.stringify(message));
1561
1687
  }
1688
+ /**
1689
+ * Reset the server's inactivity timer. Only needed when the session was
1690
+ * created with `inactivityTimeout` and no audio is being sent.
1691
+ */
1692
+ keepAlive() {
1693
+ const message = {
1694
+ type: "KeepAlive",
1695
+ };
1696
+ this.send(JSON.stringify(message));
1697
+ }
1562
1698
  send(data) {
1563
1699
  if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
1564
1700
  throw new Error("Socket is not open for communication");
@@ -1,7 +1,7 @@
1
1
  export * from "../types/asyncapi.generated";
2
2
  export * from "../types/realtime";
3
3
  export * from "../types/helpers";
4
- export * from "../types/streaming/dual-channel";
4
+ export * from "../types/streaming";
5
5
  export * from "../services/realtime/service";
6
6
  export * from "../services/streaming/service";
7
7
  export * from "../services/streaming/factory";