@livekit/rtc-node 0.13.32 → 0.13.34

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.
Files changed (141) hide show
  1. package/dist/async_queue.d.ts +2 -3
  2. package/dist/audio_filter.d.ts +2 -3
  3. package/dist/audio_frame.d.ts +5 -6
  4. package/dist/audio_mixer.d.ts +3 -6
  5. package/dist/audio_resampler.d.ts +3 -6
  6. package/dist/audio_source.d.ts +5 -6
  7. package/dist/{audio_stream-4M8XlO4-.d.cts → audio_stream-BNVaONuH.d.cts} +36 -10
  8. package/dist/audio_stream.d.cts +2 -2
  9. package/dist/audio_stream.d.ts +61 -18
  10. package/dist/data_streams/index.d.cts +1 -1
  11. package/dist/data_streams/index.d.ts +4 -3
  12. package/dist/data_streams/stream_reader.cjs +97 -58
  13. package/dist/data_streams/stream_reader.cjs.map +1 -1
  14. package/dist/data_streams/stream_reader.d.cts +1 -1
  15. package/dist/data_streams/stream_reader.d.ts +65 -2
  16. package/dist/data_streams/stream_reader.d.ts.map +1 -1
  17. package/dist/data_streams/stream_writer.d.cts +1 -1
  18. package/dist/data_streams/stream_writer.d.ts +5 -7
  19. package/dist/data_streams/types.cjs.map +1 -1
  20. package/dist/data_streams/types.d.cts +1 -1
  21. package/dist/data_streams/types.d.ts +47 -2
  22. package/dist/data_streams/types.d.ts.map +1 -1
  23. package/dist/e2ee.cjs +5 -1
  24. package/dist/e2ee.cjs.map +1 -1
  25. package/dist/e2ee.d.cts +3 -1
  26. package/dist/e2ee.d.ts +11 -11
  27. package/dist/e2ee.d.ts.map +1 -1
  28. package/dist/ffi_client.d.ts +8 -10
  29. package/dist/frame_processor.d.ts +8 -11
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +2 -2
  32. package/dist/index.d.ts +22 -17
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +1 -107
  35. package/dist/log.d.ts +5 -7
  36. package/dist/participant.cjs +179 -192
  37. package/dist/participant.cjs.map +1 -1
  38. package/dist/participant.d.cts +2 -2
  39. package/dist/participant.d.ts +169 -18
  40. package/dist/participant.d.ts.map +1 -1
  41. package/dist/room.cjs +166 -120
  42. package/dist/room.cjs.map +1 -1
  43. package/dist/room.d.cts +2 -2
  44. package/dist/room.d.ts +228 -18
  45. package/dist/room.d.ts.map +1 -1
  46. package/dist/rpc.d.ts +7 -9
  47. package/dist/tests/e2e_common.cjs +157 -0
  48. package/dist/tests/e2e_common.cjs.map +1 -0
  49. package/dist/tests/e2e_common.d.cts +56 -0
  50. package/dist/tests/e2e_common.d.ts +37 -0
  51. package/dist/tests/e2e_common.d.ts.map +1 -0
  52. package/dist/track.d.cts +2 -2
  53. package/dist/track.d.ts +67 -18
  54. package/dist/track_publication.d.cts +2 -2
  55. package/dist/track_publication.d.ts +45 -18
  56. package/dist/transcription.d.ts +3 -4
  57. package/dist/{types-_PdPVish.d.ts → types-f7wTlFjs.d.cts} +34 -11
  58. package/dist/types.d.ts +2 -3
  59. package/dist/utils.cjs +27 -2
  60. package/dist/utils.cjs.map +1 -1
  61. package/dist/utils.d.cts +8 -1
  62. package/dist/utils.d.ts +10 -5
  63. package/dist/utils.d.ts.map +1 -1
  64. package/dist/version.cjs +1 -1
  65. package/dist/version.cjs.map +1 -1
  66. package/dist/version.d.cts +1 -1
  67. package/dist/version.d.ts +2 -3
  68. package/dist/video_frame.d.ts +4 -5
  69. package/dist/video_source.d.ts +6 -8
  70. package/dist/video_stream.d.cts +2 -2
  71. package/dist/video_stream.d.ts +5 -22
  72. package/package.json +6 -7
  73. package/src/audio_stream_room_lifecycle.test.ts +4 -1
  74. package/src/data_streams/stream_reader.ts +132 -82
  75. package/src/data_streams/types.ts +6 -0
  76. package/src/e2ee.ts +7 -0
  77. package/src/index.ts +8 -1
  78. package/src/participant.ts +252 -223
  79. package/src/room.ts +233 -134
  80. package/src/tests/e2e.test.ts +50 -139
  81. package/src/tests/e2e_common.ts +163 -0
  82. package/src/tests/e2e_data_streams.test.ts +639 -0
  83. package/src/utils.ts +30 -0
  84. package/src/version.ts +1 -1
  85. package/dist/async_queue.js +0 -56
  86. package/dist/async_queue.js.map +0 -1
  87. package/dist/audio_filter.js +0 -24
  88. package/dist/audio_filter.js.map +0 -1
  89. package/dist/audio_frame.js +0 -78
  90. package/dist/audio_frame.js.map +0 -1
  91. package/dist/audio_mixer.js +0 -269
  92. package/dist/audio_mixer.js.map +0 -1
  93. package/dist/audio_resampler.js +0 -154
  94. package/dist/audio_resampler.js.map +0 -1
  95. package/dist/audio_source.js +0 -134
  96. package/dist/audio_source.js.map +0 -1
  97. package/dist/audio_stream-CTyN1Ldy.d.ts +0 -523
  98. package/dist/audio_stream.js +0 -122
  99. package/dist/audio_stream.js.map +0 -1
  100. package/dist/data_streams/index.js +0 -3
  101. package/dist/data_streams/index.js.map +0 -1
  102. package/dist/data_streams/stream_reader.js +0 -124
  103. package/dist/data_streams/stream_reader.js.map +0 -1
  104. package/dist/data_streams/stream_writer.js +0 -26
  105. package/dist/data_streams/stream_writer.js.map +0 -1
  106. package/dist/data_streams/types.js +0 -1
  107. package/dist/data_streams/types.js.map +0 -1
  108. package/dist/e2ee.js +0 -251
  109. package/dist/e2ee.js.map +0 -1
  110. package/dist/ffi_client.js +0 -89
  111. package/dist/ffi_client.js.map +0 -1
  112. package/dist/frame_processor.js +0 -29
  113. package/dist/frame_processor.js.map +0 -1
  114. package/dist/index.js.map +0 -1
  115. package/dist/log.js +0 -9
  116. package/dist/log.js.map +0 -1
  117. package/dist/participant.js +0 -715
  118. package/dist/participant.js.map +0 -1
  119. package/dist/room.js +0 -900
  120. package/dist/room.js.map +0 -1
  121. package/dist/rpc.js +0 -65
  122. package/dist/rpc.js.map +0 -1
  123. package/dist/track.js +0 -234
  124. package/dist/track.js.map +0 -1
  125. package/dist/track_publication.js +0 -100
  126. package/dist/track_publication.js.map +0 -1
  127. package/dist/transcription.js +0 -1
  128. package/dist/transcription.js.map +0 -1
  129. package/dist/types-_PdPVish.d.cts +0 -87
  130. package/dist/types.js +0 -1
  131. package/dist/types.js.map +0 -1
  132. package/dist/utils.js +0 -35
  133. package/dist/utils.js.map +0 -1
  134. package/dist/version.js +0 -5
  135. package/dist/version.js.map +0 -1
  136. package/dist/video_frame.js +0 -207
  137. package/dist/video_frame.js.map +0 -1
  138. package/dist/video_source.js +0 -53
  139. package/dist/video_source.js.map +0 -1
  140. package/dist/video_stream.js +0 -70
  141. package/dist/video_stream.js.map +0 -1
package/dist/room.cjs CHANGED
@@ -40,8 +40,10 @@ var import_rtc_ffi_bindings = require("@livekit/rtc-ffi-bindings");
40
40
  var import_rtc_ffi_bindings2 = require("@livekit/rtc-ffi-bindings");
41
41
  var import_rtc_ffi_bindings3 = require("@livekit/rtc-ffi-bindings");
42
42
  var import_rtc_ffi_bindings4 = require("@livekit/rtc-ffi-bindings");
43
+ var import_rtc_ffi_bindings5 = require("@livekit/rtc-ffi-bindings");
43
44
  var import_events = __toESM(require("events"), 1);
44
45
  var import_stream_reader = require("./data_streams/stream_reader.cjs");
46
+ var import_types = require("./data_streams/types.cjs");
45
47
  var import_e2ee = require("./e2ee.cjs");
46
48
  var import_ffi_client = require("./ffi_client.cjs");
47
49
  var import_log = require("./log.cjs");
@@ -50,11 +52,11 @@ var import_track = require("./track.cjs");
50
52
  var import_track_publication = require("./track_publication.cjs");
51
53
  var import_utils = require("./utils.cjs");
52
54
  const defaultRtcConfiguration = {
53
- iceTransportType: import_rtc_ffi_bindings3.IceTransportType.TRANSPORT_ALL,
54
- continualGatheringPolicy: import_rtc_ffi_bindings3.ContinualGatheringPolicy.GATHER_CONTINUALLY,
55
+ iceTransportType: import_rtc_ffi_bindings4.IceTransportType.TRANSPORT_ALL,
56
+ continualGatheringPolicy: import_rtc_ffi_bindings4.ContinualGatheringPolicy.GATHER_CONTINUALLY,
55
57
  iceServers: []
56
58
  };
57
- const defaultRoomOptions = new import_rtc_ffi_bindings3.RoomOptions({
59
+ const defaultRoomOptions = new import_rtc_ffi_bindings4.RoomOptions({
58
60
  autoSubscribe: true,
59
61
  dynacast: false,
60
62
  e2ee: void 0,
@@ -72,8 +74,9 @@ class Room extends import_events.default {
72
74
  * before processing the next events
73
75
  */
74
76
  this.ffiEventLock = new import_mutex.Mutex();
75
- this.byteStreamControllers = /* @__PURE__ */ new Map();
76
- this.textStreamControllers = /* @__PURE__ */ new Map();
77
+ // Active incoming stream readers keyed by their FFI reader handle id. Each entry
78
+ // owns the FfiClient listener feeding reader events into the ReadableStream.
79
+ this.streamReaders = /* @__PURE__ */ new Map();
77
80
  this.byteStreamHandlers = /* @__PURE__ */ new Map();
78
81
  this.textStreamHandlers = /* @__PURE__ */ new Map();
79
82
  this.preConnectEvents = [];
@@ -84,7 +87,7 @@ class Room extends import_events.default {
84
87
  // events fire exactly once, no matter which path (explicit disconnect()
85
88
  // vs. FFI 'disconnected' event) wins the race.
86
89
  this.hasCleanedUp = false;
87
- this._connectionState = import_rtc_ffi_bindings3.ConnectionState.CONN_DISCONNECTED;
90
+ this._connectionState = import_rtc_ffi_bindings4.ConnectionState.CONN_DISCONNECTED;
88
91
  this.remoteParticipants = /* @__PURE__ */ new Map();
89
92
  this.onFfiEvent = async (ffiEvent) => {
90
93
  const unlock = await this.ffiEventLock.lock();
@@ -211,9 +214,9 @@ class Room extends import_events.default {
211
214
  trackInfo.sid
212
215
  );
213
216
  publication.subscribed = true;
214
- if (trackInfo.kind == import_rtc_ffi_bindings4.TrackKind.KIND_VIDEO) {
217
+ if (trackInfo.kind == import_rtc_ffi_bindings5.TrackKind.KIND_VIDEO) {
215
218
  publication.track = new import_track.RemoteVideoTrack(ownedTrack);
216
- } else if (trackInfo.kind == import_rtc_ffi_bindings4.TrackKind.KIND_AUDIO) {
219
+ } else if (trackInfo.kind == import_rtc_ffi_bindings5.TrackKind.KIND_AUDIO) {
217
220
  publication.track = new import_track.RemoteAudioTrack(ownedTrack);
218
221
  }
219
222
  (_b = publication.track) == null ? void 0 : _b.setRoom(this);
@@ -384,12 +387,10 @@ class Room extends import_events.default {
384
387
  this.emit("reconnected" /* Reconnected */);
385
388
  } else if (ev.case == "roomSidChanged") {
386
389
  this.emit("roomSidChanged" /* RoomSidChanged */, ev.value.sid);
387
- } else if (ev.case === "streamHeaderReceived" && ev.value.header) {
388
- this.handleStreamHeader(ev.value.header, ev.value.participantIdentity);
389
- } else if (ev.case === "streamChunkReceived" && ev.value.chunk) {
390
- this.handleStreamChunk(ev.value.chunk);
391
- } else if (ev.case === "streamTrailerReceived" && ev.value.trailer) {
392
- this.handleStreamTrailer(ev.value.trailer);
390
+ } else if (ev.case === "byteStreamOpened" && ev.value.reader) {
391
+ this.handleByteStreamOpened(ev.value.reader, ev.value.participantIdentity);
392
+ } else if (ev.case === "textStreamOpened" && ev.value.reader) {
393
+ this.handleTextStreamOpened(ev.value.reader, ev.value.participantIdentity);
393
394
  } else if (ev.case === "roomUpdated") {
394
395
  this.info = ev.value;
395
396
  this.emit("roomUpdated" /* RoomUpdated */);
@@ -432,7 +433,7 @@ class Room extends import_events.default {
432
433
  return (_a = this.info) == null ? void 0 : _a.metadata;
433
434
  }
434
435
  get isConnected() {
435
- return this.ffiHandle != void 0 && this.connectionState != import_rtc_ffi_bindings3.ConnectionState.CONN_DISCONNECTED;
436
+ return this.ffiHandle != void 0 && this.connectionState != import_rtc_ffi_bindings4.ConnectionState.CONN_DISCONNECTED;
436
437
  }
437
438
  /** @internal */
438
439
  get token() {
@@ -547,13 +548,33 @@ class Room extends import_events.default {
547
548
  * @throws ConnectError - if connection fails
548
549
  */
549
550
  async connect(url, token, opts) {
550
- const options = { ...defaultRoomOptions, ...opts };
551
+ const { dataStream, ...restOpts } = opts ?? {};
552
+ const options = { ...defaultRoomOptions, ...restOpts };
551
553
  const e2eeEnabled = options.encryption || options.e2ee;
552
- const e2eeOptions = options.encryption ? { ...import_e2ee.defaultE2EEOptions, ...options.encryption } : { ...import_e2ee.defaultE2EEOptions, ...options.e2ee };
553
- const req = new import_rtc_ffi_bindings3.ConnectRequest({
554
+ const userE2EEOptions = options.encryption ?? options.e2ee;
555
+ const e2eeOptions = {
556
+ ...import_e2ee.defaultE2EEOptions,
557
+ ...userE2EEOptions,
558
+ keyProviderOptions: {
559
+ ...import_e2ee.defaultE2EEOptions.keyProviderOptions,
560
+ ...userE2EEOptions == null ? void 0 : userE2EEOptions.keyProviderOptions
561
+ }
562
+ };
563
+ const mergedOptions = { ...options };
564
+ if (options.encryption) {
565
+ mergedOptions.encryption = e2eeOptions;
566
+ } else if (options.e2ee) {
567
+ mergedOptions.e2ee = e2eeOptions;
568
+ }
569
+ if (dataStream) {
570
+ mergedOptions.dataStream = new import_rtc_ffi_bindings4.RoomDataStreamOptions({
571
+ maxPayloadByteLength: (0, import_utils.numberToBigInt)(dataStream.maxPayloadByteLength)
572
+ });
573
+ }
574
+ const req = new import_rtc_ffi_bindings4.ConnectRequest({
554
575
  url,
555
576
  token,
556
- options
577
+ options: mergedOptions
557
578
  });
558
579
  import_ffi_client.FfiClient.instance.on(import_ffi_client.FfiClientEvent.FfiEvent, this.onFfiEvent);
559
580
  try {
@@ -588,7 +609,7 @@ class Room extends import_events.default {
588
609
  rp.trackPublications.set(publication.sid, publication);
589
610
  }
590
611
  }
591
- this.updateConnectionState(import_rtc_ffi_bindings3.ConnectionState.CONN_CONNECTED);
612
+ this.updateConnectionState(import_rtc_ffi_bindings4.ConnectionState.CONN_CONNECTED);
592
613
  import_ffi_client.FfiClient.instance.request({
593
614
  message: {
594
615
  case: "readyForRoomEvent",
@@ -674,21 +695,16 @@ class Room extends import_events.default {
674
695
  var _a, _b;
675
696
  if (this.hasCleanedUp) return;
676
697
  this.hasCleanedUp = true;
677
- for (const [, streamController] of this.byteStreamControllers) {
678
- try {
679
- streamController.controller.error(new Error("Disconnected while receiving"));
680
- } catch {
681
- }
682
- }
683
- this.byteStreamControllers.clear();
684
- for (const [, streamController] of this.textStreamControllers) {
698
+ for (const [, { controller, listener }] of this.streamReaders) {
699
+ import_ffi_client.FfiClient.instance.off(import_ffi_client.FfiClientEvent.FfiEvent, listener);
685
700
  try {
686
- streamController.controller.error(new Error("Disconnected while receiving"));
701
+ controller.error(new Error("Disconnected while receiving"));
687
702
  } catch {
688
703
  }
689
704
  }
690
- this.textStreamControllers.clear();
705
+ this.streamReaders.clear();
691
706
  if (this.localParticipant) {
707
+ this.localParticipant.disposeOpenStreamWriters();
692
708
  for (const pub of this.localParticipant.trackPublications.values()) {
693
709
  (_a = pub.track) == null ? void 0 : _a.setRoom(null);
694
710
  }
@@ -700,7 +716,7 @@ class Room extends import_events.default {
700
716
  }
701
717
  this.sidPromise = void 0;
702
718
  this.disconnectController.abort();
703
- this.updateConnectionState(import_rtc_ffi_bindings3.ConnectionState.CONN_DISCONNECTED);
719
+ this.updateConnectionState(import_rtc_ffi_bindings4.ConnectionState.CONN_DISCONNECTED);
704
720
  this.emit("disconnected" /* Disconnected */, reason);
705
721
  }
706
722
  /**
@@ -784,100 +800,130 @@ class Room extends import_events.default {
784
800
  this.remoteParticipants.set(ownedInfo.info.identity, participant);
785
801
  return participant;
786
802
  }
787
- handleStreamHeader(streamHeader, participantIdentity) {
788
- if (streamHeader.contentHeader.case === "byteHeader") {
789
- const streamHandlerCallback = this.byteStreamHandlers.get(streamHeader.topic ?? "");
790
- if (!streamHandlerCallback) {
791
- import_log.log.debug(
792
- "ignoring incoming byte stream due to no handler for topic: %s",
793
- streamHeader.topic ?? "undefined"
794
- );
795
- return;
796
- }
797
- let streamController;
798
- const stream = new ReadableStream({
799
- start: (controller) => {
800
- streamController = controller;
801
- this.byteStreamControllers.set(streamHeader.streamId, {
802
- header: streamHeader,
803
- controller: streamController,
804
- startTime: Date.now()
805
- });
806
- }
807
- });
808
- const info = {
809
- streamId: streamHeader.streamId,
810
- name: streamHeader.contentHeader.value.name ?? "unknown",
811
- mimeType: streamHeader.mimeType,
812
- totalSize: streamHeader.totalLength ? Number(streamHeader.totalLength) : void 0,
813
- topic: streamHeader.topic,
814
- timestamp: (0, import_utils.bigIntToNumber)(streamHeader.timestamp),
815
- attributes: streamHeader.attributes
816
- };
817
- streamHandlerCallback(
818
- new import_stream_reader.ByteStreamReader(info, stream, (0, import_utils.bigIntToNumber)(streamHeader.totalLength)),
819
- { identity: participantIdentity }
820
- );
821
- } else if (streamHeader.contentHeader.case === "textHeader") {
822
- const streamHandlerCallback = this.textStreamHandlers.get(streamHeader.topic ?? "");
823
- if (!streamHandlerCallback) {
824
- import_log.log.debug(
825
- "ignoring incoming text stream due to no handler for topic: %s",
826
- streamHeader.topic ?? "undefined"
827
- );
803
+ handleByteStreamOpened(ownedReader, participantIdentity) {
804
+ const info = (0, import_utils.byteStreamInfoFromProto)(ownedReader.info);
805
+ const readerHandle = ownedReader.handle.id;
806
+ const streamHandlerCallback = this.byteStreamHandlers.get(info.topic);
807
+ if (!streamHandlerCallback) {
808
+ import_log.log.debug("ignoring incoming byte stream due to no handler for topic: %s", info.topic);
809
+ new import_ffi_client.FfiHandle(readerHandle).dispose();
810
+ return;
811
+ }
812
+ let listener = null;
813
+ const unsubscribe = () => {
814
+ if (!listener) {
828
815
  return;
829
816
  }
830
- let streamController;
831
- const stream = new ReadableStream({
832
- start: (controller) => {
833
- streamController = controller;
834
- this.textStreamControllers.set(streamHeader.streamId, {
835
- header: streamHeader,
836
- controller: streamController,
837
- startTime: Date.now()
838
- });
839
- }
840
- });
841
- const info = {
842
- streamId: streamHeader.streamId,
843
- mimeType: streamHeader.mimeType,
844
- totalSize: streamHeader.totalLength ? Number(streamHeader.totalLength) : void 0,
845
- topic: streamHeader.topic,
846
- timestamp: Number(streamHeader.timestamp),
847
- attributes: streamHeader.attributes
848
- };
849
- streamHandlerCallback(
850
- new import_stream_reader.TextStreamReader(info, stream, (0, import_utils.bigIntToNumber)(streamHeader.totalLength)),
851
- { identity: participantIdentity }
852
- );
853
- }
817
+ import_ffi_client.FfiClient.instance.off(import_ffi_client.FfiClientEvent.FfiEvent, listener);
818
+ listener = null;
819
+ this.streamReaders.delete(readerHandle);
820
+ };
821
+ const stream = new ReadableStream({
822
+ start: (controller) => {
823
+ listener = (ev) => {
824
+ if (ev.message.case !== "byteStreamReaderEvent" || ev.message.value.readerHandle !== readerHandle) {
825
+ return;
826
+ }
827
+ const detail = ev.message.value.detail;
828
+ if (detail.case === "chunkReceived") {
829
+ const content = detail.value.content;
830
+ if (content.length > 0) {
831
+ controller.enqueue(new import_rtc_ffi_bindings4.DataStream_Chunk({ content }));
832
+ }
833
+ } else if (detail.case === "eos") {
834
+ unsubscribe();
835
+ const error = detail.value.error;
836
+ if (error) {
837
+ controller.error(new Error(error.description ?? "stream terminated"));
838
+ } else {
839
+ controller.close();
840
+ }
841
+ }
842
+ };
843
+ import_ffi_client.FfiClient.instance.on(import_ffi_client.FfiClientEvent.FfiEvent, listener);
844
+ this.streamReaders.set(readerHandle, { controller, listener });
845
+ import_ffi_client.FfiClient.instance.request({
846
+ message: {
847
+ case: "byteReadIncremental",
848
+ value: new import_rtc_ffi_bindings4.ByteStreamReaderReadIncrementalRequest({ readerHandle })
849
+ }
850
+ });
851
+ },
852
+ // The consumer stopped reading before end-of-stream — ByteStreamReader
853
+ // .close(), or breaking out of a for-await. The eos event that would have
854
+ // unsubscribed is never consumed, so the listener would otherwise be run
855
+ // against every FFI event for the rest of the process. Only the
856
+ // subscription is released: the reader handle was consumed by the
857
+ // readIncremental request above, so there is nothing left to dispose.
858
+ cancel: () => unsubscribe()
859
+ });
860
+ streamHandlerCallback(
861
+ new import_stream_reader.ByteStreamReader(info, stream, (0, import_utils.bigIntToNumber)(ownedReader.info.totalLength)),
862
+ { identity: participantIdentity }
863
+ );
854
864
  }
855
- handleStreamChunk(chunk) {
856
- const fileBuffer = this.byteStreamControllers.get(chunk.streamId);
857
- if (fileBuffer) {
858
- if (chunk.content.length > 0) {
859
- fileBuffer.controller.enqueue(chunk);
860
- }
865
+ handleTextStreamOpened(ownedReader, participantIdentity) {
866
+ const info = (0, import_utils.textStreamInfoFromProto)(ownedReader.info);
867
+ const readerHandle = ownedReader.handle.id;
868
+ const streamHandlerCallback = this.textStreamHandlers.get(info.topic);
869
+ if (!streamHandlerCallback) {
870
+ import_log.log.debug("ignoring incoming text stream due to no handler for topic: %s", info.topic);
871
+ new import_ffi_client.FfiHandle(readerHandle).dispose();
872
+ return;
861
873
  }
862
- const textBuffer = this.textStreamControllers.get(chunk.streamId);
863
- if (textBuffer) {
864
- if (chunk.content.length > 0) {
865
- textBuffer.controller.enqueue(chunk);
874
+ const textEncoder = new TextEncoder();
875
+ let listener = null;
876
+ const unsubscribe = () => {
877
+ if (!listener) {
878
+ return;
866
879
  }
867
- }
868
- }
869
- handleStreamTrailer(trailer) {
870
- const streamId = trailer.streamId;
871
- const fileBuffer = this.byteStreamControllers.get(streamId);
872
- if (fileBuffer) {
873
- fileBuffer.controller.close();
874
- this.byteStreamControllers.delete(streamId);
875
- }
876
- const textBuffer = this.textStreamControllers.get(streamId);
877
- if (textBuffer) {
878
- textBuffer.controller.close();
879
- this.textStreamControllers.delete(streamId);
880
- }
880
+ import_ffi_client.FfiClient.instance.off(import_ffi_client.FfiClientEvent.FfiEvent, listener);
881
+ listener = null;
882
+ this.streamReaders.delete(readerHandle);
883
+ };
884
+ const stream = new ReadableStream({
885
+ start: (controller) => {
886
+ listener = (ev) => {
887
+ if (ev.message.case !== "textStreamReaderEvent" || ev.message.value.readerHandle !== readerHandle) {
888
+ return;
889
+ }
890
+ const detail = ev.message.value.detail;
891
+ if (detail.case === "chunkReceived") {
892
+ const content = textEncoder.encode(detail.value.content);
893
+ if (content.length > 0) {
894
+ controller.enqueue(new import_rtc_ffi_bindings4.DataStream_Chunk({ content }));
895
+ }
896
+ } else if (detail.case === "eos") {
897
+ unsubscribe();
898
+ const error = detail.value.error;
899
+ if (error) {
900
+ controller.error(new Error(error.description ?? "stream terminated"));
901
+ } else {
902
+ controller.close();
903
+ }
904
+ }
905
+ };
906
+ import_ffi_client.FfiClient.instance.on(import_ffi_client.FfiClientEvent.FfiEvent, listener);
907
+ this.streamReaders.set(readerHandle, { controller, listener });
908
+ import_ffi_client.FfiClient.instance.request({
909
+ message: {
910
+ case: "textReadIncremental",
911
+ value: new import_rtc_ffi_bindings4.TextStreamReaderReadIncrementalRequest({ readerHandle })
912
+ }
913
+ });
914
+ },
915
+ // The consumer stopped reading before end-of-stream — TextStreamReader
916
+ // .close(), or breaking out of a for-await. The eos event that would have
917
+ // unsubscribed is never consumed, so the listener would otherwise be run
918
+ // against every FFI event for the rest of the process. Only the
919
+ // subscription is released: the reader handle was consumed by the
920
+ // readIncremental request above, so there is nothing left to dispose.
921
+ cancel: () => unsubscribe()
922
+ });
923
+ streamHandlerCallback(
924
+ new import_stream_reader.TextStreamReader(info, stream, (0, import_utils.bigIntToNumber)(ownedReader.info.totalLength)),
925
+ { identity: participantIdentity }
926
+ );
881
927
  }
882
928
  }
883
929
  class ConnectError extends Error {