@soniox/node 2.0.0 → 2.0.1

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/index.cjs CHANGED
@@ -342,10 +342,34 @@ var AsyncEventQueue = class {
342
342
  return this.done;
343
343
  }
344
344
  /**
345
+ * Drop buffered events without ending the queue.
346
+ *
347
+ * Intended for owners that know their consumer has gone away (e.g. an
348
+ * async-iterator consumer broke out of its `for await` loop). The queue
349
+ * remains active and accepts future pushes. Callers must ensure no other
350
+ * iterator is concurrently consuming this queue, since this also drops
351
+ * events those consumers would have observed.
352
+ */
353
+ clear() {
354
+ this.queue = [];
355
+ }
356
+ /**
345
357
  * Async iterator implementation.
358
+ *
359
+ * The returned iterator implements `return()` so consumers that exit
360
+ * `for await` early (via `break`, `throw`, or an outer `return`) cleanly
361
+ * release the iteration without further work. The queue itself is left
362
+ * in place — call {@link clear} or {@link end}/{@link abort} if buffered
363
+ * events should also be dropped.
346
364
  */
347
365
  [Symbol.asyncIterator]() {
348
- return { next: () => this.next() };
366
+ return {
367
+ next: () => this.next(),
368
+ return: (value) => Promise.resolve({
369
+ value,
370
+ done: true
371
+ })
372
+ };
349
373
  }
350
374
  /**
351
375
  * Get the next event from the queue.
@@ -695,6 +719,7 @@ function filterSpecialTokens(tokens) {
695
719
  var RealtimeSttSession = class {
696
720
  emitter = new TypedEmitter();
697
721
  eventQueue = new AsyncEventQueue();
722
+ iteratorAttached = false;
698
723
  apiKey;
699
724
  wsBaseUrl;
700
725
  config;
@@ -883,9 +908,26 @@ var RealtimeSttSession = class {
883
908
  }
884
909
  /**
885
910
  * Async iterator for consuming events.
911
+ *
912
+ * The returned iterator's `return()` resets the internal iterator-attach
913
+ * flag and drops any buffered events, so consumers that exit `for await`
914
+ * early (via `break` etc.) stop accruing memory while the session keeps
915
+ * running.
886
916
  */
887
917
  [Symbol.asyncIterator]() {
888
- return this.eventQueue[Symbol.asyncIterator]();
918
+ this.iteratorAttached = true;
919
+ const inner = this.eventQueue[Symbol.asyncIterator]();
920
+ return {
921
+ next: () => inner.next(),
922
+ return: (value) => {
923
+ this.iteratorAttached = false;
924
+ this.eventQueue.clear();
925
+ return inner.return?.(value) ?? Promise.resolve({
926
+ value,
927
+ done: true
928
+ });
929
+ }
930
+ };
889
931
  }
890
932
  /**
891
933
  * @internal Debug-only: forcefully close the underlying WebSocket to
@@ -894,6 +936,15 @@ var RealtimeSttSession = class {
894
936
  __debugForceDisconnect() {
895
937
  this.ws?.close(4999, "debug: simulated disconnect");
896
938
  }
939
+ /**
940
+ * Push an event to the async iterator queue only when a consumer has
941
+ * attached via `[Symbol.asyncIterator]()`. Listener-only consumers
942
+ * (the documented `.on()` pattern) never drain the queue, so pushing
943
+ * unconditionally would leak buffered events on long-running sessions.
944
+ */
945
+ enqueueIfIterating(event) {
946
+ if (this.iteratorAttached) this.eventQueue.push(event);
947
+ }
897
948
  async createWebSocket() {
898
949
  return new Promise((resolve, reject) => {
899
950
  try {
@@ -943,21 +994,21 @@ var RealtimeSttSession = class {
943
994
  tokens: userTokens
944
995
  };
945
996
  this.emitter.emit("result", filteredResult);
946
- this.eventQueue.push({
997
+ this.enqueueIfIterating({
947
998
  kind: "result",
948
999
  data: filteredResult
949
1000
  });
950
1001
  if (hasEndpoint) {
951
1002
  this.emitter.emit("endpoint");
952
- this.eventQueue.push({ kind: "endpoint" });
1003
+ this.enqueueIfIterating({ kind: "endpoint" });
953
1004
  }
954
1005
  if (hasFinalized) {
955
1006
  this.emitter.emit("finalized");
956
- this.eventQueue.push({ kind: "finalized" });
1007
+ this.enqueueIfIterating({ kind: "finalized" });
957
1008
  }
958
1009
  if (result.finished) {
959
1010
  this.emitter.emit("finished");
960
- this.eventQueue.push({ kind: "finished" });
1011
+ this.enqueueIfIterating({ kind: "finished" });
961
1012
  this.settleFinish();
962
1013
  this.cleanup("finished", void 0, "finished");
963
1014
  }
@@ -1142,6 +1193,7 @@ var RealtimeTtsStream = class extends TypedEmitter {
1142
1193
  streamId;
1143
1194
  _state = "active";
1144
1195
  audioQueue = new AsyncEventQueue();
1196
+ iteratorAttached = false;
1145
1197
  connection;
1146
1198
  ownsConnection;
1147
1199
  /** @internal */
@@ -1220,9 +1272,37 @@ var RealtimeTtsStream = class extends TypedEmitter {
1220
1272
  this._endStream();
1221
1273
  if (this.ownsConnection) this.connection.close();
1222
1274
  }
1223
- /** Async iterator that yields decoded audio chunks. */
1275
+ /**
1276
+ * Async iterator that yields decoded audio chunks.
1277
+ *
1278
+ * The returned iterator's `return()` resets the internal iterator-attach
1279
+ * flag and drops any buffered audio, so consumers that exit `for await`
1280
+ * early (via `break` etc.) stop accruing memory while the stream keeps
1281
+ * receiving server audio.
1282
+ */
1224
1283
  [Symbol.asyncIterator]() {
1225
- return this.audioQueue[Symbol.asyncIterator]();
1284
+ this.iteratorAttached = true;
1285
+ const inner = this.audioQueue[Symbol.asyncIterator]();
1286
+ return {
1287
+ next: () => inner.next(),
1288
+ return: (value) => {
1289
+ this.iteratorAttached = false;
1290
+ this.audioQueue.clear();
1291
+ return inner.return?.(value) ?? Promise.resolve({
1292
+ value,
1293
+ done: true
1294
+ });
1295
+ }
1296
+ };
1297
+ }
1298
+ /**
1299
+ * Push an audio chunk to the async iterator queue only when a consumer
1300
+ * has attached via `[Symbol.asyncIterator]()`. Listener-only consumers
1301
+ * (the documented `.on('audio', ...)` pattern) never drain the queue,
1302
+ * so pushing unconditionally would leak buffered chunks.
1303
+ */
1304
+ enqueueIfIterating(chunk) {
1305
+ if (this.iteratorAttached) this.audioQueue.push(chunk);
1226
1306
  }
1227
1307
  /** @internal Dispatch a server event to this stream. */
1228
1308
  _handleEvent(event) {
@@ -1239,7 +1319,7 @@ var RealtimeTtsStream = class extends TypedEmitter {
1239
1319
  if (event.audio !== void 0) {
1240
1320
  const chunk = decodeBase64ToUint8Array(event.audio);
1241
1321
  this.emit("audio", chunk);
1242
- this.audioQueue.push(chunk);
1322
+ this.enqueueIfIterating(chunk);
1243
1323
  }
1244
1324
  if (event.audio_end) this.emit("audioEnd");
1245
1325
  if (event.terminated) this._endStream();