livekit-client 2.15.4 → 2.15.5

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 (62) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +329 -124
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +834 -541
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/e2ee/worker/FrameCryptor.d.ts +0 -46
  10. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  11. package/dist/src/e2ee/worker/naluUtils.d.ts +27 -0
  12. package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -0
  13. package/dist/src/index.d.ts +2 -2
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/room/Room.d.ts +6 -10
  16. package/dist/src/room/Room.d.ts.map +1 -1
  17. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +20 -0
  18. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -0
  19. package/dist/src/room/{StreamReader.d.ts → data-stream/incoming/StreamReader.d.ts} +32 -6
  20. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -0
  21. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +27 -0
  22. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -0
  23. package/dist/src/room/{StreamWriter.d.ts → data-stream/outgoing/StreamWriter.d.ts} +1 -1
  24. package/dist/src/room/data-stream/outgoing/StreamWriter.d.ts.map +1 -0
  25. package/dist/src/room/errors.d.ts +13 -0
  26. package/dist/src/room/errors.d.ts.map +1 -1
  27. package/dist/src/room/participant/LocalParticipant.d.ts +32 -19
  28. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  29. package/dist/src/room/track/LocalTrack.d.ts +7 -2
  30. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  31. package/dist/src/room/types.d.ts +17 -1
  32. package/dist/src/room/types.d.ts.map +1 -1
  33. package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +0 -46
  34. package/dist/ts4.2/src/e2ee/worker/naluUtils.d.ts +27 -0
  35. package/dist/ts4.2/src/index.d.ts +2 -2
  36. package/dist/ts4.2/src/room/Room.d.ts +6 -10
  37. package/dist/ts4.2/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +20 -0
  38. package/dist/ts4.2/src/room/{StreamReader.d.ts → data-stream/incoming/StreamReader.d.ts} +32 -6
  39. package/dist/ts4.2/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +27 -0
  40. package/dist/ts4.2/src/room/{StreamWriter.d.ts → data-stream/outgoing/StreamWriter.d.ts} +1 -1
  41. package/dist/ts4.2/src/room/errors.d.ts +13 -0
  42. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +32 -19
  43. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +7 -2
  44. package/dist/ts4.2/src/room/types.d.ts +17 -1
  45. package/package.json +1 -1
  46. package/src/e2ee/worker/FrameCryptor.ts +48 -139
  47. package/src/e2ee/worker/naluUtils.ts +328 -0
  48. package/src/index.ts +2 -2
  49. package/src/room/Room.ts +93 -206
  50. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +247 -0
  51. package/src/room/data-stream/incoming/StreamReader.ts +317 -0
  52. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +316 -0
  53. package/src/room/{StreamWriter.ts → data-stream/outgoing/StreamWriter.ts} +1 -1
  54. package/src/room/errors.ts +34 -0
  55. package/src/room/participant/LocalParticipant.ts +39 -295
  56. package/src/room/track/LocalAudioTrack.ts +2 -2
  57. package/src/room/track/LocalTrack.ts +65 -48
  58. package/src/room/types.ts +22 -1
  59. package/src/room/utils.ts +2 -2
  60. package/dist/src/room/StreamReader.d.ts.map +0 -1
  61. package/dist/src/room/StreamWriter.d.ts.map +0 -1
  62. package/src/room/StreamReader.ts +0 -170
package/src/room/Room.ts CHANGED
@@ -4,9 +4,6 @@ import {
4
4
  ConnectionQualityUpdate,
5
5
  type DataPacket,
6
6
  DataPacket_Kind,
7
- DataStream_Chunk,
8
- DataStream_Header,
9
- DataStream_Trailer,
10
7
  DisconnectReason,
11
8
  JoinResponse,
12
9
  LeaveRequest,
@@ -48,12 +45,12 @@ import { getBrowser } from '../utils/browserParser';
48
45
  import DeviceManager from './DeviceManager';
49
46
  import RTCEngine from './RTCEngine';
50
47
  import { RegionUrlProvider } from './RegionUrlProvider';
48
+ import IncomingDataStreamManager from './data-stream/incoming/IncomingDataStreamManager';
51
49
  import {
52
50
  type ByteStreamHandler,
53
- ByteStreamReader,
54
51
  type TextStreamHandler,
55
- TextStreamReader,
56
- } from './StreamReader';
52
+ } from './data-stream/incoming/StreamReader';
53
+ import OutgoingDataStreamManager from './data-stream/outgoing/OutgoingDataStreamManager';
57
54
  import {
58
55
  audioDefaults,
59
56
  publishDefaults,
@@ -81,17 +78,13 @@ import type { TrackProcessor } from './track/processor/types';
81
78
  import type { AdaptiveStreamSettings } from './track/types';
82
79
  import { getNewAudioContext, kindToSource, sourceToKind } from './track/utils';
83
80
  import {
84
- type ByteStreamInfo,
85
81
  type ChatMessage,
86
82
  type SimulationOptions,
87
83
  type SimulationScenario,
88
- type StreamController,
89
- type TextStreamInfo,
90
84
  type TranscriptionSegment,
91
85
  } from './types';
92
86
  import {
93
87
  Future,
94
- bigIntToNumber,
95
88
  createDummyVideoStreamTrack,
96
89
  extractChatMessage,
97
90
  extractTranscriptionSegments,
@@ -199,13 +192,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
199
192
  */
200
193
  private transcriptionReceivedTimes: Map<string, number>;
201
194
 
202
- private byteStreamControllers = new Map<string, StreamController<DataStream_Chunk>>();
195
+ private incomingDataStreamManager: IncomingDataStreamManager;
203
196
 
204
- private textStreamControllers = new Map<string, StreamController<DataStream_Chunk>>();
205
-
206
- private byteStreamHandlers = new Map<string, ByteStreamHandler>();
207
-
208
- private textStreamHandlers = new Map<string, TextStreamHandler>();
197
+ private outgoingDataStreamManager: OutgoingDataStreamManager;
209
198
 
210
199
  private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>> = new Map();
211
200
 
@@ -238,6 +227,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
238
227
 
239
228
  this.maybeCreateEngine();
240
229
 
230
+ this.incomingDataStreamManager = new IncomingDataStreamManager();
231
+ this.outgoingDataStreamManager = new OutgoingDataStreamManager(this.engine, this.log);
232
+
241
233
  this.disconnectLock = new Mutex();
242
234
 
243
235
  this.localParticipant = new LocalParticipant(
@@ -246,6 +238,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
246
238
  this.engine,
247
239
  this.options,
248
240
  this.rpcHandlers,
241
+ this.outgoingDataStreamManager,
249
242
  );
250
243
 
251
244
  if (this.options.videoCaptureDefaults.deviceId) {
@@ -288,25 +281,19 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
288
281
  }
289
282
 
290
283
  registerTextStreamHandler(topic: string, callback: TextStreamHandler) {
291
- if (this.textStreamHandlers.has(topic)) {
292
- throw new TypeError(`A text stream handler for topic "${topic}" has already been set.`);
293
- }
294
- this.textStreamHandlers.set(topic, callback);
284
+ return this.incomingDataStreamManager.registerTextStreamHandler(topic, callback);
295
285
  }
296
286
 
297
287
  unregisterTextStreamHandler(topic: string) {
298
- this.textStreamHandlers.delete(topic);
288
+ return this.incomingDataStreamManager.unregisterTextStreamHandler(topic);
299
289
  }
300
290
 
301
291
  registerByteStreamHandler(topic: string, callback: ByteStreamHandler) {
302
- if (this.byteStreamHandlers.has(topic)) {
303
- throw new TypeError(`A byte stream handler for topic "${topic}" has already been set.`);
304
- }
305
- this.byteStreamHandlers.set(topic, callback);
292
+ return this.incomingDataStreamManager.registerByteStreamHandler(topic, callback);
306
293
  }
307
294
 
308
295
  unregisterByteStreamHandler(topic: string) {
309
- this.byteStreamHandlers.delete(topic);
296
+ return this.incomingDataStreamManager.unregisterByteStreamHandler(topic);
310
297
  }
311
298
 
312
299
  /**
@@ -353,68 +340,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
353
340
  this.rpcHandlers.delete(method);
354
341
  }
355
342
 
356
- private async handleIncomingRpcRequest(
357
- callerIdentity: string,
358
- requestId: string,
359
- method: string,
360
- payload: string,
361
- responseTimeout: number,
362
- version: number,
363
- ) {
364
- await this.engine.publishRpcAck(callerIdentity, requestId);
365
-
366
- if (version !== 1) {
367
- await this.engine.publishRpcResponse(
368
- callerIdentity,
369
- requestId,
370
- null,
371
- RpcError.builtIn('UNSUPPORTED_VERSION'),
372
- );
373
- return;
374
- }
375
-
376
- const handler = this.rpcHandlers.get(method);
377
-
378
- if (!handler) {
379
- await this.engine.publishRpcResponse(
380
- callerIdentity,
381
- requestId,
382
- null,
383
- RpcError.builtIn('UNSUPPORTED_METHOD'),
384
- );
385
- return;
386
- }
387
-
388
- let responseError: RpcError | null = null;
389
- let responsePayload: string | null = null;
390
-
391
- try {
392
- const response = await handler({
393
- requestId,
394
- callerIdentity,
395
- payload,
396
- responseTimeout,
397
- });
398
- if (byteLength(response) > MAX_PAYLOAD_BYTES) {
399
- responseError = RpcError.builtIn('RESPONSE_PAYLOAD_TOO_LARGE');
400
- console.warn(`RPC Response payload too large for ${method}`);
401
- } else {
402
- responsePayload = response;
403
- }
404
- } catch (error) {
405
- if (error instanceof RpcError) {
406
- responseError = error;
407
- } else {
408
- console.warn(
409
- `Uncaught error returned by RPC handler for ${method}. Returning APPLICATION_ERROR instead.`,
410
- error,
411
- );
412
- responseError = RpcError.builtIn('APPLICATION_ERROR');
413
- }
414
- }
415
- await this.engine.publishRpcResponse(callerIdentity, requestId, responsePayload, responseError);
416
- }
417
-
418
343
  /**
419
344
  * @experimental
420
345
  */
@@ -616,6 +541,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
616
541
  if (this.e2eeManager) {
617
542
  this.e2eeManager.setupEngine(this.engine);
618
543
  }
544
+ if (this.outgoingDataStreamManager) {
545
+ this.outgoingDataStreamManager.setupEngine(this.engine);
546
+ }
619
547
  }
620
548
 
621
549
  /**
@@ -1545,6 +1473,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1545
1473
  this.isResuming = false;
1546
1474
  this.bufferedEvents = [];
1547
1475
  this.transcriptionReceivedTimes.clear();
1476
+ this.incomingDataStreamManager.clearHandlersAndControllers();
1548
1477
  if (this.state === ConnectionState.Disconnected) {
1549
1478
  return;
1550
1479
  }
@@ -1643,6 +1572,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1643
1572
  return;
1644
1573
  }
1645
1574
 
1575
+ this.incomingDataStreamManager.validateParticipantHasNoActiveDataStreams(identity);
1576
+
1646
1577
  participant.trackPublications.forEach((publication) => {
1647
1578
  participant.unpublishTrack(publication.trackSid, true);
1648
1579
  });
@@ -1784,12 +1715,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1784
1715
  this.handleChatMessage(participant, packet.value.value);
1785
1716
  } else if (packet.value.case === 'metrics') {
1786
1717
  this.handleMetrics(packet.value.value, participant);
1787
- } else if (packet.value.case === 'streamHeader') {
1788
- this.handleStreamHeader(packet.value.value, packet.participantIdentity);
1789
- } else if (packet.value.case === 'streamChunk') {
1790
- this.handleStreamChunk(packet.value.value);
1791
- } else if (packet.value.case === 'streamTrailer') {
1792
- this.handleStreamTrailer(packet.value.value);
1718
+ } else if (
1719
+ packet.value.case === 'streamHeader' ||
1720
+ packet.value.case === 'streamChunk' ||
1721
+ packet.value.case === 'streamTrailer'
1722
+ ) {
1723
+ this.handleDataStream(packet);
1793
1724
  } else if (packet.value.case === 'rpcRequest') {
1794
1725
  const rpc = packet.value.value;
1795
1726
  this.handleIncomingRpcRequest(
@@ -1803,116 +1734,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1803
1734
  }
1804
1735
  };
1805
1736
 
1806
- private async handleStreamHeader(streamHeader: DataStream_Header, participantIdentity: string) {
1807
- if (streamHeader.contentHeader.case === 'byteHeader') {
1808
- const streamHandlerCallback = this.byteStreamHandlers.get(streamHeader.topic);
1809
-
1810
- if (!streamHandlerCallback) {
1811
- this.log.debug(
1812
- 'ignoring incoming byte stream due to no handler for topic',
1813
- streamHeader.topic,
1814
- );
1815
- return;
1816
- }
1817
- let streamController: ReadableStreamDefaultController<DataStream_Chunk>;
1818
- const info: ByteStreamInfo = {
1819
- id: streamHeader.streamId,
1820
- name: streamHeader.contentHeader.value.name ?? 'unknown',
1821
- mimeType: streamHeader.mimeType,
1822
- size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
1823
- topic: streamHeader.topic,
1824
- timestamp: bigIntToNumber(streamHeader.timestamp),
1825
- attributes: streamHeader.attributes,
1826
- };
1827
- const stream = new ReadableStream({
1828
- start: (controller) => {
1829
- streamController = controller;
1830
- this.byteStreamControllers.set(streamHeader.streamId, {
1831
- info,
1832
- controller: streamController,
1833
- startTime: Date.now(),
1834
- });
1835
- },
1836
- });
1837
- streamHandlerCallback(
1838
- new ByteStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)),
1839
- {
1840
- identity: participantIdentity,
1841
- },
1842
- );
1843
- } else if (streamHeader.contentHeader.case === 'textHeader') {
1844
- const streamHandlerCallback = this.textStreamHandlers.get(streamHeader.topic);
1845
-
1846
- if (!streamHandlerCallback) {
1847
- this.log.debug(
1848
- 'ignoring incoming text stream due to no handler for topic',
1849
- streamHeader.topic,
1850
- );
1851
- return;
1852
- }
1853
- let streamController: ReadableStreamDefaultController<DataStream_Chunk>;
1854
- const info: TextStreamInfo = {
1855
- id: streamHeader.streamId,
1856
- mimeType: streamHeader.mimeType,
1857
- size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
1858
- topic: streamHeader.topic,
1859
- timestamp: Number(streamHeader.timestamp),
1860
- attributes: streamHeader.attributes,
1861
- };
1862
-
1863
- const stream = new ReadableStream<DataStream_Chunk>({
1864
- start: (controller) => {
1865
- streamController = controller;
1866
- this.textStreamControllers.set(streamHeader.streamId, {
1867
- info,
1868
- controller: streamController,
1869
- startTime: Date.now(),
1870
- });
1871
- },
1872
- });
1873
- streamHandlerCallback(
1874
- new TextStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)),
1875
- { identity: participantIdentity },
1876
- );
1877
- }
1878
- }
1879
-
1880
- private handleStreamChunk(chunk: DataStream_Chunk) {
1881
- const fileBuffer = this.byteStreamControllers.get(chunk.streamId);
1882
- if (fileBuffer) {
1883
- if (chunk.content.length > 0) {
1884
- fileBuffer.controller.enqueue(chunk);
1885
- }
1886
- }
1887
- const textBuffer = this.textStreamControllers.get(chunk.streamId);
1888
- if (textBuffer) {
1889
- if (chunk.content.length > 0) {
1890
- textBuffer.controller.enqueue(chunk);
1891
- }
1892
- }
1893
- }
1894
-
1895
- private handleStreamTrailer(trailer: DataStream_Trailer) {
1896
- const textBuffer = this.textStreamControllers.get(trailer.streamId);
1897
- if (textBuffer) {
1898
- textBuffer.info.attributes = {
1899
- ...textBuffer.info.attributes,
1900
- ...trailer.attributes,
1901
- };
1902
- textBuffer.controller.close();
1903
- this.textStreamControllers.delete(trailer.streamId);
1904
- }
1905
-
1906
- const fileBuffer = this.byteStreamControllers.get(trailer.streamId);
1907
- if (fileBuffer) {
1908
- {
1909
- fileBuffer.info.attributes = { ...fileBuffer.info.attributes, ...trailer.attributes };
1910
- fileBuffer.controller.close();
1911
- this.byteStreamControllers.delete(trailer.streamId);
1912
- }
1913
- }
1914
- }
1915
-
1916
1737
  private handleUserPacket = (
1917
1738
  participant: RemoteParticipant | undefined,
1918
1739
  userPacket: UserPacket,
@@ -1931,8 +1752,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1931
1752
  participant?.emit(ParticipantEvent.SipDTMFReceived, dtmf);
1932
1753
  };
1933
1754
 
1934
- bufferedSegments: Map<string, TranscriptionSegmentModel> = new Map();
1935
-
1936
1755
  private handleTranscription = (
1937
1756
  _remoteParticipant: RemoteParticipant | undefined,
1938
1757
  transcription: TranscriptionModel,
@@ -1963,6 +1782,74 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1963
1782
  this.emit(RoomEvent.MetricsReceived, metrics, participant);
1964
1783
  };
1965
1784
 
1785
+ private handleDataStream = (packet: DataPacket) => {
1786
+ this.incomingDataStreamManager.handleDataStreamPacket(packet);
1787
+ };
1788
+
1789
+ private async handleIncomingRpcRequest(
1790
+ callerIdentity: string,
1791
+ requestId: string,
1792
+ method: string,
1793
+ payload: string,
1794
+ responseTimeout: number,
1795
+ version: number,
1796
+ ) {
1797
+ await this.engine.publishRpcAck(callerIdentity, requestId);
1798
+
1799
+ if (version !== 1) {
1800
+ await this.engine.publishRpcResponse(
1801
+ callerIdentity,
1802
+ requestId,
1803
+ null,
1804
+ RpcError.builtIn('UNSUPPORTED_VERSION'),
1805
+ );
1806
+ return;
1807
+ }
1808
+
1809
+ const handler = this.rpcHandlers.get(method);
1810
+
1811
+ if (!handler) {
1812
+ await this.engine.publishRpcResponse(
1813
+ callerIdentity,
1814
+ requestId,
1815
+ null,
1816
+ RpcError.builtIn('UNSUPPORTED_METHOD'),
1817
+ );
1818
+ return;
1819
+ }
1820
+
1821
+ let responseError: RpcError | null = null;
1822
+ let responsePayload: string | null = null;
1823
+
1824
+ try {
1825
+ const response = await handler({
1826
+ requestId,
1827
+ callerIdentity,
1828
+ payload,
1829
+ responseTimeout,
1830
+ });
1831
+ if (byteLength(response) > MAX_PAYLOAD_BYTES) {
1832
+ responseError = RpcError.builtIn('RESPONSE_PAYLOAD_TOO_LARGE');
1833
+ console.warn(`RPC Response payload too large for ${method}`);
1834
+ } else {
1835
+ responsePayload = response;
1836
+ }
1837
+ } catch (error) {
1838
+ if (error instanceof RpcError) {
1839
+ responseError = error;
1840
+ } else {
1841
+ console.warn(
1842
+ `Uncaught error returned by RPC handler for ${method}. Returning APPLICATION_ERROR instead.`,
1843
+ error,
1844
+ );
1845
+ responseError = RpcError.builtIn('APPLICATION_ERROR');
1846
+ }
1847
+ }
1848
+ await this.engine.publishRpcResponse(callerIdentity, requestId, responsePayload, responseError);
1849
+ }
1850
+
1851
+ bufferedSegments: Map<string, TranscriptionSegmentModel> = new Map();
1852
+
1966
1853
  private handleAudioPlaybackStarted = () => {
1967
1854
  if (this.canPlaybackAudio) {
1968
1855
  return;
@@ -0,0 +1,247 @@
1
+ import {
2
+ type DataPacket,
3
+ DataStream_Chunk,
4
+ DataStream_Header,
5
+ DataStream_Trailer,
6
+ } from '@livekit/protocol';
7
+ import log from '../../../logger';
8
+ import { DataStreamError, DataStreamErrorReason } from '../../errors';
9
+ import { type ByteStreamInfo, type StreamController, type TextStreamInfo } from '../../types';
10
+ import { Future, bigIntToNumber } from '../../utils';
11
+ import {
12
+ type ByteStreamHandler,
13
+ ByteStreamReader,
14
+ type TextStreamHandler,
15
+ TextStreamReader,
16
+ } from './StreamReader';
17
+
18
+ export default class IncomingDataStreamManager {
19
+ private log = log;
20
+
21
+ private byteStreamControllers = new Map<string, StreamController<DataStream_Chunk>>();
22
+
23
+ private textStreamControllers = new Map<string, StreamController<DataStream_Chunk>>();
24
+
25
+ private byteStreamHandlers = new Map<string, ByteStreamHandler>();
26
+
27
+ private textStreamHandlers = new Map<string, TextStreamHandler>();
28
+
29
+ registerTextStreamHandler(topic: string, callback: TextStreamHandler) {
30
+ if (this.textStreamHandlers.has(topic)) {
31
+ throw new DataStreamError(
32
+ `A text stream handler for topic "${topic}" has already been set.`,
33
+ DataStreamErrorReason.HandlerAlreadyRegistered,
34
+ );
35
+ }
36
+ this.textStreamHandlers.set(topic, callback);
37
+ }
38
+
39
+ unregisterTextStreamHandler(topic: string) {
40
+ this.textStreamHandlers.delete(topic);
41
+ }
42
+
43
+ registerByteStreamHandler(topic: string, callback: ByteStreamHandler) {
44
+ if (this.byteStreamHandlers.has(topic)) {
45
+ throw new DataStreamError(
46
+ `A byte stream handler for topic "${topic}" has already been set.`,
47
+ DataStreamErrorReason.HandlerAlreadyRegistered,
48
+ );
49
+ }
50
+ this.byteStreamHandlers.set(topic, callback);
51
+ }
52
+
53
+ unregisterByteStreamHandler(topic: string) {
54
+ this.byteStreamHandlers.delete(topic);
55
+ }
56
+
57
+ clearHandlersAndControllers() {
58
+ this.byteStreamControllers.clear();
59
+ this.textStreamControllers.clear();
60
+ this.byteStreamHandlers.clear();
61
+ this.textStreamHandlers.clear();
62
+ }
63
+
64
+ validateParticipantHasNoActiveDataStreams(participantIdentity: string) {
65
+ // Terminate any in flight data stream receives from the given participant
66
+ const textStreamsBeingSentByDisconnectingParticipant = Array.from(
67
+ this.textStreamControllers.entries(),
68
+ ).filter((entry) => entry[1].sendingParticipantIdentity === participantIdentity);
69
+ const byteStreamsBeingSentByDisconnectingParticipant = Array.from(
70
+ this.byteStreamControllers.entries(),
71
+ ).filter((entry) => entry[1].sendingParticipantIdentity === participantIdentity);
72
+
73
+ if (
74
+ textStreamsBeingSentByDisconnectingParticipant.length > 0 ||
75
+ byteStreamsBeingSentByDisconnectingParticipant.length > 0
76
+ ) {
77
+ const abnormalEndError = new DataStreamError(
78
+ `Participant ${participantIdentity} unexpectedly disconnected in the middle of sending data`,
79
+ DataStreamErrorReason.AbnormalEnd,
80
+ );
81
+ for (const [id, controller] of byteStreamsBeingSentByDisconnectingParticipant) {
82
+ controller.outOfBandFailureRejectingFuture.reject?.(abnormalEndError);
83
+ this.byteStreamControllers.delete(id);
84
+ }
85
+ for (const [id, controller] of textStreamsBeingSentByDisconnectingParticipant) {
86
+ controller.outOfBandFailureRejectingFuture.reject?.(abnormalEndError);
87
+ this.textStreamControllers.delete(id);
88
+ }
89
+ }
90
+ }
91
+
92
+ async handleDataStreamPacket(packet: DataPacket) {
93
+ switch (packet.value.case) {
94
+ case 'streamHeader':
95
+ return this.handleStreamHeader(packet.value.value, packet.participantIdentity);
96
+ case 'streamChunk':
97
+ return this.handleStreamChunk(packet.value.value);
98
+ case 'streamTrailer':
99
+ return this.handleStreamTrailer(packet.value.value);
100
+ default:
101
+ throw new Error(`DataPacket of value "${packet.value.case}" is not data stream related!`);
102
+ }
103
+ }
104
+
105
+ private async handleStreamHeader(streamHeader: DataStream_Header, participantIdentity: string) {
106
+ if (streamHeader.contentHeader.case === 'byteHeader') {
107
+ const streamHandlerCallback = this.byteStreamHandlers.get(streamHeader.topic);
108
+ if (!streamHandlerCallback) {
109
+ this.log.debug(
110
+ 'ignoring incoming byte stream due to no handler for topic',
111
+ streamHeader.topic,
112
+ );
113
+ return;
114
+ }
115
+
116
+ let streamController: ReadableStreamDefaultController<DataStream_Chunk>;
117
+ const outOfBandFailureRejectingFuture = new Future<never>();
118
+
119
+ const info: ByteStreamInfo = {
120
+ id: streamHeader.streamId,
121
+ name: streamHeader.contentHeader.value.name ?? 'unknown',
122
+ mimeType: streamHeader.mimeType,
123
+ size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
124
+ topic: streamHeader.topic,
125
+ timestamp: bigIntToNumber(streamHeader.timestamp),
126
+ attributes: streamHeader.attributes,
127
+ };
128
+ const stream = new ReadableStream({
129
+ start: (controller) => {
130
+ streamController = controller;
131
+
132
+ if (this.textStreamControllers.has(streamHeader.streamId)) {
133
+ throw new DataStreamError(
134
+ `A data stream read is already in progress for a stream with id ${streamHeader.streamId}.`,
135
+ DataStreamErrorReason.AlreadyOpened,
136
+ );
137
+ }
138
+
139
+ this.byteStreamControllers.set(streamHeader.streamId, {
140
+ info,
141
+ controller: streamController,
142
+ startTime: Date.now(),
143
+ sendingParticipantIdentity: participantIdentity,
144
+ outOfBandFailureRejectingFuture,
145
+ });
146
+ },
147
+ });
148
+ streamHandlerCallback(
149
+ new ByteStreamReader(
150
+ info,
151
+ stream,
152
+ bigIntToNumber(streamHeader.totalLength),
153
+ outOfBandFailureRejectingFuture,
154
+ ),
155
+ {
156
+ identity: participantIdentity,
157
+ },
158
+ );
159
+ } else if (streamHeader.contentHeader.case === 'textHeader') {
160
+ const streamHandlerCallback = this.textStreamHandlers.get(streamHeader.topic);
161
+ if (!streamHandlerCallback) {
162
+ this.log.debug(
163
+ 'ignoring incoming text stream due to no handler for topic',
164
+ streamHeader.topic,
165
+ );
166
+ return;
167
+ }
168
+
169
+ let streamController: ReadableStreamDefaultController<DataStream_Chunk>;
170
+ const outOfBandFailureRejectingFuture = new Future<never>();
171
+ const info: TextStreamInfo = {
172
+ id: streamHeader.streamId,
173
+ mimeType: streamHeader.mimeType,
174
+ size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
175
+ topic: streamHeader.topic,
176
+ timestamp: Number(streamHeader.timestamp),
177
+ attributes: streamHeader.attributes,
178
+ };
179
+
180
+ const stream = new ReadableStream<DataStream_Chunk>({
181
+ start: (controller) => {
182
+ streamController = controller;
183
+
184
+ if (this.textStreamControllers.has(streamHeader.streamId)) {
185
+ throw new DataStreamError(
186
+ `A data stream read is already in progress for a stream with id ${streamHeader.streamId}.`,
187
+ DataStreamErrorReason.AlreadyOpened,
188
+ );
189
+ }
190
+
191
+ this.textStreamControllers.set(streamHeader.streamId, {
192
+ info,
193
+ controller: streamController,
194
+ startTime: Date.now(),
195
+ sendingParticipantIdentity: participantIdentity,
196
+ outOfBandFailureRejectingFuture,
197
+ });
198
+ },
199
+ });
200
+ streamHandlerCallback(
201
+ new TextStreamReader(
202
+ info,
203
+ stream,
204
+ bigIntToNumber(streamHeader.totalLength),
205
+ outOfBandFailureRejectingFuture,
206
+ ),
207
+ { identity: participantIdentity },
208
+ );
209
+ }
210
+ }
211
+
212
+ private handleStreamChunk(chunk: DataStream_Chunk) {
213
+ const fileBuffer = this.byteStreamControllers.get(chunk.streamId);
214
+ if (fileBuffer) {
215
+ if (chunk.content.length > 0) {
216
+ fileBuffer.controller.enqueue(chunk);
217
+ }
218
+ }
219
+ const textBuffer = this.textStreamControllers.get(chunk.streamId);
220
+ if (textBuffer) {
221
+ if (chunk.content.length > 0) {
222
+ textBuffer.controller.enqueue(chunk);
223
+ }
224
+ }
225
+ }
226
+
227
+ private handleStreamTrailer(trailer: DataStream_Trailer) {
228
+ const textBuffer = this.textStreamControllers.get(trailer.streamId);
229
+ if (textBuffer) {
230
+ textBuffer.info.attributes = {
231
+ ...textBuffer.info.attributes,
232
+ ...trailer.attributes,
233
+ };
234
+ textBuffer.controller.close();
235
+ this.textStreamControllers.delete(trailer.streamId);
236
+ }
237
+
238
+ const fileBuffer = this.byteStreamControllers.get(trailer.streamId);
239
+ if (fileBuffer) {
240
+ {
241
+ fileBuffer.info.attributes = { ...fileBuffer.info.attributes, ...trailer.attributes };
242
+ fileBuffer.controller.close();
243
+ this.byteStreamControllers.delete(trailer.streamId);
244
+ }
245
+ }
246
+ }
247
+ }