livekit-client 2.17.3 → 2.18.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.
Files changed (125) hide show
  1. package/dist/livekit-client.esm.mjs +64 -46
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/index.d.ts +1 -1
  6. package/dist/src/index.d.ts.map +1 -1
  7. package/dist/src/room/RTCEngine.d.ts +3 -1
  8. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  9. package/dist/src/room/data-track/{track.d.ts → LocalDataTrack.d.ts} +9 -14
  10. package/dist/src/room/data-track/LocalDataTrack.d.ts.map +1 -0
  11. package/dist/src/room/data-track/RemoteDataTrack.d.ts +46 -0
  12. package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -0
  13. package/dist/src/room/data-track/depacketizer.d.ts +3 -3
  14. package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
  15. package/dist/src/room/data-track/e2ee.d.ts +1 -1
  16. package/dist/src/room/data-track/e2ee.d.ts.map +1 -1
  17. package/dist/src/room/data-track/handle.d.ts +2 -2
  18. package/dist/src/room/data-track/handle.d.ts.map +1 -1
  19. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +96 -0
  20. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -0
  21. package/dist/src/room/data-track/incoming/errors.d.ts +24 -0
  22. package/dist/src/room/data-track/incoming/errors.d.ts.map +1 -0
  23. package/dist/src/room/data-track/incoming/pipeline.d.ts +37 -0
  24. package/dist/src/room/data-track/incoming/pipeline.d.ts.map +1 -0
  25. package/dist/src/room/data-track/incoming/types.d.ts +20 -0
  26. package/dist/src/room/data-track/incoming/types.d.ts.map +1 -0
  27. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +9 -8
  28. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  29. package/dist/src/room/data-track/outgoing/errors.d.ts +4 -4
  30. package/dist/src/room/data-track/outgoing/errors.d.ts.map +1 -1
  31. package/dist/src/room/data-track/outgoing/pipeline.d.ts +3 -3
  32. package/dist/src/room/data-track/outgoing/pipeline.d.ts.map +1 -1
  33. package/dist/src/room/data-track/outgoing/types.d.ts +4 -5
  34. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  35. package/dist/src/room/data-track/packet/errors.d.ts +2 -4
  36. package/dist/src/room/data-track/packet/errors.d.ts.map +1 -1
  37. package/dist/src/room/data-track/packet/extensions.d.ts +4 -4
  38. package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
  39. package/dist/src/room/data-track/packet/index.d.ts +5 -5
  40. package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
  41. package/dist/src/room/data-track/packet/serializable.d.ts +4 -4
  42. package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
  43. package/dist/src/room/data-track/packetizer.d.ts +3 -3
  44. package/dist/src/room/data-track/packetizer.d.ts.map +1 -1
  45. package/dist/src/room/data-track/track-interfaces.d.ts +23 -0
  46. package/dist/src/room/data-track/track-interfaces.d.ts.map +1 -0
  47. package/dist/src/room/data-track/types.d.ts +10 -0
  48. package/dist/src/room/data-track/types.d.ts.map +1 -0
  49. package/dist/src/room/participant/RemoteParticipant.d.ts +1 -1
  50. package/dist/src/room/token-source/TokenSource.d.ts +1 -1
  51. package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
  52. package/dist/src/room/token-source/types.d.ts +1 -0
  53. package/dist/src/room/token-source/types.d.ts.map +1 -1
  54. package/dist/src/room/utils.d.ts +1 -1
  55. package/dist/src/room/utils.d.ts.map +1 -1
  56. package/dist/src/utils/abort-signal-polyfill.d.ts +13 -0
  57. package/dist/src/utils/abort-signal-polyfill.d.ts.map +1 -0
  58. package/dist/src/utils/subscribeToEvents.d.ts +3 -0
  59. package/dist/src/utils/subscribeToEvents.d.ts.map +1 -1
  60. package/dist/ts4.2/index.d.ts +1 -1
  61. package/dist/ts4.2/room/RTCEngine.d.ts +3 -1
  62. package/dist/ts4.2/room/data-track/{track.d.ts → LocalDataTrack.d.ts} +9 -14
  63. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +46 -0
  64. package/dist/ts4.2/room/data-track/depacketizer.d.ts +3 -3
  65. package/dist/ts4.2/room/data-track/e2ee.d.ts +1 -1
  66. package/dist/ts4.2/room/data-track/handle.d.ts +2 -2
  67. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +99 -0
  68. package/dist/ts4.2/room/data-track/incoming/errors.d.ts +24 -0
  69. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +37 -0
  70. package/dist/ts4.2/room/data-track/incoming/types.d.ts +20 -0
  71. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +9 -9
  72. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +4 -4
  73. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +3 -3
  74. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +4 -5
  75. package/dist/ts4.2/room/data-track/packet/errors.d.ts +2 -4
  76. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +4 -4
  77. package/dist/ts4.2/room/data-track/packet/index.d.ts +5 -6
  78. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +4 -4
  79. package/dist/ts4.2/room/data-track/packetizer.d.ts +3 -3
  80. package/dist/ts4.2/room/data-track/track-interfaces.d.ts +23 -0
  81. package/dist/ts4.2/room/data-track/types.d.ts +10 -0
  82. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +1 -1
  83. package/dist/ts4.2/room/token-source/TokenSource.d.ts +1 -1
  84. package/dist/ts4.2/room/token-source/types.d.ts +1 -0
  85. package/dist/ts4.2/room/utils.d.ts +1 -1
  86. package/dist/ts4.2/utils/abort-signal-polyfill.d.ts +13 -0
  87. package/dist/ts4.2/utils/subscribeToEvents.d.ts +3 -0
  88. package/package.json +4 -2
  89. package/src/index.ts +1 -1
  90. package/src/room/RTCEngine.ts +8 -1
  91. package/src/room/Room.ts +3 -3
  92. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +2 -2
  93. package/src/room/data-track/{track.ts → LocalDataTrack.ts} +13 -10
  94. package/src/room/data-track/RemoteDataTrack.ts +82 -0
  95. package/src/room/data-track/depacketizer.ts +4 -16
  96. package/src/room/data-track/e2ee.ts +2 -1
  97. package/src/room/data-track/handle.ts +2 -8
  98. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +570 -0
  99. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +537 -0
  100. package/src/room/data-track/incoming/errors.ts +57 -0
  101. package/src/room/data-track/incoming/pipeline.ts +116 -0
  102. package/src/room/data-track/incoming/types.ts +22 -0
  103. package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +20 -0
  104. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +18 -12
  105. package/src/room/data-track/outgoing/errors.ts +4 -4
  106. package/src/room/data-track/outgoing/pipeline.ts +3 -7
  107. package/src/room/data-track/outgoing/types.ts +4 -5
  108. package/src/room/data-track/packet/errors.ts +2 -14
  109. package/src/room/data-track/packet/extensions.ts +4 -4
  110. package/src/room/data-track/packet/index.ts +4 -6
  111. package/src/room/data-track/packet/serializable.ts +4 -4
  112. package/src/room/data-track/packetizer.ts +3 -6
  113. package/src/room/data-track/track-interfaces.ts +53 -0
  114. package/src/room/data-track/types.ts +11 -0
  115. package/src/room/participant/RemoteParticipant.ts +1 -1
  116. package/src/room/token-source/TokenSource.ts +8 -2
  117. package/src/room/token-source/types.ts +4 -0
  118. package/src/room/utils.ts +1 -1
  119. package/src/utils/abort-signal-polyfill.ts +63 -0
  120. package/src/utils/subscribeToEvents.ts +18 -1
  121. package/dist/src/room/data-track/track.d.ts.map +0 -1
  122. package/dist/src/utils/throws.d.ts +0 -36
  123. package/dist/src/utils/throws.d.ts.map +0 -1
  124. package/dist/ts4.2/utils/throws.d.ts +0 -39
  125. package/src/utils/throws.ts +0 -42
@@ -0,0 +1,570 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { subscribeToEvents } from '../../../utils/subscribeToEvents';
4
+ import { DecryptionProvider, EncryptedPayload } from '../e2ee';
5
+ import { DataTrackFrame } from '../frame';
6
+ import { DataTrackHandle, DataTrackHandleAllocator } from '../handle';
7
+ import { DataTrackPacket, DataTrackPacketHeader, FrameMarker } from '../packet';
8
+ import { DataTrackE2eeExtension, DataTrackExtensions } from '../packet/extensions';
9
+ import { DataTrackTimestamp, WrapAroundUnsignedInt } from '../utils';
10
+ import IncomingDataTrackManager, {
11
+ DataTrackIncomingManagerCallbacks,
12
+ } from './IncomingDataTrackManager';
13
+ import { DataTrackSubscribeError } from './errors';
14
+
15
+ /** A fake "decryption" provider used for test purposes. Assumes the payload is prefixed with
16
+ * 0xdeafbeef, which is stripped off. */
17
+ const PrefixingDecryptionProvider: DecryptionProvider = {
18
+ decrypt(p: EncryptedPayload, _senderIdentity: string) {
19
+ if (
20
+ p.payload[0] !== 0xde ||
21
+ p.payload[1] !== 0xad ||
22
+ p.payload[2] !== 0xbe ||
23
+ p.payload[3] !== 0xef
24
+ ) {
25
+ throw new Error(
26
+ `PrefixingDecryptionProvider: first four bytes of payload were not 0xdeadbeef, found ${p.payload.slice(0, 4)}`,
27
+ );
28
+ }
29
+ return p.payload.slice(4);
30
+ },
31
+ };
32
+
33
+ describe('DataTrackIncomingManager', () => {
34
+ describe('Track publication', () => {
35
+ it('should test track publication additions / removals', async () => {
36
+ const manager = new IncomingDataTrackManager();
37
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
38
+ 'sfuUpdateSubscription',
39
+ 'trackAvailable',
40
+ 'trackUnavailable',
41
+ ]);
42
+
43
+ // 1. Add a track, make sure the track available event was sent
44
+ await manager.receiveSfuPublicationUpdates(
45
+ new Map([
46
+ [
47
+ 'identity1',
48
+ [
49
+ {
50
+ sid: 'sid1',
51
+ pubHandle: DataTrackHandle.fromNumber(5),
52
+ name: 'test',
53
+ usesE2ee: false,
54
+ },
55
+ ],
56
+ ],
57
+ ]),
58
+ );
59
+
60
+ const trackAvailableEvent = await managerEvents.waitFor('trackAvailable');
61
+ expect(trackAvailableEvent.track.info.sid).toStrictEqual('sid1');
62
+ expect(trackAvailableEvent.track.info.pubHandle).toStrictEqual(DataTrackHandle.fromNumber(5));
63
+ expect(trackAvailableEvent.track.info.name).toStrictEqual('test');
64
+ expect(trackAvailableEvent.track.info.usesE2ee).toStrictEqual(false);
65
+
66
+ // 2. Check to make sure the publication has been noted in internal state
67
+ expect((await manager.queryPublications()).map((p) => p.pubHandle)).to.deep.equal([
68
+ DataTrackHandle.fromNumber(5),
69
+ ]);
70
+
71
+ // 3. Remove all tracks, and make sure the internal state is cleared
72
+ await manager.receiveSfuPublicationUpdates(new Map([['identity1', []]]));
73
+ expect(await manager.queryPublications()).to.deep.equal([]);
74
+
75
+ const trackUnavailableEvent = await managerEvents.waitFor('trackUnavailable');
76
+ expect(trackUnavailableEvent.sid).toStrictEqual('sid1');
77
+ expect(trackUnavailableEvent.publisherIdentity).toStrictEqual('identity1');
78
+ });
79
+
80
+ it('should process sfu publication updates idempotently', async () => {
81
+ const manager = new IncomingDataTrackManager();
82
+
83
+ // 1. Simulate three identical track publications being received
84
+ for (let i = 0; i < 3; i += 1) {
85
+ await manager.receiveSfuPublicationUpdates(
86
+ new Map([
87
+ [
88
+ 'identity1',
89
+ [
90
+ {
91
+ sid: 'sid1',
92
+ pubHandle: DataTrackHandle.fromNumber(5),
93
+ name: 'test',
94
+ usesE2ee: false,
95
+ },
96
+ ],
97
+ ],
98
+ ]),
99
+ );
100
+ }
101
+
102
+ // 2. Check to make sure the publication has been noted in internal state only once
103
+ expect((await manager.queryPublications()).map((p) => p.pubHandle)).to.deep.equal([
104
+ DataTrackHandle.fromNumber(5),
105
+ ]);
106
+ });
107
+ });
108
+
109
+ describe('Track subscription', () => {
110
+ it('should test data track subscribing (ok case)', async () => {
111
+ const manager = new IncomingDataTrackManager();
112
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
113
+ 'sfuUpdateSubscription',
114
+ 'trackAvailable',
115
+ ]);
116
+
117
+ const senderIdentity = 'identity';
118
+ const sid = 'data track sid';
119
+ const handle = DataTrackHandle.fromNumber(5);
120
+
121
+ // 1. Make sure the data track publication is registered
122
+ await manager.receiveSfuPublicationUpdates(
123
+ new Map([[senderIdentity, [{ sid, pubHandle: handle, name: 'test', usesE2ee: false }]]]),
124
+ );
125
+ await managerEvents.waitFor('trackAvailable');
126
+
127
+ // 2. Subscribe to a data track
128
+ const subscribeRequestPromise = manager.subscribeRequest(sid);
129
+
130
+ // 3. This subscribe request should be sent along to the SFU
131
+ const sfuUpdateSubscriptionEvent = await managerEvents.waitFor('sfuUpdateSubscription');
132
+ expect(sfuUpdateSubscriptionEvent.sid).toStrictEqual(sid);
133
+ expect(sfuUpdateSubscriptionEvent.subscribe).toStrictEqual(true);
134
+
135
+ // 4. Once the SFU has acknowledged the subscription, a handle is sent back representing
136
+ // the subscription
137
+ manager.receivedSfuSubscriberHandles(new Map([[handle, sid]]));
138
+
139
+ // 5. Make sure that the subscription promise resolves.
140
+ const readableStream = await subscribeRequestPromise;
141
+ const reader = readableStream.getReader();
142
+
143
+ // 6. Simulate receiving a packet
144
+ manager.packetReceived(
145
+ new DataTrackPacket(
146
+ new DataTrackPacketHeader({
147
+ extensions: new DataTrackExtensions(),
148
+ frameNumber: WrapAroundUnsignedInt.u16(0),
149
+ marker: FrameMarker.Single,
150
+ sequence: WrapAroundUnsignedInt.u16(0),
151
+ timestamp: DataTrackTimestamp.fromRtpTicks(0),
152
+ trackHandle: DataTrackHandle.fromNumber(5),
153
+ }),
154
+ new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]),
155
+ ).toBinary(),
156
+ );
157
+
158
+ // 7. Make sure that packet comes out of the ReadableStream
159
+ const { value, done } = await reader.read();
160
+ expect(done).toStrictEqual(false);
161
+ expect(value?.payload).toStrictEqual(new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]));
162
+ });
163
+
164
+ it('should test data track subscribing with end to end encryption (ok case)', async () => {
165
+ const manager = new IncomingDataTrackManager({
166
+ decryptionProvider: PrefixingDecryptionProvider,
167
+ });
168
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
169
+ 'sfuUpdateSubscription',
170
+ 'trackAvailable',
171
+ ]);
172
+
173
+ const senderIdentity = 'identity';
174
+ const sid = 'data track sid';
175
+ const handle = DataTrackHandle.fromNumber(5);
176
+
177
+ // 1. Make sure the data track publication is registered
178
+ await manager.receiveSfuPublicationUpdates(
179
+ new Map([[senderIdentity, [{ sid, pubHandle: handle, name: 'test', usesE2ee: true }]]]),
180
+ );
181
+ await managerEvents.waitFor('trackAvailable');
182
+
183
+ // 2. Subscribe to a data track
184
+ const subscribeRequestPromise = manager.subscribeRequest(sid);
185
+
186
+ // 3. This subscribe request should be sent along to the SFU
187
+ const sfuUpdateSubscriptionEvent = await managerEvents.waitFor('sfuUpdateSubscription');
188
+ expect(sfuUpdateSubscriptionEvent.sid).toStrictEqual(sid);
189
+ expect(sfuUpdateSubscriptionEvent.subscribe).toStrictEqual(true);
190
+
191
+ // 4. Once the SFU has acknowledged the subscription, a handle is sent back representing
192
+ // the subscription
193
+ manager.receivedSfuSubscriberHandles(new Map([[handle, sid]]));
194
+
195
+ // 5. Make sure that the subscription promise resolves.
196
+ const readableStream = await subscribeRequestPromise;
197
+ const reader = readableStream.getReader();
198
+
199
+ // 6. Simulate receiving a (fake) encrypted packet
200
+ manager.packetReceived(
201
+ new DataTrackPacket(
202
+ new DataTrackPacketHeader({
203
+ extensions: new DataTrackExtensions({
204
+ e2ee: new DataTrackE2eeExtension(0, new Uint8Array(12)),
205
+ }),
206
+ frameNumber: WrapAroundUnsignedInt.u16(0),
207
+ marker: FrameMarker.Single,
208
+ sequence: WrapAroundUnsignedInt.u16(0),
209
+ timestamp: DataTrackTimestamp.fromRtpTicks(0),
210
+ trackHandle: DataTrackHandle.fromNumber(5),
211
+ }),
212
+ new Uint8Array([
213
+ // Fake encryption bytes prefix
214
+ 0xde, 0xad, 0xbe, 0xef,
215
+ // Actual payload
216
+ 0x01, 0x02, 0x03, 0x04, 0x05,
217
+ ]),
218
+ ).toBinary(),
219
+ );
220
+
221
+ // 7. Make sure that packet comes out of the ReadableStream
222
+ const { value, done } = await reader.read();
223
+ expect(done).toStrictEqual(false);
224
+ expect(value?.payload).toStrictEqual(new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]));
225
+ });
226
+
227
+ it('should fan out received events across multiple subscriptions', async () => {
228
+ const manager = new IncomingDataTrackManager();
229
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
230
+ 'sfuUpdateSubscription',
231
+ 'trackAvailable',
232
+ ]);
233
+
234
+ const senderIdentity = 'identity';
235
+ const sid = 'data track sid';
236
+
237
+ const handleAllocator = new DataTrackHandleAllocator();
238
+
239
+ // 1. Make sure the data track publication is registered
240
+ const pubHandle = handleAllocator.get()!;
241
+ await manager.receiveSfuPublicationUpdates(
242
+ new Map([[senderIdentity, [{ sid, pubHandle, name: 'test', usesE2ee: false }]]]),
243
+ );
244
+ await managerEvents.waitFor('trackAvailable');
245
+
246
+ // 2. Set up lots of subscribers
247
+ const readers: Array<ReadableStreamDefaultReader<DataTrackFrame>> = [];
248
+ for (let index = 0; index < 8; index += 1) {
249
+ // Subscribe to a data track
250
+ const subscribeRequestPromise = manager.subscribeRequest(sid);
251
+
252
+ // Make sure that the sfu interactions ONLY happen for the first subscription opened.
253
+ if (index === 0) {
254
+ // This subscribe request should be sent along to the SFU
255
+ const sfuUpdateSubscriptionEvent = await managerEvents.waitFor('sfuUpdateSubscription');
256
+ expect(sfuUpdateSubscriptionEvent.sid).toStrictEqual(sid);
257
+ expect(sfuUpdateSubscriptionEvent.subscribe).toStrictEqual(true);
258
+
259
+ // Simulate the subscribe being acknowledged by the SFU
260
+ manager.receivedSfuSubscriberHandles(
261
+ new Map([[DataTrackHandle.fromNumber(1 /* publish handle */ + index), sid]]),
262
+ );
263
+ }
264
+
265
+ // 5. Make sure that the subscription promise resolves.
266
+ const readableStream = await subscribeRequestPromise;
267
+ const reader = readableStream.getReader();
268
+ readers.push(reader);
269
+ }
270
+
271
+ // 6. Simulate receiving a packet
272
+ manager.packetReceived(
273
+ new DataTrackPacket(
274
+ new DataTrackPacketHeader({
275
+ extensions: new DataTrackExtensions(),
276
+ frameNumber: WrapAroundUnsignedInt.u16(0),
277
+ marker: FrameMarker.Single,
278
+ sequence: WrapAroundUnsignedInt.u16(0),
279
+ timestamp: DataTrackTimestamp.fromRtpTicks(0),
280
+ trackHandle: pubHandle,
281
+ }),
282
+ new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]),
283
+ ).toBinary(),
284
+ );
285
+
286
+ // 7. Make sure that packet comes out of all of the `ReadableStream`s
287
+ const results = await Promise.all(readers.map((reader) => reader.read()));
288
+ for (const { value, done } of results) {
289
+ expect(done).toStrictEqual(false);
290
+ expect(value?.payload).toStrictEqual(new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]));
291
+ }
292
+ });
293
+
294
+ it('should be unable to subscribe to a non existing data track', async () => {
295
+ const manager = new IncomingDataTrackManager();
296
+ await expect(manager.subscribeRequest('does not exist')).rejects.toThrowError(
297
+ 'Cannot subscribe to unknown track',
298
+ );
299
+ });
300
+
301
+ it('should terminate the sfu subscription if the abortsignal is triggered on the only subscription', async () => {
302
+ const manager = new IncomingDataTrackManager();
303
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
304
+ 'sfuUpdateSubscription',
305
+ 'trackAvailable',
306
+ ]);
307
+
308
+ const sid = 'data track sid';
309
+
310
+ // 1. Make sure the data track publication is registered
311
+ await manager.receiveSfuPublicationUpdates(
312
+ new Map([
313
+ [
314
+ 'identity',
315
+ [{ sid, pubHandle: DataTrackHandle.fromNumber(5), name: 'test', usesE2ee: false }],
316
+ ],
317
+ ]),
318
+ );
319
+ await managerEvents.waitFor('trackAvailable');
320
+
321
+ // 2. Subscribe to a data track
322
+ const controller = new AbortController();
323
+ const subscribeRequestPromise = manager.subscribeRequest(sid, controller.signal);
324
+ await managerEvents.waitFor('sfuUpdateSubscription');
325
+
326
+ // 3. Cancel the subscription
327
+ controller.abort();
328
+ await expect(subscribeRequestPromise).rejects.toThrowError(
329
+ 'Subscription to data track cancelled by caller',
330
+ );
331
+
332
+ // 4. Make sure the underlying sfu subscription is also terminated, since nothing needs it
333
+ // anymore.
334
+ const sfuUpdateSubscriptionEvent = await managerEvents.waitFor('sfuUpdateSubscription');
335
+ expect(sfuUpdateSubscriptionEvent.sid).toStrictEqual(sid);
336
+ expect(sfuUpdateSubscriptionEvent.subscribe).toStrictEqual(false);
337
+ });
338
+
339
+ it('should NOT terminate the sfu subscription if the abortsignal is triggered on one of two active subscriptions', async () => {
340
+ const manager = new IncomingDataTrackManager();
341
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
342
+ 'sfuUpdateSubscription',
343
+ 'trackAvailable',
344
+ ]);
345
+
346
+ const sid = 'data track sid';
347
+
348
+ // 1. Make sure the data track publication is registered
349
+ await manager.receiveSfuPublicationUpdates(
350
+ new Map([
351
+ [
352
+ 'identity',
353
+ [{ sid, pubHandle: DataTrackHandle.fromNumber(5), name: 'test', usesE2ee: false }],
354
+ ],
355
+ ]),
356
+ );
357
+ await managerEvents.waitFor('trackAvailable');
358
+
359
+ // 2. Subscribe to a data track twice
360
+ const controllerOne = new AbortController();
361
+ const subscribeRequestOnePromise = manager.subscribeRequest(sid, controllerOne.signal);
362
+ await managerEvents.waitFor('sfuUpdateSubscription'); // Subscription started
363
+
364
+ const controllerTwo = new AbortController();
365
+ manager.subscribeRequest(sid, controllerTwo.signal);
366
+
367
+ // 3. Cancel the first subscription
368
+ controllerOne.abort();
369
+ await expect(subscribeRequestOnePromise).rejects.toThrowError(
370
+ 'Subscription to data track cancelled by caller',
371
+ );
372
+
373
+ // 4. Make sure the underlying sfu subscription has not been also cancelled, there still is
374
+ // one data track subscription active
375
+ expect(managerEvents.areThereBufferedEvents('sfuUpdateSubscription')).toBe(false);
376
+ });
377
+
378
+ it('should terminate the sfu subscription if the abortsignal is already aborted', async () => {
379
+ const manager = new IncomingDataTrackManager();
380
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
381
+ 'sfuUpdateSubscription',
382
+ ]);
383
+
384
+ const sid = 'data track sid';
385
+
386
+ // Make sure the data track publication is registered
387
+ await manager.receiveSfuPublicationUpdates(
388
+ new Map([
389
+ [
390
+ 'identity',
391
+ [{ sid, pubHandle: DataTrackHandle.fromNumber(5), name: 'test', usesE2ee: false }],
392
+ ],
393
+ ]),
394
+ );
395
+
396
+ // Subscribe to a data track
397
+ const subscribeRequestPromise = manager.subscribeRequest(
398
+ sid,
399
+ AbortSignal.abort(/* already aborted */),
400
+ );
401
+ const start = await managerEvents.waitFor('sfuUpdateSubscription');
402
+ expect(start.subscribe).toBe(true);
403
+
404
+ // Make sure cancellation is immediately bubbled up
405
+ await expect(subscribeRequestPromise).rejects.toStrictEqual(
406
+ DataTrackSubscribeError.cancelled(),
407
+ );
408
+
409
+ // Make sure that there is immediately another "unsubscribe" sent
410
+ const end = await managerEvents.waitFor('sfuUpdateSubscription');
411
+ expect(end.subscribe).toBe(false);
412
+ });
413
+
414
+ it('should terminate the sfu subscription once all listeners have unsubscribed', async () => {
415
+ const manager = new IncomingDataTrackManager();
416
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
417
+ 'sfuUpdateSubscription',
418
+ 'trackAvailable',
419
+ ]);
420
+
421
+ const sid = 'data track sid';
422
+
423
+ // 1. Make sure the data track publication is registered
424
+ await manager.receiveSfuPublicationUpdates(
425
+ new Map([
426
+ [
427
+ 'identity',
428
+ [{ sid, pubHandle: DataTrackHandle.fromNumber(5), name: 'test', usesE2ee: false }],
429
+ ],
430
+ ]),
431
+ );
432
+ await managerEvents.waitFor('trackAvailable');
433
+
434
+ // 2. Create subscription A
435
+ const controllerA = new AbortController();
436
+ const subscribeAPromise = manager.subscribeRequest(sid, controllerA.signal);
437
+ const startEvent = await managerEvents.waitFor('sfuUpdateSubscription');
438
+ expect(startEvent.sid).toStrictEqual(sid);
439
+ expect(startEvent.subscribe).toStrictEqual(true);
440
+
441
+ // 2. Create subscription B
442
+ const controllerB = new AbortController();
443
+ const subscribeBPromise = manager.subscribeRequest(sid, controllerB.signal);
444
+ expect(managerEvents.areThereBufferedEvents('sfuUpdateSubscription')).toStrictEqual(false);
445
+
446
+ // 3. Cancel the subscription A
447
+ controllerA.abort();
448
+ expect(managerEvents.areThereBufferedEvents('sfuUpdateSubscription')).toStrictEqual(false);
449
+
450
+ // 4. Cancel the subscription B, make sure the underlying sfu subscription is disposed
451
+ controllerB.abort();
452
+ const endEvent = await managerEvents.waitFor('sfuUpdateSubscription');
453
+ expect(endEvent.sid).toStrictEqual(sid);
454
+ expect(endEvent.subscribe).toStrictEqual(false);
455
+
456
+ await expect(subscribeAPromise).rejects.toThrow();
457
+ await expect(subscribeBPromise).rejects.toThrow();
458
+ });
459
+
460
+ it('should terminate PENDING sfu subscriptions if the participant disconnects', async () => {
461
+ const manager = new IncomingDataTrackManager();
462
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
463
+ 'sfuUpdateSubscription',
464
+ 'trackAvailable',
465
+ ]);
466
+
467
+ const senderIdentity = 'identity';
468
+ const sid = 'data track sid';
469
+ const handle = DataTrackHandle.fromNumber(5);
470
+
471
+ // 1. Make sure the data track publication is registered
472
+ await manager.receiveSfuPublicationUpdates(
473
+ new Map([[senderIdentity, [{ sid, pubHandle: handle, name: 'test', usesE2ee: false }]]]),
474
+ );
475
+ await managerEvents.waitFor('trackAvailable');
476
+
477
+ // 2. Begin subscribing to a data track
478
+ const promise = manager.subscribeRequest(sid);
479
+
480
+ // 3. Simulate the remote participant disconnecting
481
+ manager.handleRemoteParticipantDisconnected(senderIdentity);
482
+
483
+ // 4. Make sure the pending subscribe was terminated
484
+ await expect(promise).rejects.toThrowError(
485
+ 'Cannot subscribe to data track when disconnected',
486
+ );
487
+ });
488
+
489
+ it('should terminate ACTIVE sfu subscriptions if the participant disconnects', async () => {
490
+ const manager = new IncomingDataTrackManager();
491
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
492
+ 'sfuUpdateSubscription',
493
+ 'trackAvailable',
494
+ ]);
495
+
496
+ const senderIdentity = 'identity';
497
+ const sid = 'data track sid';
498
+ const handle = DataTrackHandle.fromNumber(5);
499
+
500
+ // 1. Make sure the data track publication is registered
501
+ await manager.receiveSfuPublicationUpdates(
502
+ new Map([[senderIdentity, [{ sid, pubHandle: handle, name: 'test', usesE2ee: false }]]]),
503
+ );
504
+ await managerEvents.waitFor('trackAvailable');
505
+
506
+ // 2. Subscribe to a data track, and send the handle back as if the SFU acknowledged it
507
+ const subscribeRequestPromise = manager.subscribeRequest(sid);
508
+ const sfuUpdateSubscriptionEvent = await managerEvents.waitFor('sfuUpdateSubscription');
509
+ expect(sfuUpdateSubscriptionEvent.sid).toStrictEqual(sid);
510
+ expect(sfuUpdateSubscriptionEvent.subscribe).toStrictEqual(true);
511
+ manager.receivedSfuSubscriberHandles(new Map([[handle, sid]]));
512
+
513
+ // 3. Start an active stream read for later
514
+ const reader = (await subscribeRequestPromise).getReader();
515
+
516
+ // 4. Simulate the remote participant disconnecting
517
+ manager.handleRemoteParticipantDisconnected(senderIdentity);
518
+
519
+ // 5. Make sure the sfu unsubscribes
520
+ const endEvent = await managerEvents.waitFor('sfuUpdateSubscription');
521
+ expect(endEvent.sid).toStrictEqual(sid);
522
+ expect(endEvent.subscribe).toStrictEqual(false);
523
+
524
+ // 6. Make sure the in flight stream read was closed
525
+ await reader.closed;
526
+ });
527
+
528
+ it('should terminate the sfu subscription once all downstream ReadableStreams are cancelled', async () => {
529
+ const manager = new IncomingDataTrackManager();
530
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
531
+ 'sfuUpdateSubscription',
532
+ 'trackAvailable',
533
+ ]);
534
+
535
+ const senderIdentity = 'identity';
536
+ const sid = 'data track sid';
537
+ const handle = DataTrackHandle.fromNumber(5);
538
+
539
+ // 1. Make sure the data track publication is registered
540
+ await manager.receiveSfuPublicationUpdates(
541
+ new Map([[senderIdentity, [{ sid, pubHandle: handle, name: 'test', usesE2ee: false }]]]),
542
+ );
543
+ await managerEvents.waitFor('trackAvailable');
544
+
545
+ // 2. Subscribe to a data track
546
+ const subscribeRequestPromise = manager.subscribeRequest(sid);
547
+
548
+ // 3. This subscribe request should be sent along to the SFU
549
+ const sfuUpdateSubscriptionInitEvent = await managerEvents.waitFor('sfuUpdateSubscription');
550
+ expect(sfuUpdateSubscriptionInitEvent.sid).toStrictEqual(sid);
551
+ expect(sfuUpdateSubscriptionInitEvent.subscribe).toStrictEqual(true);
552
+
553
+ // 4. Once the SFU has acknowledged the subscription, a handle is sent back representing
554
+ // the subscription
555
+ manager.receivedSfuSubscriberHandles(new Map([[handle, sid]]));
556
+
557
+ // 5. Make sure that the subscription promise resolves.
558
+ const readableStream = await subscribeRequestPromise;
559
+ const reader = readableStream.getReader();
560
+
561
+ // 6. Manually cancel the readable stream
562
+ await reader.cancel();
563
+
564
+ // 7. Make sure the underlying SFU subscription is terminated
565
+ const sfuUpdateSubscriptionCancelEvent = await managerEvents.waitFor('sfuUpdateSubscription');
566
+ expect(sfuUpdateSubscriptionCancelEvent.sid).toStrictEqual(sid);
567
+ expect(sfuUpdateSubscriptionCancelEvent.subscribe).toStrictEqual(false);
568
+ });
569
+ });
570
+ });