livekit-client 2.20.2 → 2.21.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 (105) 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 +4 -0
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +1993 -791
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.fm.worker.js +1 -1
  8. package/dist/livekit-client.fm.worker.js.map +1 -1
  9. package/dist/livekit-client.fm.worker.mjs +4 -0
  10. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  11. package/dist/livekit-client.umd.js +1 -1
  12. package/dist/livekit-client.umd.js.map +1 -1
  13. package/dist/src/index.d.ts +1 -1
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/room/RTCEngine.d.ts +28 -64
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/Room.d.ts +4 -0
  18. package/dist/src/room/Room.d.ts.map +1 -1
  19. package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
  20. package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
  21. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  22. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
  23. package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
  24. package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
  25. package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
  26. package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
  27. package/dist/src/room/data-channel/types.d.ts +12 -0
  28. package/dist/src/room/data-channel/types.d.ts.map +1 -0
  29. package/dist/src/room/data-stream/compression.d.ts +49 -0
  30. package/dist/src/room/data-stream/compression.d.ts.map +1 -0
  31. package/dist/src/room/data-stream/constants.d.ts +18 -0
  32. package/dist/src/room/data-stream/constants.d.ts.map +1 -0
  33. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  34. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  35. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  36. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  37. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  38. package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
  39. package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
  40. package/dist/src/room/errors.d.ts +3 -1
  41. package/dist/src/room/errors.d.ts.map +1 -1
  42. package/dist/src/room/participant/LocalParticipant.d.ts +9 -1
  43. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  44. package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
  45. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  46. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  47. package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
  48. package/dist/src/room/types.d.ts +17 -0
  49. package/dist/src/room/types.d.ts.map +1 -1
  50. package/dist/src/room/utils.d.ts +13 -0
  51. package/dist/src/room/utils.d.ts.map +1 -1
  52. package/dist/src/version.d.ts +5 -1
  53. package/dist/src/version.d.ts.map +1 -1
  54. package/dist/ts4.2/index.d.ts +1 -1
  55. package/dist/ts4.2/room/RTCEngine.d.ts +28 -64
  56. package/dist/ts4.2/room/Room.d.ts +4 -0
  57. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
  58. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  59. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
  60. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
  61. package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
  62. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
  63. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
  64. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  65. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  66. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
  67. package/dist/ts4.2/room/errors.d.ts +3 -1
  68. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +9 -1
  69. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
  70. package/dist/ts4.2/room/types.d.ts +17 -0
  71. package/dist/ts4.2/room/utils.d.ts +13 -0
  72. package/dist/ts4.2/version.d.ts +5 -1
  73. package/package.json +2 -2
  74. package/src/index.ts +2 -0
  75. package/src/room/RTCEngine.test.ts +58 -99
  76. package/src/room/RTCEngine.ts +92 -534
  77. package/src/room/Room.test.ts +4 -1
  78. package/src/room/Room.ts +40 -8
  79. package/src/room/data-channel/DataChannelManager.test.ts +152 -0
  80. package/src/room/data-channel/DataChannelManager.ts +237 -0
  81. package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
  82. package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
  83. package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
  84. package/src/room/data-channel/LossyDataChannel.ts +125 -0
  85. package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
  86. package/src/room/data-channel/ReliableDataChannel.ts +153 -0
  87. package/src/room/data-channel/types.ts +30 -0
  88. package/src/room/data-stream/compression.ts +117 -0
  89. package/src/room/data-stream/constants.ts +18 -0
  90. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
  91. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
  92. package/src/room/data-stream/incoming/StreamReader.ts +8 -2
  93. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
  94. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
  95. package/src/room/data-stream/outgoing/header-utils.ts +87 -0
  96. package/src/room/errors.ts +6 -0
  97. package/src/room/participant/LocalParticipant.ts +13 -0
  98. package/src/room/participant/RemoteParticipant.ts +19 -5
  99. package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
  100. package/src/room/rpc/client/RpcClientManager.ts +1 -4
  101. package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
  102. package/src/room/rpc/server/RpcServerManager.ts +1 -3
  103. package/src/room/types.ts +18 -0
  104. package/src/room/utils.ts +67 -0
  105. package/src/version.ts +5 -1
@@ -0,0 +1,1292 @@
1
+ import {
2
+ ClientInfo_Capability,
3
+ type DataPacket,
4
+ type DataStream_ByteHeader,
5
+ DataStream_CompressionType,
6
+ type DataStream_TextHeader,
7
+ } from '@livekit/protocol';
8
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
9
+ import log from '../../../logger';
10
+ import {
11
+ CLIENT_PROTOCOL_DATA_STREAM_RPC,
12
+ CLIENT_PROTOCOL_DATA_STREAM_V2,
13
+ CLIENT_PROTOCOL_DEFAULT,
14
+ } from '../../../version';
15
+ import type RTCEngine from '../../RTCEngine';
16
+ import OutgoingDataStreamManager from './OutgoingDataStreamManager';
17
+
18
+ /** Builds a low quality random string of the given length. */
19
+ function randomText(length: number): string {
20
+ let s = '';
21
+ while (s.length < length) {
22
+ s += Math.random().toString(36).slice(2);
23
+ }
24
+ return s.slice(0, length);
25
+ }
26
+
27
+ /** Fills a buffer with uniform random bytes — genuinely incompressible. */
28
+ function randomBytes(length: number): Uint8Array {
29
+ const out = new Uint8Array(length);
30
+ // crypto.getRandomValues rejects requests over 65536 bytes, so chunk it.
31
+ for (let offset = 0; offset < length; offset += 65536) {
32
+ crypto.getRandomValues(out.subarray(offset, offset + 65536));
33
+ }
34
+ return out;
35
+ }
36
+
37
+ /**
38
+ * @param participants the remote participants in the room, mapped from identity to the client
39
+ * protocol each advertises. Defaults to a single v2 participant named "bob".
40
+ */
41
+ function createManager(
42
+ participants: Record<string, number | [number, Array<number>]> = {
43
+ bob: CLIENT_PROTOCOL_DATA_STREAM_V2,
44
+ },
45
+ ) {
46
+ const sentPackets: DataPacket[] = [];
47
+ const engine = {
48
+ sendDataPacket: vi.fn(async (packet: DataPacket) => {
49
+ sentPackets.push(packet);
50
+ }),
51
+ e2eeManager: undefined,
52
+ once: vi.fn(),
53
+ off: vi.fn(),
54
+ } as unknown as RTCEngine;
55
+ const manager = new OutgoingDataStreamManager(
56
+ engine,
57
+ log,
58
+ (identity) =>
59
+ (Array.isArray(participants[identity])
60
+ ? participants[identity][0]
61
+ : participants[identity]) ?? CLIENT_PROTOCOL_DEFAULT,
62
+ (identity) =>
63
+ Array.isArray(participants[identity])
64
+ ? participants[identity][1]
65
+ : [ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW],
66
+ () => Object.keys(participants),
67
+ );
68
+ return { manager, sentPackets };
69
+ }
70
+
71
+ function headerOf(packet: DataPacket) {
72
+ return packet.value.value as Extract<DataPacket['value'], { case: 'streamHeader' }>['value'];
73
+ }
74
+
75
+ function chunkOf(packet: DataPacket) {
76
+ return packet.value.value as Extract<DataPacket['value'], { case: 'streamChunk' }>['value'];
77
+ }
78
+
79
+ function trailerOf(packet: DataPacket) {
80
+ return packet.value.value as Extract<DataPacket['value'], { case: 'streamTrailer' }>['value'];
81
+ }
82
+
83
+ describe('OutgoingDataStreamManager', () => {
84
+ describe('v2 -> room of all v1', () => {
85
+ let manager: OutgoingDataStreamManager, sentPackets: Array<DataPacket>;
86
+ beforeEach(() => {
87
+ const result = createManager({
88
+ alice: CLIENT_PROTOCOL_DEFAULT,
89
+ bob: CLIENT_PROTOCOL_DEFAULT,
90
+ jim: CLIENT_PROTOCOL_DATA_STREAM_RPC,
91
+ });
92
+ manager = result.manager;
93
+ sentPackets = result.sentPackets;
94
+ });
95
+
96
+ it('should send short TEXT data stream using non single packet "legacy" format and NO compression (happy path)', async () => {
97
+ const info = await manager.sendText('hello world', {
98
+ topic: 'my-topic',
99
+ });
100
+
101
+ // Make sure three packets were received, matching the legacy v1 data stream format
102
+ expect(sentPackets).toHaveLength(3);
103
+
104
+ expect(sentPackets[0].value.case).toBe('streamHeader');
105
+ const header = headerOf(sentPackets[0]);
106
+ expect(header.streamId).toStrictEqual(info.id);
107
+ expect(header.topic).toStrictEqual('my-topic');
108
+ expect(header.contentHeader.case).toBe('textHeader');
109
+
110
+ expect(sentPackets[1].value.case).toStrictEqual('streamChunk');
111
+ const chunk = chunkOf(sentPackets[1]);
112
+ expect(chunk.streamId).toStrictEqual(info.id);
113
+ expect(chunk.chunkIndex).toStrictEqual(0n);
114
+ expect(chunk.content).toStrictEqual(new TextEncoder().encode('hello world'));
115
+
116
+ expect(sentPackets[2].value.case).toStrictEqual('streamTrailer');
117
+ const trailer = trailerOf(sentPackets[2]);
118
+ expect(trailer.streamId).toStrictEqual(info.id);
119
+ expect(trailer.reason).toStrictEqual('');
120
+ });
121
+
122
+ it('should send short BYTE data stream using non single packet "legacy" format and NO compression (happy path)', async () => {
123
+ const writer = await manager.streamBytes({
124
+ topic: 'my-topic',
125
+ });
126
+ await writer.write(new Uint8Array([0x00, 0x01, 0x02, 0x03]));
127
+ await writer.close();
128
+
129
+ // Make sure three packets were received, matching the legacy v1 data stream format
130
+ expect(sentPackets).toHaveLength(3);
131
+
132
+ expect(sentPackets[0].value.case).toBe('streamHeader');
133
+ const header = headerOf(sentPackets[0]);
134
+ expect(header.streamId).toStrictEqual(writer.info.id);
135
+ expect(header.topic).toStrictEqual('my-topic');
136
+ expect(header.contentHeader.case).toBe('byteHeader');
137
+
138
+ expect(sentPackets[1].value.case).toStrictEqual('streamChunk');
139
+ const chunk = chunkOf(sentPackets[1]);
140
+ expect(chunk.streamId).toStrictEqual(writer.info.id);
141
+ expect(chunk.chunkIndex).toStrictEqual(0n);
142
+ expect(chunk.content).toStrictEqual(new Uint8Array([0x00, 0x01, 0x02, 0x03]));
143
+
144
+ expect(sentPackets[2].value.case).toStrictEqual('streamTrailer');
145
+ const trailer = trailerOf(sentPackets[2]);
146
+ expect(trailer.streamId).toStrictEqual(writer.info.id);
147
+ expect(trailer.reason).toStrictEqual('');
148
+ });
149
+
150
+ it('should send long TEXT data stream without compression (happy path)', async () => {
151
+ const longPayload = new Array(40_000).fill('A').join('');
152
+ const info = await manager.sendText(longPayload, {
153
+ topic: 'my-topic',
154
+ });
155
+
156
+ // Make sure five packets were received, matching the legacy v1 data stream format
157
+ expect(sentPackets).toHaveLength(5);
158
+
159
+ expect(sentPackets[0].value.case).toBe('streamHeader');
160
+ const header = headerOf(sentPackets[0]);
161
+ expect(header.streamId).toStrictEqual(info.id);
162
+ expect(header.topic).toStrictEqual('my-topic');
163
+ expect(header.contentHeader.case).toBe('textHeader');
164
+
165
+ for (let i = 0; i < 3; i += 1) {
166
+ expect(sentPackets[i + 1].value.case).toStrictEqual('streamChunk');
167
+ const chunk = chunkOf(sentPackets[i + 1]);
168
+ expect(chunk.streamId).toStrictEqual(info.id);
169
+ expect(chunk.chunkIndex).toStrictEqual(BigInt(i));
170
+ expect(chunk.content.every((char) => char === 'A'.charCodeAt(0))).toBeTruthy();
171
+ }
172
+
173
+ expect(sentPackets[4].value.case).toStrictEqual('streamTrailer');
174
+ const trailer = trailerOf(sentPackets[4]);
175
+ expect(trailer.streamId).toStrictEqual(info.id);
176
+ expect(trailer.reason).toStrictEqual('');
177
+ });
178
+
179
+ it('should send long BYTE data stream without compression (happy path)', async () => {
180
+ const writer = await manager.streamBytes({
181
+ topic: 'my-topic',
182
+ });
183
+ await writer.write(new Uint8Array(20_000).fill(0x01));
184
+ await writer.write(new Uint8Array(20_000).fill(0x01));
185
+ await writer.close();
186
+
187
+ // Make sure five packets were received, matching the legacy v1 data stream format
188
+ expect(sentPackets).toHaveLength(6);
189
+
190
+ expect(sentPackets[0].value.case).toBe('streamHeader');
191
+ const header = headerOf(sentPackets[0]);
192
+ expect(header.streamId).toStrictEqual(writer.info.id);
193
+ expect(header.topic).toStrictEqual('my-topic');
194
+ expect(header.contentHeader.case).toBe('byteHeader');
195
+
196
+ // First write generates two packets, 15k long + 5k long
197
+ expect(sentPackets[1].value.case).toStrictEqual('streamChunk');
198
+ let chunk = chunkOf(sentPackets[1]);
199
+ expect(chunk.streamId).toStrictEqual(writer.info.id);
200
+ expect(chunk.chunkIndex).toStrictEqual(0n);
201
+ expect(chunk.content).toHaveLength(15_000); // MTU
202
+ expect(chunk.content.every((byte) => byte === 0x01)).toBeTruthy();
203
+
204
+ expect(sentPackets[2].value.case).toStrictEqual('streamChunk');
205
+ chunk = chunkOf(sentPackets[2]);
206
+ expect(chunk.streamId).toStrictEqual(writer.info.id);
207
+ expect(chunk.chunkIndex).toStrictEqual(1n);
208
+ expect(chunk.content).toHaveLength(5_000); // MTU
209
+ expect(chunk.content.every((byte) => byte === 0x01)).toBeTruthy();
210
+
211
+ // Second write generates two packets, 15k long + 5k long
212
+ expect(sentPackets[3].value.case).toStrictEqual('streamChunk');
213
+ chunk = chunkOf(sentPackets[3]);
214
+ expect(chunk.streamId).toStrictEqual(writer.info.id);
215
+ expect(chunk.chunkIndex).toStrictEqual(2n);
216
+ expect(chunk.content).toHaveLength(15_000);
217
+ expect(chunk.content.every((byte) => byte === 0x01)).toBeTruthy();
218
+
219
+ expect(sentPackets[4].value.case).toStrictEqual('streamChunk');
220
+ chunk = chunkOf(sentPackets[4]);
221
+ expect(chunk.streamId).toStrictEqual(writer.info.id);
222
+ expect(chunk.chunkIndex).toStrictEqual(3n);
223
+ expect(chunk.content).toHaveLength(5_000);
224
+ expect(chunk.content.every((byte) => byte === 0x01)).toBeTruthy();
225
+
226
+ expect(sentPackets[5].value.case).toStrictEqual('streamTrailer');
227
+ const trailer = trailerOf(sentPackets[5]);
228
+ expect(trailer.streamId).toStrictEqual(writer.info.id);
229
+ expect(trailer.reason).toStrictEqual('');
230
+ });
231
+
232
+ it('should send a FILE via sendFile without compression (happy path)', async () => {
233
+ const bytes = new Uint8Array(20_000).fill(0x07);
234
+ const info = await manager.sendFile(new File([bytes as NonSharedUint8Array], 'text.txt'), {
235
+ topic: 'my-topic',
236
+ });
237
+
238
+ // Pre-v2 recipients: uncompressed, multi-packet legacy format.
239
+ // 20k of data -> 15k + 5k chunks. 1 header + 2 chunks + 1 trailer = 4 packets.
240
+ expect(sentPackets).toHaveLength(4);
241
+
242
+ expect(sentPackets[0].value.case).toBe('streamHeader');
243
+ const header = headerOf(sentPackets[0]);
244
+ expect(header.streamId).toStrictEqual(info.id);
245
+ expect(header.topic).toStrictEqual('my-topic');
246
+ expect(header.contentHeader.case).toBe('byteHeader');
247
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
248
+
249
+ expect(sentPackets[1].value.case).toStrictEqual('streamChunk');
250
+ let chunk = chunkOf(sentPackets[1]);
251
+ expect(chunk.chunkIndex).toStrictEqual(0n);
252
+ expect(chunk.content).toHaveLength(15_000); // MTU
253
+ expect(chunk.content.every((byte) => byte === 0x07)).toBeTruthy();
254
+
255
+ expect(sentPackets[2].value.case).toStrictEqual('streamChunk');
256
+ chunk = chunkOf(sentPackets[2]);
257
+ expect(chunk.chunkIndex).toStrictEqual(1n);
258
+ expect(chunk.content).toHaveLength(5_000);
259
+ expect(chunk.content.every((byte) => byte === 0x07)).toBeTruthy();
260
+
261
+ expect(sentPackets[3].value.case).toStrictEqual('streamTrailer');
262
+ expect(trailerOf(sentPackets[3]).streamId).toStrictEqual(info.id);
263
+ });
264
+ });
265
+ describe('v2 -> room of all v2', () => {
266
+ let manager: OutgoingDataStreamManager, sentPackets: Array<DataPacket>;
267
+ beforeEach(() => {
268
+ const result = createManager({
269
+ alice: CLIENT_PROTOCOL_DATA_STREAM_V2,
270
+ bob: CLIENT_PROTOCOL_DATA_STREAM_V2,
271
+ noCompression: [CLIENT_PROTOCOL_DATA_STREAM_V2, []],
272
+ });
273
+ manager = result.manager;
274
+ sentPackets = result.sentPackets;
275
+ });
276
+
277
+ it('should send short TEXT data stream with single packet and compression (happy path)', async () => {
278
+ const info = await manager.sendText('hello hello compressible world', {
279
+ topic: 'my-topic',
280
+ destinationIdentities: ['alice', 'bob'],
281
+ });
282
+
283
+ // Make sure one single packet was used, since data streams v2 + compression is supported
284
+ // across all participants
285
+ expect(sentPackets).toHaveLength(1);
286
+
287
+ expect(sentPackets[0].value.case).toBe('streamHeader');
288
+ const header = headerOf(sentPackets[0]);
289
+ expect(header.streamId).toStrictEqual(info.id);
290
+ expect(header.topic).toStrictEqual('my-topic');
291
+ expect(header.contentHeader.case).toBe('textHeader');
292
+
293
+ // Make sure the contents of that packet was compressed
294
+ expect(header.compression).toBe(DataStream_CompressionType.DEFLATE_RAW);
295
+ expect(header.inlineContent).toBeInstanceOf(Uint8Array);
296
+ expect(header.inlineContent).not.toStrictEqual(
297
+ new TextEncoder().encode('hello hello compressible world'),
298
+ );
299
+ });
300
+ it('should send short TEXT data stream with uncompressible payload in single packet', async () => {
301
+ const info = await manager.sendText('short', {
302
+ topic: 'my-topic',
303
+ destinationIdentities: ['alice', 'bob'],
304
+ });
305
+
306
+ // Make sure one single packet was used, since data streams v2 + compression is supported
307
+ // across all participants
308
+ expect(sentPackets).toHaveLength(1);
309
+
310
+ expect(sentPackets[0].value.case).toBe('streamHeader');
311
+ const header = headerOf(sentPackets[0]);
312
+ expect(header.streamId).toStrictEqual(info.id);
313
+ expect(header.topic).toStrictEqual('my-topic');
314
+ expect(header.contentHeader.case).toBe('textHeader');
315
+
316
+ // Make sure the contents of that packet was uncompressed - "short" isn't long enough to
317
+ // meaningfully compress with DEFLATE
318
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
319
+ expect(header.inlineContent).toStrictEqual(new TextEncoder().encode('short'));
320
+ });
321
+ it('should send short data stream with single packet and NO compression if remote participant does not support compression', async () => {
322
+ const info = await manager.sendText('hello hello compressible world', {
323
+ topic: 'my-topic',
324
+ destinationIdentities: ['noCompression'],
325
+ });
326
+
327
+ // Make sure one single packet was used, since data streams v2 is supported for that
328
+ // participant.
329
+ expect(sentPackets).toHaveLength(1);
330
+
331
+ expect(sentPackets[0].value.case).toBe('streamHeader');
332
+ const header = headerOf(sentPackets[0]);
333
+ expect(header.streamId).toStrictEqual(info.id);
334
+ expect(header.topic).toStrictEqual('my-topic');
335
+ expect(header.contentHeader.case).toBe('textHeader');
336
+
337
+ // Make sure the contents of that packet was NOT compressed
338
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
339
+ expect(header.inlineContent).toStrictEqual(
340
+ new TextEncoder().encode('hello hello compressible world'),
341
+ );
342
+ });
343
+ it('should send long but highly compressible TEXT data stream as single packet', async () => {
344
+ // A phrase which repeats over and over should compress extremely well.
345
+ const text = new Array(20_000).fill('hello world').join('');
346
+
347
+ const info = await manager.sendText(text, {
348
+ topic: 'my-topic',
349
+ destinationIdentities: ['alice', 'bob'],
350
+ });
351
+
352
+ // Make sure one single packet was used, since data streams v2 is supported and the contents
353
+ // should be able to be highly compressed to be well under the 15k MTU
354
+ expect(sentPackets).toHaveLength(1);
355
+
356
+ expect(sentPackets[0].value.case).toBe('streamHeader');
357
+ const header = headerOf(sentPackets[0]);
358
+ expect(header.streamId).toStrictEqual(info.id);
359
+ expect(header.topic).toStrictEqual('my-topic');
360
+ expect(header.contentHeader.case).toBe('textHeader');
361
+
362
+ // Make sure the contents of that packet was compressed
363
+ expect(header.compression).toBe(DataStream_CompressionType.DEFLATE_RAW);
364
+ expect(header.inlineContent).toBeInstanceOf(Uint8Array);
365
+ // Compressed bytes must not begin with the raw UTF-8 prefix of the payload.
366
+ const helloWorld = new TextEncoder().encode('hello world');
367
+ expect(header.inlineContent!.slice(0, helloWorld.length)).not.toStrictEqual(helloWorld);
368
+ });
369
+ it('should send long but somewhat compressible data stream as a compressed multi packet data stream', async () => {
370
+ // Mostly incompressible, but the hello world parts repeating should mean that the compressed
371
+ // contents is smaller than the full uncompressed data.
372
+ const text = new Array(50)
373
+ .fill(null)
374
+ .map(() => `hello world${randomText(1_000)}`)
375
+ .join('');
376
+
377
+ const info = await manager.sendText(text, {
378
+ topic: 'my-topic',
379
+ destinationIdentities: ['alice', 'bob'],
380
+ });
381
+
382
+ // 1 header + 3 data packets + 1 trailer = 5 total packets
383
+ //
384
+ // 3 data packets is less than the Math.ceil(~50k / 15k) = 4 packets that would be
385
+ // required if data was uncompressed
386
+ expect(sentPackets).toHaveLength(5);
387
+
388
+ expect(sentPackets[0].value.case).toBe('streamHeader');
389
+ const header = headerOf(sentPackets[0]);
390
+ expect(header.streamId).toStrictEqual(info.id);
391
+ expect(header.topic).toStrictEqual('my-topic');
392
+ expect(header.contentHeader.case).toBe('textHeader');
393
+
394
+ // Make sure the contents of that packet was compressed
395
+ expect(header.compression).toBe(DataStream_CompressionType.DEFLATE_RAW);
396
+
397
+ // Verify there are three data packets:
398
+ expect(sentPackets[1].value.case).toStrictEqual('streamChunk');
399
+ let chunk = chunkOf(sentPackets[1]);
400
+ expect(chunk.streamId).toStrictEqual(info.id);
401
+ expect(chunk.chunkIndex).toStrictEqual(0n);
402
+ expect(chunk.content).toHaveLength(15_000); // MTU
403
+
404
+ expect(sentPackets[2].value.case).toStrictEqual('streamChunk');
405
+ expect(sentPackets[3].value.case).toStrictEqual('streamChunk');
406
+
407
+ // Final packet should be a trailer
408
+ expect(sentPackets[4].value.case).toStrictEqual('streamTrailer');
409
+ const trailer = trailerOf(sentPackets[4]);
410
+ expect(trailer.streamId).toStrictEqual(info.id);
411
+ expect(trailer.reason).toStrictEqual('');
412
+ });
413
+ it('should send long, uncompressible data stream as a compressed multi packet data stream', async () => {
414
+ // This is random data which should be uncompressible
415
+ const bytes = randomBytes(50_000);
416
+ const info = await manager.sendFile(new File([bytes as NonSharedUint8Array], 'text.txt'), {
417
+ topic: 'my-topic',
418
+ destinationIdentities: ['alice', 'bob'],
419
+ });
420
+
421
+ // Math.ceil(~50k / 15k) = 4 data packets
422
+ // 1 header + 4 data packets + 1 trailer = 6 total packets
423
+ expect(sentPackets).toHaveLength(6);
424
+
425
+ expect(sentPackets[0].value.case).toBe('streamHeader');
426
+ const header = headerOf(sentPackets[0]);
427
+ expect(header.streamId).toStrictEqual(info.id);
428
+ expect(header.topic).toStrictEqual('my-topic');
429
+ expect(header.contentHeader.case).toBe('byteHeader');
430
+
431
+ // Make sure the contents of that packet was NOT compressed
432
+ expect(header.compression).toBe(DataStream_CompressionType.DEFLATE_RAW);
433
+
434
+ // Verify there are four data packets:
435
+ let totalLength = 0;
436
+ expect(sentPackets[1].value.case).toStrictEqual('streamChunk');
437
+ let chunk = chunkOf(sentPackets[1]);
438
+ expect(chunk.streamId).toStrictEqual(info.id);
439
+ expect(chunk.chunkIndex).toStrictEqual(0n);
440
+ expect(chunk.content).toHaveLength(15_000); // MTU
441
+ totalLength += chunk.content.byteLength;
442
+
443
+ expect(sentPackets[2].value.case).toStrictEqual('streamChunk');
444
+ chunk = chunkOf(sentPackets[2]);
445
+ totalLength += chunk.content.byteLength;
446
+
447
+ expect(sentPackets[3].value.case).toStrictEqual('streamChunk');
448
+ chunk = chunkOf(sentPackets[3]);
449
+ totalLength += chunk.content.byteLength;
450
+
451
+ expect(sentPackets[4].value.case).toStrictEqual('streamChunk');
452
+ chunk = chunkOf(sentPackets[4]);
453
+ totalLength += chunk.content.byteLength;
454
+
455
+ // Make sure total length is LARGER than the raw bytes length (only slightly, due to the extra
456
+ // DEFLATE metadata being added to an otherwise incompressible binary blob)
457
+ //
458
+ // This is sort of unfortunate that this happens, but the tradeoff to this slight size bump is
459
+ // that the whole binary doesn't have to be buffered into memory all at once.
460
+ expect(totalLength).toBeGreaterThan(bytes.byteLength);
461
+
462
+ // Final packet should be a trailer
463
+ expect(sentPackets[5].value.case).toStrictEqual('streamTrailer');
464
+ const trailer = trailerOf(sentPackets[5]);
465
+ expect(trailer.streamId).toStrictEqual(info.id);
466
+ expect(trailer.reason).toStrictEqual('');
467
+ });
468
+ it('should send short data stream with single packet but skip compression due to compress: false being passed', async () => {
469
+ const info = await manager.sendText('hello hello compressible world', {
470
+ topic: 'my-topic',
471
+ destinationIdentities: ['alice', 'bob'],
472
+ compress: false,
473
+ });
474
+
475
+ // Make sure one single packet was used, since data streams v2 is supported across all participants
476
+ expect(sentPackets).toHaveLength(1);
477
+
478
+ expect(sentPackets[0].value.case).toBe('streamHeader');
479
+ const header = headerOf(sentPackets[0]);
480
+ expect(header.streamId).toStrictEqual(info.id);
481
+ expect(header.topic).toStrictEqual('my-topic');
482
+ expect(header.contentHeader.case).toBe('textHeader');
483
+
484
+ // Make sure the contents of that packet was NOT compressed (compress: false opt-out)
485
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
486
+ expect(header.inlineContent).toStrictEqual(
487
+ new TextEncoder().encode('hello hello compressible world'),
488
+ );
489
+ });
490
+ it('should send long but somewhat compressible data stream but skip compression due to compress: false being passed', async () => {
491
+ // Mostly incompressible, but the hello world parts repeating should mean that the compressed
492
+ // contents is smaller than the full uncompressed data.
493
+ const text = new Array(50)
494
+ .fill(null)
495
+ .map(() => `hello world${randomText(1_000)}`)
496
+ .join('');
497
+
498
+ const info = await manager.sendText(text, {
499
+ topic: 'my-topic',
500
+ destinationIdentities: ['alice', 'bob'],
501
+ compress: false,
502
+ });
503
+
504
+ // Math.ceil(~50k / 15k) = 4 data packets
505
+ // 1 header + 4 data packets + 1 trailer = 6 total packets
506
+ expect(sentPackets).toHaveLength(6);
507
+
508
+ expect(sentPackets[0].value.case).toBe('streamHeader');
509
+ const header = headerOf(sentPackets[0]);
510
+ expect(header.streamId).toStrictEqual(info.id);
511
+ expect(header.topic).toStrictEqual('my-topic');
512
+ expect(header.contentHeader.case).toBe('textHeader');
513
+
514
+ // Make sure the contents of that packet was uncompressed
515
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
516
+
517
+ // Verify there are four data packets:
518
+ expect(sentPackets[1].value.case).toStrictEqual('streamChunk');
519
+ let chunk = chunkOf(sentPackets[1]);
520
+ expect(chunk.streamId).toStrictEqual(info.id);
521
+ expect(chunk.chunkIndex).toStrictEqual(0n);
522
+ expect(chunk.content).toHaveLength(15_000); // MTU
523
+
524
+ expect(sentPackets[2].value.case).toStrictEqual('streamChunk');
525
+ expect(sentPackets[3].value.case).toStrictEqual('streamChunk');
526
+ expect(sentPackets[4].value.case).toStrictEqual('streamChunk');
527
+
528
+ // Final packet should be a trailer
529
+ expect(sentPackets[5].value.case).toStrictEqual('streamTrailer');
530
+ const trailer = trailerOf(sentPackets[5]);
531
+ expect(trailer.streamId).toStrictEqual(info.id);
532
+ expect(trailer.reason).toStrictEqual('');
533
+ });
534
+
535
+ it('should NEVER use compression or single packet data streams with streamText', async () => {
536
+ const writer = await manager.streamText({
537
+ topic: 'my-topic',
538
+ destinationIdentities: ['noCompression'],
539
+ });
540
+
541
+ // Make sure the header packet was sent
542
+ expect(sentPackets).toHaveLength(1);
543
+
544
+ expect(sentPackets[0].value.case).toBe('streamHeader');
545
+ const header = headerOf(sentPackets[0]);
546
+ expect(header.streamId).toStrictEqual(writer.info.id);
547
+ expect(header.topic).toStrictEqual('my-topic');
548
+ expect(header.contentHeader.case).toBe('textHeader');
549
+ expect(header.compression).toBe(DataStream_CompressionType.NONE); // Make sure compression is disabled
550
+
551
+ await writer.write('hello world');
552
+
553
+ // Make sure a single chunk packet was emitted
554
+ expect(sentPackets).toHaveLength(2 /* 1 header + 1 chunk */);
555
+
556
+ expect(sentPackets[1].value.case).toBe('streamChunk');
557
+ const chunk = chunkOf(sentPackets[1]);
558
+ expect(chunk.streamId).toStrictEqual(writer.info.id);
559
+ expect(chunk.content).toStrictEqual(new TextEncoder().encode('hello world'));
560
+
561
+ await writer.close();
562
+
563
+ // Finally, verify the trailer
564
+ expect(sentPackets).toHaveLength(3 /* 1 header + 1 chunk + 1 trailer */);
565
+ expect(sentPackets[2].value.case).toBe('streamTrailer');
566
+ });
567
+ it('should NEVER use compression or single packet data streams with streamBytes', async () => {
568
+ const writer = await manager.streamBytes({
569
+ topic: 'my-topic',
570
+ destinationIdentities: ['noCompression'],
571
+ });
572
+
573
+ // Make sure the header packet was sent
574
+ expect(sentPackets).toHaveLength(1);
575
+
576
+ expect(sentPackets[0].value.case).toBe('streamHeader');
577
+ const header = headerOf(sentPackets[0]);
578
+ expect(header.streamId).toStrictEqual(writer.info.id);
579
+ expect(header.topic).toStrictEqual('my-topic');
580
+ expect(header.contentHeader.case).toBe('byteHeader');
581
+ expect(header.compression).toBe(DataStream_CompressionType.NONE); // Make sure compression is disabled
582
+
583
+ await writer.write(new Uint8Array([0x00, 0x01, 0x02, 0x03]));
584
+
585
+ // Make sure a single chunk packet was emitted
586
+ expect(sentPackets).toHaveLength(2 /* 1 header + 1 chunk */);
587
+
588
+ expect(sentPackets[1].value.case).toBe('streamChunk');
589
+ const chunk = chunkOf(sentPackets[1]);
590
+ expect(chunk.streamId).toStrictEqual(writer.info.id);
591
+ expect(chunk.content).toStrictEqual(new Uint8Array([0x00, 0x01, 0x02, 0x03]));
592
+
593
+ await writer.close();
594
+
595
+ // Finally, verify the trailer
596
+ expect(sentPackets).toHaveLength(3 /* 1 header + 1 chunk + 1 trailer */);
597
+ expect(sentPackets[2].value.case).toBe('streamTrailer');
598
+ });
599
+
600
+ it('should NOT send bytes single packet with sendFile', async () => {
601
+ // This is random data which should be uncompressible
602
+ const bytes = new Uint8Array(10_000).fill(0x01);
603
+ const info = await manager.sendFile(new File([bytes as NonSharedUint8Array], 'text.txt'), {
604
+ topic: 'my-topic',
605
+ destinationIdentities: ['alice', 'bob'],
606
+ });
607
+
608
+ // Should be a multi-packet result
609
+ //
610
+ // Sending single packet data streams for files is tricky because it's really difficult to
611
+ // determine ahead of time if a file can fit into a single packet without a ton of ahead of
612
+ // time in memory buffering.
613
+ expect(sentPackets).toHaveLength(3);
614
+
615
+ expect(sentPackets[0].value.case).toBe('streamHeader');
616
+ const header = headerOf(sentPackets[0]);
617
+ expect(header.streamId).toStrictEqual(info.id);
618
+ expect(header.topic).toStrictEqual('my-topic');
619
+ expect(header.contentHeader.case).toBe('byteHeader');
620
+
621
+ // Make sure the contents of that packet was NOT compressed
622
+ expect(header.compression).toBe(DataStream_CompressionType.DEFLATE_RAW);
623
+
624
+ expect(sentPackets[1].value.case).toStrictEqual('streamChunk');
625
+ let chunk = chunkOf(sentPackets[1]);
626
+ expect(chunk.streamId).toStrictEqual(info.id);
627
+ expect(chunk.chunkIndex).toStrictEqual(0n);
628
+
629
+ // Make sure contents were compressed
630
+ expect(chunk.content.byteLength).toBeLessThan(bytes.byteLength);
631
+
632
+ // Final packet should be a trailer
633
+ expect(sentPackets[2].value.case).toStrictEqual('streamTrailer');
634
+ const trailer = trailerOf(sentPackets[2]);
635
+ expect(trailer.streamId).toStrictEqual(info.id);
636
+ expect(trailer.reason).toStrictEqual('');
637
+ });
638
+
639
+ it('should send a FILE via sendFile WITHOUT compression if remote does not support compression', async () => {
640
+ const bytes = new Uint8Array(10_000).fill(0x07);
641
+ const info = await manager.sendFile(new File([bytes as NonSharedUint8Array], 'text.txt'), {
642
+ topic: 'my-topic',
643
+ destinationIdentities: ['noCompression'],
644
+ });
645
+
646
+ // v2 recipient but no deflate-raw capability: uncompressed, multi-packet (never inline).
647
+ expect(sentPackets).toHaveLength(3);
648
+
649
+ expect(sentPackets[0].value.case).toBe('streamHeader');
650
+ const header = headerOf(sentPackets[0]);
651
+ expect(header.streamId).toStrictEqual(info.id);
652
+ expect(header.contentHeader.case).toBe('byteHeader');
653
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
654
+ expect(header.inlineContent).toBeUndefined();
655
+
656
+ expect(sentPackets[1].value.case).toStrictEqual('streamChunk');
657
+ const chunk = chunkOf(sentPackets[1]);
658
+ expect(chunk.chunkIndex).toStrictEqual(0n);
659
+ expect(chunk.content).toHaveLength(10_000); // uncompressed, single chunk under the MTU
660
+ expect(chunk.content.every((byte) => byte === 0x07)).toBeTruthy();
661
+
662
+ expect(sentPackets[2].value.case).toStrictEqual('streamTrailer');
663
+ expect(trailerOf(sentPackets[2]).streamId).toStrictEqual(info.id);
664
+ });
665
+
666
+ it('should send an empty FILE via sendFile', async () => {
667
+ const info = await manager.sendFile(new File([], 'empty.bin'), {
668
+ topic: 'my-topic',
669
+ destinationIdentities: ['alice', 'bob'],
670
+ });
671
+
672
+ // An empty file still produces a well-formed compressed byte stream: a header declaring zero
673
+ // length, the deflate stream's final block, and a trailer.
674
+ expect(sentPackets[0].value.case).toBe('streamHeader');
675
+ const header = headerOf(sentPackets[0]);
676
+ expect(header.streamId).toStrictEqual(info.id);
677
+ expect(header.contentHeader.case).toBe('byteHeader');
678
+ expect(header.totalLength).toStrictEqual(0n);
679
+ expect(header.compression).toBe(DataStream_CompressionType.DEFLATE_RAW);
680
+
681
+ const last = sentPackets[sentPackets.length - 1];
682
+ expect(last.value.case).toStrictEqual('streamTrailer');
683
+ expect(trailerOf(last).streamId).toStrictEqual(info.id);
684
+ });
685
+ });
686
+ describe('v2 -> room of mixed v1 / v2', () => {
687
+ let manager: OutgoingDataStreamManager, sentPackets: Array<DataPacket>;
688
+ beforeEach(() => {
689
+ const result = createManager({
690
+ alice: CLIENT_PROTOCOL_DEFAULT,
691
+ bob: CLIENT_PROTOCOL_DATA_STREAM_V2,
692
+ jim: CLIENT_PROTOCOL_DATA_STREAM_V2,
693
+ mallory: CLIENT_PROTOCOL_DATA_STREAM_RPC,
694
+ noCompression: [CLIENT_PROTOCOL_DATA_STREAM_V2, []],
695
+ });
696
+ manager = result.manager;
697
+ sentPackets = result.sentPackets;
698
+ });
699
+
700
+ it('should send data stream using v1 legacy data stream format in room of mixed v1/v2', async () => {
701
+ const info = await manager.sendText('hello world', {
702
+ topic: 'my-topic',
703
+ });
704
+
705
+ // Make sure three packets were received, matching the legacy v1 data stream format
706
+ expect(sentPackets).toHaveLength(3);
707
+
708
+ expect(sentPackets[0].value.case).toBe('streamHeader');
709
+ const header = headerOf(sentPackets[0]);
710
+ expect(header.streamId).toStrictEqual(info.id);
711
+ expect(header.topic).toStrictEqual('my-topic');
712
+ expect(header.contentHeader.case).toBe('textHeader');
713
+
714
+ expect(sentPackets[1].value.case).toStrictEqual('streamChunk');
715
+ const chunk = chunkOf(sentPackets[1]);
716
+ expect(chunk.streamId).toStrictEqual(info.id);
717
+ expect(chunk.chunkIndex).toStrictEqual(0n);
718
+ expect(chunk.content).toStrictEqual(new TextEncoder().encode('hello world'));
719
+
720
+ expect(sentPackets[2].value.case).toStrictEqual('streamTrailer');
721
+ const trailer = trailerOf(sentPackets[2]);
722
+ expect(trailer.streamId).toStrictEqual(info.id);
723
+ expect(trailer.reason).toStrictEqual('');
724
+ });
725
+ it('should send data stream using data stream v2 format + compression when only sending to a subset of participants that are all v2', async () => {
726
+ const info = await manager.sendText('hello hello compressible world', {
727
+ topic: 'my-topic',
728
+ destinationIdentities: ['bob', 'jim'],
729
+ });
730
+
731
+ // Make sure one single packet was used, since data streams v2 + compression is supported
732
+ // across bob + jim
733
+ expect(sentPackets).toHaveLength(1);
734
+
735
+ expect(sentPackets[0].value.case).toBe('streamHeader');
736
+ const header = headerOf(sentPackets[0]);
737
+ expect(header.streamId).toStrictEqual(info.id);
738
+ expect(header.topic).toStrictEqual('my-topic');
739
+ expect(header.contentHeader.case).toBe('textHeader');
740
+
741
+ // Make sure the contents of that packet was compressed
742
+ expect(header.compression).toBe(DataStream_CompressionType.DEFLATE_RAW);
743
+ expect(header.inlineContent).toBeInstanceOf(Uint8Array);
744
+ expect(header.inlineContent).not.toStrictEqual(
745
+ new TextEncoder().encode('hello hello compressible world'),
746
+ );
747
+ });
748
+ it('should send data stream using data stream v2 format but NO compression when only sending to a subset of participants where one does NOT support compression', async () => {
749
+ const info = await manager.sendText('hello hello compressible world', {
750
+ topic: 'my-topic',
751
+ destinationIdentities: ['bob', 'jim', 'noCompression'],
752
+ });
753
+
754
+ // Make sure one single packet was used, since data streams v2 + compression is supported
755
+ // across bob + jim
756
+ expect(sentPackets).toHaveLength(1);
757
+
758
+ expect(sentPackets[0].value.case).toBe('streamHeader');
759
+ const header = headerOf(sentPackets[0]);
760
+ expect(header.streamId).toStrictEqual(info.id);
761
+ expect(header.topic).toStrictEqual('my-topic');
762
+ expect(header.contentHeader.case).toBe('textHeader');
763
+
764
+ // Make sure the contents of that packet was compressed
765
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
766
+ expect(header.inlineContent).toStrictEqual(
767
+ new TextEncoder().encode('hello hello compressible world'),
768
+ );
769
+ });
770
+ });
771
+
772
+ describe('sendBytes', () => {
773
+ describe('v2 -> room of all v2', () => {
774
+ let manager: OutgoingDataStreamManager, sentPackets: Array<DataPacket>;
775
+ beforeEach(() => {
776
+ const result = createManager({
777
+ alice: CLIENT_PROTOCOL_DATA_STREAM_V2,
778
+ bob: CLIENT_PROTOCOL_DATA_STREAM_V2,
779
+ noCompression: [CLIENT_PROTOCOL_DATA_STREAM_V2, []],
780
+ });
781
+ manager = result.manager;
782
+ sentPackets = result.sentPackets;
783
+ });
784
+
785
+ it('should send short compressible BYTE payload as a single compressed packet (happy path)', async () => {
786
+ const bytes = new TextEncoder().encode('hello hello compressible world');
787
+ const info = await manager.sendBytes(bytes, {
788
+ topic: 'my-topic',
789
+ attributes: { foo: 'bar' },
790
+ destinationIdentities: ['alice', 'bob'],
791
+ });
792
+
793
+ expect(sentPackets).toHaveLength(1);
794
+ expect(sentPackets[0].value.case).toBe('streamHeader');
795
+ const header = headerOf(sentPackets[0]);
796
+ expect(header.streamId).toStrictEqual(info.id);
797
+ expect(header.topic).toStrictEqual('my-topic');
798
+ expect(header.contentHeader.case).toBe('byteHeader');
799
+
800
+ // Compressed inline payload
801
+ expect(header.compression).toBe(DataStream_CompressionType.DEFLATE_RAW);
802
+ expect(header.inlineContent).toBeInstanceOf(Uint8Array);
803
+ expect(header.inlineContent).not.toStrictEqual(bytes);
804
+
805
+ // Returned info uses the byte-stream defaults
806
+ expect(info.name).toStrictEqual('unknown');
807
+ expect(info.mimeType).toStrictEqual('application/octet-stream');
808
+ expect(info.size).toStrictEqual(bytes.byteLength);
809
+ expect(info.attributes).toStrictEqual({ foo: 'bar' });
810
+ });
811
+
812
+ it('should send short uncompressible BYTE payload inline without compression', async () => {
813
+ const bytes = new Uint8Array([0x00, 0x01, 0x02, 0x03]);
814
+ const info = await manager.sendBytes(bytes, {
815
+ topic: 'my-topic',
816
+ destinationIdentities: ['alice', 'bob'],
817
+ });
818
+
819
+ expect(sentPackets).toHaveLength(1);
820
+ const header = headerOf(sentPackets[0]);
821
+ expect(header.streamId).toStrictEqual(info.id);
822
+ expect(header.contentHeader.case).toBe('byteHeader');
823
+ // Tiny payload doesn't shrink under DEFLATE framing, so it's sent raw.
824
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
825
+ expect(header.inlineContent).toStrictEqual(bytes);
826
+ });
827
+
828
+ it('should send BYTE payload inline with NO compression if a recipient does not support compression', async () => {
829
+ const bytes = new TextEncoder().encode('hello hello compressible world');
830
+ const info = await manager.sendBytes(bytes, {
831
+ topic: 'my-topic',
832
+ destinationIdentities: ['noCompression'],
833
+ });
834
+
835
+ // Inline still applies (gated on v2 alone), but compression is skipped.
836
+ expect(sentPackets).toHaveLength(1);
837
+ const header = headerOf(sentPackets[0]);
838
+ expect(header.streamId).toStrictEqual(info.id);
839
+ expect(header.contentHeader.case).toBe('byteHeader');
840
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
841
+ expect(header.inlineContent).toStrictEqual(bytes);
842
+ });
843
+
844
+ it('should send long, highly compressible BYTE payload as a single compressed packet', async () => {
845
+ // Repeating bytes compress well under the 15k MTU even though the raw payload is 50k.
846
+ const bytes = new Uint8Array(50_000).fill(0x01);
847
+ const info = await manager.sendBytes(bytes, {
848
+ topic: 'my-topic',
849
+ destinationIdentities: ['alice', 'bob'],
850
+ });
851
+
852
+ expect(sentPackets).toHaveLength(1);
853
+ const header = headerOf(sentPackets[0]);
854
+ expect(header.streamId).toStrictEqual(info.id);
855
+ expect(header.contentHeader.case).toBe('byteHeader');
856
+ expect(header.compression).toBe(DataStream_CompressionType.DEFLATE_RAW);
857
+ expect(header.inlineContent).toBeInstanceOf(Uint8Array);
858
+ expect(header.inlineContent!.byteLength).toBeLessThan(bytes.byteLength);
859
+ });
860
+
861
+ it('should send long but somewhat compressible BYTE payload as a compressed multi packet stream', async () => {
862
+ const bytes = new TextEncoder().encode(
863
+ new Array(50)
864
+ .fill(null)
865
+ .map(() => `hello world${randomText(1_000)}`)
866
+ .join(''),
867
+ );
868
+ const info = await manager.sendBytes(bytes, {
869
+ topic: 'my-topic',
870
+ destinationIdentities: ['alice', 'bob'],
871
+ });
872
+
873
+ // 1 header + 3 chunks + 1 trailer (compressed ~45k -> 3 MTU chunks)
874
+ expect(sentPackets).toHaveLength(5);
875
+ const header = headerOf(sentPackets[0]);
876
+ expect(header.streamId).toStrictEqual(info.id);
877
+ expect(header.contentHeader.case).toBe('byteHeader');
878
+ expect(header.compression).toBe(DataStream_CompressionType.DEFLATE_RAW);
879
+ expect(header.inlineContent).toBeUndefined();
880
+
881
+ expect(sentPackets[1].value.case).toStrictEqual('streamChunk');
882
+ const chunk = chunkOf(sentPackets[1]);
883
+ expect(chunk.chunkIndex).toStrictEqual(0n);
884
+ expect(chunk.content).toHaveLength(15_000); // MTU
885
+ expect(sentPackets[2].value.case).toStrictEqual('streamChunk');
886
+ expect(sentPackets[3].value.case).toStrictEqual('streamChunk');
887
+ expect(sentPackets[4].value.case).toStrictEqual('streamTrailer');
888
+ expect(trailerOf(sentPackets[4]).streamId).toStrictEqual(info.id);
889
+ });
890
+
891
+ it('should skip compression for an inline payload when compress: false', async () => {
892
+ const bytes = new TextEncoder().encode('hello hello compressible world');
893
+ const info = await manager.sendBytes(bytes, {
894
+ topic: 'my-topic',
895
+ destinationIdentities: ['alice', 'bob'],
896
+ compress: false,
897
+ });
898
+
899
+ expect(sentPackets).toHaveLength(1);
900
+ const header = headerOf(sentPackets[0]);
901
+ expect(header.streamId).toStrictEqual(info.id);
902
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
903
+ expect(header.inlineContent).toStrictEqual(bytes);
904
+ });
905
+
906
+ it('should send an uncompressed multi packet stream when compress: false and payload exceeds the MTU', async () => {
907
+ const bytes = new Uint8Array(40_000).fill(0x07);
908
+ const info = await manager.sendBytes(bytes, {
909
+ topic: 'my-topic',
910
+ destinationIdentities: ['alice', 'bob'],
911
+ compress: false,
912
+ });
913
+
914
+ // 40k -> 15k + 15k + 10k chunks. 1 header + 3 chunks + 1 trailer = 5 packets.
915
+ expect(sentPackets).toHaveLength(5);
916
+ const header = headerOf(sentPackets[0]);
917
+ expect(header.streamId).toStrictEqual(info.id);
918
+ expect(header.contentHeader.case).toBe('byteHeader');
919
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
920
+ expect(header.inlineContent).toBeUndefined();
921
+
922
+ expect(sentPackets[1].value.case).toStrictEqual('streamChunk');
923
+ let chunk = chunkOf(sentPackets[1]);
924
+ expect(chunk.chunkIndex).toStrictEqual(0n);
925
+ expect(chunk.content).toHaveLength(15_000);
926
+ expect(chunk.content.every((byte) => byte === 0x07)).toBeTruthy();
927
+ chunk = chunkOf(sentPackets[3]);
928
+ expect(chunk.content).toHaveLength(10_000);
929
+ expect(sentPackets[4].value.case).toStrictEqual('streamTrailer');
930
+ });
931
+ });
932
+
933
+ describe('v2 -> room of all v1', () => {
934
+ it('should send a legacy uncompressed multi packet stream', async () => {
935
+ const { manager, sentPackets } = createManager({
936
+ alice: CLIENT_PROTOCOL_DEFAULT,
937
+ jim: CLIENT_PROTOCOL_DATA_STREAM_RPC,
938
+ });
939
+ const bytes = new Uint8Array([0x00, 0x01, 0x02, 0x03]);
940
+ const info = await manager.sendBytes(bytes, { topic: 'my-topic' });
941
+
942
+ // No v2 recipients: header + chunk + trailer, never inline, never compressed.
943
+ expect(sentPackets).toHaveLength(3);
944
+ const header = headerOf(sentPackets[0]);
945
+ expect(header.streamId).toStrictEqual(info.id);
946
+ expect(header.contentHeader.case).toBe('byteHeader');
947
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
948
+ expect(header.inlineContent).toBeUndefined();
949
+
950
+ expect(sentPackets[1].value.case).toStrictEqual('streamChunk');
951
+ const chunk = chunkOf(sentPackets[1]);
952
+ expect(chunk.chunkIndex).toStrictEqual(0n);
953
+ expect(chunk.content).toStrictEqual(bytes);
954
+ expect(sentPackets[2].value.case).toStrictEqual('streamTrailer');
955
+ expect(trailerOf(sentPackets[2]).streamId).toStrictEqual(info.id);
956
+ });
957
+ });
958
+ });
959
+
960
+ describe('header size limit (MTU)', () => {
961
+ // Attributes large enough that the serialized header packet alone exceeds
962
+ // STREAM_CHUNK_SIZE_BYTES (15 000).
963
+ const hugeAttributes = { big: 'x'.repeat(20_000) };
964
+
965
+ it('should throw HeaderTooLarge when sending a chunked TEXT stream with oversized attributes (pre-v2 room)', async () => {
966
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DEFAULT });
967
+
968
+ await expect(
969
+ manager.sendText('hello world', { topic: 'my-topic', attributes: hugeAttributes }),
970
+ ).rejects.toThrow('exceeds');
971
+
972
+ // The error must be raised before anything hits the wire.
973
+ expect(sentPackets).toHaveLength(0);
974
+ });
975
+
976
+ it('should throw HeaderTooLarge when oversized attributes force the inline path to fall back (v2 room)', async () => {
977
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DATA_STREAM_V2 });
978
+
979
+ // The payload is small, but the attributes alone blow the MTU budget: the inline attempt
980
+ // falls through gracefully (no throw), and then the chunked header send enforces the hard
981
+ // limit — per spec, large *attributes* throw while a large *payload* falls back fine.
982
+ await expect(
983
+ manager.sendText('hello hello compressible world', {
984
+ topic: 'my-topic',
985
+ destinationIdentities: ['alice'],
986
+ attributes: hugeAttributes,
987
+ }),
988
+ ).rejects.toThrow('exceeds');
989
+
990
+ expect(sentPackets).toHaveLength(0);
991
+ });
992
+
993
+ it('should throw HeaderTooLarge when opening a streamText writer with oversized attributes', async () => {
994
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DATA_STREAM_V2 });
995
+
996
+ await expect(
997
+ manager.streamText({ topic: 'my-topic', attributes: hugeAttributes }),
998
+ ).rejects.toThrow('exceeds');
999
+
1000
+ expect(sentPackets).toHaveLength(0);
1001
+ });
1002
+
1003
+ it('should throw HeaderTooLarge when opening a streamBytes writer with oversized attributes', async () => {
1004
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DATA_STREAM_V2 });
1005
+
1006
+ await expect(
1007
+ manager.streamBytes({ topic: 'my-topic', attributes: hugeAttributes }),
1008
+ ).rejects.toThrow('exceeds');
1009
+
1010
+ expect(sentPackets).toHaveLength(0);
1011
+ });
1012
+ });
1013
+
1014
+ describe('attachments', () => {
1015
+ it('should send attachments as separate byte streams referenced by attachedStreamIds (never inline)', async () => {
1016
+ const { manager, sentPackets } = createManager({
1017
+ alice: CLIENT_PROTOCOL_DATA_STREAM_V2,
1018
+ bob: CLIENT_PROTOCOL_DATA_STREAM_V2,
1019
+ });
1020
+ const file = new File([new Uint8Array(100).fill(0x07) as NonSharedUint8Array], 'att.bin');
1021
+
1022
+ const info = await manager.sendText('hello hello compressible world', {
1023
+ topic: 'my-topic',
1024
+ destinationIdentities: ['alice', 'bob'],
1025
+ attachments: [file],
1026
+ });
1027
+
1028
+ // The text part must NOT be inline — attachments disable the single-packet path — so both
1029
+ // the text stream and the attachment stream open with their own headers.
1030
+ const headers = sentPackets.filter((p) => p.value.case === 'streamHeader');
1031
+ expect(headers).toHaveLength(2);
1032
+
1033
+ const textHeader = headerOf(headers[0]);
1034
+ expect(textHeader.streamId).toStrictEqual(info.id);
1035
+ expect(textHeader.contentHeader.case).toBe('textHeader');
1036
+ expect(textHeader.inlineContent).toBeUndefined();
1037
+ const attachedStreamIds = (textHeader.contentHeader.value as DataStream_TextHeader)
1038
+ .attachedStreamIds;
1039
+ expect(attachedStreamIds).toHaveLength(1);
1040
+
1041
+ // The attachment is a byte stream whose streamId matches the reference in the text header.
1042
+ const byteHeader = headerOf(headers[1]);
1043
+ expect(byteHeader.contentHeader.case).toBe('byteHeader');
1044
+ expect(byteHeader.streamId).toStrictEqual(attachedStreamIds[0]);
1045
+ expect((byteHeader.contentHeader.value as DataStream_ByteHeader).name).toStrictEqual(
1046
+ 'att.bin',
1047
+ );
1048
+
1049
+ // Both streams are terminated by trailers.
1050
+ const trailers = sentPackets.filter((p) => p.value.case === 'streamTrailer');
1051
+ expect(trailers.map((p) => trailerOf(p).streamId).sort()).toStrictEqual(
1052
+ [info.id, attachedStreamIds[0]].sort(),
1053
+ );
1054
+ });
1055
+
1056
+ it('should forward destinationIdentities to attachment byte streams', async () => {
1057
+ // Targeted send to the v2 subset of a mixed room: the attachment streams must be targeted
1058
+ // the same way, or participants outside the destination list receive stray byte streams
1059
+ // referenced by a text stream they never got.
1060
+ const { manager, sentPackets } = createManager({
1061
+ alice: CLIENT_PROTOCOL_DEFAULT,
1062
+ bob: CLIENT_PROTOCOL_DATA_STREAM_V2,
1063
+ jim: CLIENT_PROTOCOL_DATA_STREAM_V2,
1064
+ });
1065
+ const file = new File([new Uint8Array(100).fill(0x07) as NonSharedUint8Array], 'att.bin');
1066
+
1067
+ await manager.sendText('hello world', {
1068
+ topic: 'my-topic',
1069
+ destinationIdentities: ['bob', 'jim'],
1070
+ attachments: [file],
1071
+ });
1072
+
1073
+ for (const packet of sentPackets) {
1074
+ expect(packet.destinationIdentities).toStrictEqual(['bob', 'jim']);
1075
+ }
1076
+ });
1077
+ });
1078
+
1079
+ describe('recipient eligibility edge cases', () => {
1080
+ it('should send inline + compressed on a broadcast when every participant is v2 with the capability', async () => {
1081
+ const { manager, sentPackets } = createManager({
1082
+ alice: CLIENT_PROTOCOL_DATA_STREAM_V2,
1083
+ bob: CLIENT_PROTOCOL_DATA_STREAM_V2,
1084
+ });
1085
+
1086
+ // No destinationIdentities: eligibility is evaluated over every remote participant.
1087
+ await manager.sendText('hello hello compressible world', { topic: 'my-topic' });
1088
+
1089
+ expect(sentPackets).toHaveLength(1);
1090
+ const header = headerOf(sentPackets[0]);
1091
+ expect(header.compression).toBe(DataStream_CompressionType.DEFLATE_RAW);
1092
+ expect(header.inlineContent).toBeInstanceOf(Uint8Array);
1093
+ });
1094
+
1095
+ it('should send inline but uncompressed on a broadcast when one participant lacks the compression capability', async () => {
1096
+ const { manager, sentPackets } = createManager({
1097
+ alice: CLIENT_PROTOCOL_DATA_STREAM_V2,
1098
+ noCompression: [CLIENT_PROTOCOL_DATA_STREAM_V2, []],
1099
+ });
1100
+
1101
+ await manager.sendText('hello hello compressible world', { topic: 'my-topic' });
1102
+
1103
+ expect(sentPackets).toHaveLength(1);
1104
+ const header = headerOf(sentPackets[0]);
1105
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
1106
+ expect(header.inlineContent).toStrictEqual(
1107
+ new TextEncoder().encode('hello hello compressible world'),
1108
+ );
1109
+ });
1110
+
1111
+ it('should treat an empty room as v2-eligible for a broadcast', async () => {
1112
+ const { manager, sentPackets } = createManager({});
1113
+
1114
+ await manager.sendText('hello hello compressible world', { topic: 'my-topic' });
1115
+
1116
+ // Nobody to receive it, so nothing gates the v2 fast path: single inline compressed packet.
1117
+ expect(sentPackets).toHaveLength(1);
1118
+ expect(headerOf(sentPackets[0]).compression).toBe(DataStream_CompressionType.DEFLATE_RAW);
1119
+ });
1120
+
1121
+ it('should treat an unknown destination identity as pre-v2 (legacy multi-packet)', async () => {
1122
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DATA_STREAM_V2 });
1123
+
1124
+ // "stranger" has no ParticipantInfo, so its clientProtocol resolves to 0 — the send must
1125
+ // fall back to the legacy format rather than assuming v2 support.
1126
+ await manager.sendText('hello world', {
1127
+ topic: 'my-topic',
1128
+ destinationIdentities: ['stranger'],
1129
+ });
1130
+
1131
+ expect(sentPackets).toHaveLength(3);
1132
+ const header = headerOf(sentPackets[0]);
1133
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
1134
+ expect(header.inlineContent).toBeUndefined();
1135
+ });
1136
+
1137
+ it('should send a large TEXT payload to a v2 recipient without the capability as uncompressed MTU-split chunks', async () => {
1138
+ const { manager, sentPackets } = createManager({
1139
+ noCompression: [CLIENT_PROTOCOL_DATA_STREAM_V2, []],
1140
+ });
1141
+
1142
+ const info = await manager.sendText('A'.repeat(40_000), {
1143
+ topic: 'my-topic',
1144
+ destinationIdentities: ['noCompression'],
1145
+ });
1146
+
1147
+ // Inline is attempted (recipient is v2) but the raw payload overflows the MTU, and
1148
+ // compression is gated off by the missing capability — so this falls back to an
1149
+ // uncompressed multi-packet stream split at STREAM_CHUNK_SIZE_BYTES.
1150
+ expect(sentPackets).toHaveLength(5);
1151
+ const header = headerOf(sentPackets[0]);
1152
+ expect(header.streamId).toStrictEqual(info.id);
1153
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
1154
+ expect(header.inlineContent).toBeUndefined();
1155
+ expect(chunkOf(sentPackets[1]).content).toHaveLength(15_000);
1156
+ expect(chunkOf(sentPackets[2]).content).toHaveLength(15_000);
1157
+ expect(chunkOf(sentPackets[3]).content).toHaveLength(10_000);
1158
+ expect(sentPackets[4].value.case).toBe('streamTrailer');
1159
+ });
1160
+
1161
+ it('should send a FILE uncompressed when compress: false even to capable recipients', async () => {
1162
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DATA_STREAM_V2 });
1163
+ const bytes = new Uint8Array(10_000).fill(0x07);
1164
+
1165
+ const info = await manager.sendFile(new File([bytes as NonSharedUint8Array], 'text.txt'), {
1166
+ topic: 'my-topic',
1167
+ destinationIdentities: ['alice'],
1168
+ compress: false,
1169
+ });
1170
+
1171
+ expect(sentPackets).toHaveLength(3);
1172
+ const header = headerOf(sentPackets[0]);
1173
+ expect(header.streamId).toStrictEqual(info.id);
1174
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
1175
+ const chunk = chunkOf(sentPackets[1]);
1176
+ expect(chunk.content).toHaveLength(10_000);
1177
+ expect(chunk.content.every((byte) => byte === 0x07)).toBeTruthy();
1178
+ });
1179
+ });
1180
+
1181
+ describe('UTF-8 chunk splitting', () => {
1182
+ it('should split streamText writes on UTF-8 character boundaries at the MTU', async () => {
1183
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DEFAULT });
1184
+
1185
+ // 14 999 single-byte chars put the 4-byte emoji straddling the 15 000-byte MTU boundary.
1186
+ const text = `${'a'.repeat(14_999)}😀${'b'.repeat(10)}`;
1187
+ const writer = await manager.streamText({ topic: 'my-topic' });
1188
+ await writer.write(text);
1189
+ await writer.close();
1190
+
1191
+ expect(sentPackets).toHaveLength(4); // header + 2 chunks + trailer
1192
+ const first = chunkOf(sentPackets[1]);
1193
+ const second = chunkOf(sentPackets[2]);
1194
+
1195
+ // The split backs off below the MTU rather than bisecting the emoji, so each chunk decodes
1196
+ // independently.
1197
+ expect(first.content).toHaveLength(14_999);
1198
+ const decode = (bytes: Uint8Array) => new TextDecoder('utf-8', { fatal: true }).decode(bytes);
1199
+ expect(() => decode(first.content)).not.toThrow();
1200
+ expect(() => decode(second.content)).not.toThrow();
1201
+ expect(decode(first.content) + decode(second.content)).toStrictEqual(text);
1202
+ });
1203
+ });
1204
+
1205
+ describe('empty payloads', () => {
1206
+ it('should send an empty TEXT payload to a pre-v2 room as header + trailer', async () => {
1207
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DEFAULT });
1208
+
1209
+ const info = await manager.sendText('', { topic: 'my-topic' });
1210
+
1211
+ // A well-formed (if contentless) stream: header declaring zero length, then the trailer.
1212
+ expect(sentPackets).toHaveLength(2);
1213
+ const header = headerOf(sentPackets[0]);
1214
+ expect(header.streamId).toStrictEqual(info.id);
1215
+ expect(header.totalLength).toStrictEqual(0n);
1216
+ expect(sentPackets[1].value.case).toBe('streamTrailer');
1217
+ expect(trailerOf(sentPackets[1]).streamId).toStrictEqual(info.id);
1218
+ });
1219
+
1220
+ it('should send an empty TEXT payload to a v2 room as a single raw inline packet', async () => {
1221
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DATA_STREAM_V2 });
1222
+
1223
+ await manager.sendText('', { topic: 'my-topic', destinationIdentities: ['alice'] });
1224
+
1225
+ expect(sentPackets).toHaveLength(1);
1226
+ const header = headerOf(sentPackets[0]);
1227
+ // Deflate framing can't shrink an empty payload, so the raw (empty) bytes are kept.
1228
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
1229
+ expect(header.inlineContent).toStrictEqual(new Uint8Array(0));
1230
+ expect(header.totalLength).toStrictEqual(0n);
1231
+ });
1232
+
1233
+ it('should send an empty BYTE payload to a pre-v2 room as header + trailer', async () => {
1234
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DEFAULT });
1235
+
1236
+ const info = await manager.sendBytes(new Uint8Array(0), { topic: 'my-topic' });
1237
+
1238
+ expect(sentPackets).toHaveLength(2);
1239
+ const header = headerOf(sentPackets[0]);
1240
+ expect(header.totalLength).toStrictEqual(0n);
1241
+ expect(sentPackets[1].value.case).toBe('streamTrailer');
1242
+ expect(trailerOf(sentPackets[1]).streamId).toStrictEqual(info.id);
1243
+ });
1244
+ });
1245
+
1246
+ describe('runtime without CompressionStream', () => {
1247
+ it('should send raw inline even to compression-capable recipients when the runtime cannot compress', async () => {
1248
+ const originalCompressionStream = CompressionStream;
1249
+ try {
1250
+ (globalThis as any).CompressionStream = undefined;
1251
+
1252
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DATA_STREAM_V2 });
1253
+ await manager.sendText('hello hello compressible world', {
1254
+ topic: 'my-topic',
1255
+ destinationIdentities: ['alice'],
1256
+ });
1257
+
1258
+ // Compression eligibility also requires a local compressor; without one, inline still
1259
+ // applies but the payload is sent raw.
1260
+ expect(sentPackets).toHaveLength(1);
1261
+ const header = headerOf(sentPackets[0]);
1262
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
1263
+ expect(header.inlineContent).toStrictEqual(
1264
+ new TextEncoder().encode('hello hello compressible world'),
1265
+ );
1266
+ } finally {
1267
+ (globalThis as any).CompressionStream = originalCompressionStream;
1268
+ }
1269
+ });
1270
+
1271
+ it('should send a FILE uncompressed when the runtime cannot compress', async () => {
1272
+ const originalCompressionStream = CompressionStream;
1273
+ try {
1274
+ (globalThis as any).CompressionStream = undefined;
1275
+
1276
+ const { manager, sentPackets } = createManager({ alice: CLIENT_PROTOCOL_DATA_STREAM_V2 });
1277
+ const bytes = new Uint8Array(10_000).fill(0x07);
1278
+ await manager.sendFile(new File([bytes as NonSharedUint8Array], 'text.txt'), {
1279
+ topic: 'my-topic',
1280
+ destinationIdentities: ['alice'],
1281
+ });
1282
+
1283
+ expect(sentPackets).toHaveLength(3);
1284
+ const header = headerOf(sentPackets[0]);
1285
+ expect(header.compression).toBe(DataStream_CompressionType.NONE);
1286
+ expect(chunkOf(sentPackets[1]).content).toHaveLength(10_000);
1287
+ } finally {
1288
+ (globalThis as any).CompressionStream = originalCompressionStream;
1289
+ }
1290
+ });
1291
+ });
1292
+ });