livekit-client 2.20.1 → 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.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +37 -7
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2279 -651
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +37 -7
- package/dist/livekit-client.fm.worker.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +3 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +2 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +1 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +31 -26
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +4 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/types.d.ts +12 -0
- package/dist/src/room/data-channel/types.d.ts.map +1 -0
- package/dist/src/room/data-stream/compression.d.ts +49 -0
- package/dist/src/room/data-stream/compression.d.ts.map +1 -0
- package/dist/src/room/data-stream/constants.d.ts +18 -0
- package/dist/src/room/data-stream/constants.d.ts.map +1 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +3 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +9 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/publishUtils.d.ts +7 -0
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
- package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +1 -0
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +11 -1
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +17 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +13 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/signalToken.d.ts +26 -0
- package/dist/src/test/signalToken.d.ts.map +1 -0
- package/dist/src/utils/dataPacketBuffer.d.ts +18 -0
- package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
- package/dist/src/version.d.ts +5 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/ts4.2/index.d.ts +1 -1
- package/dist/ts4.2/logger.d.ts +3 -1
- package/dist/ts4.2/room/PCTransport.d.ts +2 -0
- package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +31 -26
- package/dist/ts4.2/room/Room.d.ts +4 -0
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
- package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
- package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
- package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
- package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
- package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/ts4.2/room/errors.d.ts +3 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +9 -1
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
- package/dist/ts4.2/room/participant/publishUtils.d.ts +7 -0
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -0
- package/dist/ts4.2/room/track/options.d.ts +11 -1
- package/dist/ts4.2/room/types.d.ts +17 -0
- package/dist/ts4.2/room/utils.d.ts +13 -0
- package/dist/ts4.2/test/signalToken.d.ts +26 -0
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +18 -0
- package/dist/ts4.2/version.d.ts +5 -1
- package/package.json +8 -4
- package/src/api/SignalClient.e2e.test.ts +264 -0
- package/src/api/SignalClient.ts +34 -2
- package/src/index.ts +2 -0
- package/src/logger.ts +2 -0
- package/src/room/PCTransport.ts +66 -31
- package/src/room/PCTransportManager.ts +10 -6
- package/src/room/RTCEngine.test.ts +238 -6
- package/src/room/RTCEngine.ts +101 -338
- package/src/room/RegionUrlProvider.ts +4 -2
- package/src/room/Room.test.ts +4 -1
- package/src/room/Room.ts +47 -9
- package/src/room/data-channel/DataChannelManager.test.ts +152 -0
- package/src/room/data-channel/DataChannelManager.ts +237 -0
- package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
- package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
- package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
- package/src/room/data-channel/LossyDataChannel.ts +125 -0
- package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
- package/src/room/data-channel/ReliableDataChannel.ts +153 -0
- package/src/room/data-channel/types.ts +30 -0
- package/src/room/data-stream/compression.ts +117 -0
- package/src/room/data-stream/constants.ts +18 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
- package/src/room/data-stream/incoming/StreamReader.ts +8 -2
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
- package/src/room/data-stream/outgoing/header-utils.ts +87 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +303 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.ts +70 -8
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +2 -0
- package/src/room/errors.ts +10 -4
- package/src/room/participant/LocalParticipant.ts +29 -6
- package/src/room/participant/Participant.ts +5 -0
- package/src/room/participant/RemoteParticipant.ts +19 -5
- package/src/room/participant/publishUtils.ts +14 -11
- package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
- package/src/room/rpc/client/RpcClientManager.ts +1 -4
- package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
- package/src/room/rpc/server/RpcServerManager.ts +1 -3
- package/src/room/track/LocalAudioTrack.ts +16 -0
- package/src/room/track/Track.ts +10 -6
- package/src/room/track/options.ts +11 -1
- package/src/room/types.ts +18 -0
- package/src/room/utils.ts +67 -0
- package/src/test/signalServerSetup.ts +151 -0
- package/src/test/signalToken.ts +59 -0
- package/src/test/vitest-context.d.ts +10 -0
- package/src/utils/dataPacketBuffer.test.ts +100 -0
- package/src/utils/dataPacketBuffer.ts +46 -1
- package/src/version.ts +5 -1
package/src/room/Room.ts
CHANGED
|
@@ -118,6 +118,7 @@ import {
|
|
|
118
118
|
getEmptyAudioStreamTrack,
|
|
119
119
|
isBrowserSupported,
|
|
120
120
|
isCloud,
|
|
121
|
+
isCompressionStreamSupported,
|
|
121
122
|
isLocalAudioTrack,
|
|
122
123
|
isLocalParticipant,
|
|
123
124
|
isReactNative,
|
|
@@ -268,7 +269,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
268
269
|
this.maybeCreateEngine();
|
|
269
270
|
|
|
270
271
|
this.incomingDataStreamManager = new IncomingDataStreamManager();
|
|
271
|
-
this.outgoingDataStreamManager = new OutgoingDataStreamManager(
|
|
272
|
+
this.outgoingDataStreamManager = new OutgoingDataStreamManager(
|
|
273
|
+
this.engine,
|
|
274
|
+
this.log,
|
|
275
|
+
this.getRemoteParticipantClientProtocol,
|
|
276
|
+
this.getRemoteParticipantCapabilities,
|
|
277
|
+
this.getAllRemoteParticipantIdentities,
|
|
278
|
+
);
|
|
272
279
|
|
|
273
280
|
this.incomingDataTrackManager = new IncomingDataTrackManager({ e2eeManager: this.e2eeManager });
|
|
274
281
|
this.incomingDataTrackManager
|
|
@@ -308,7 +315,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
308
315
|
})
|
|
309
316
|
.on('packetAvailable', ({ handle, bytes }) => {
|
|
310
317
|
this.engine
|
|
311
|
-
.
|
|
318
|
+
.sendDataTrackFrame(bytes)
|
|
312
319
|
.finally(() => this.outgoingDataTrackManager.handlePacketSendComplete(handle));
|
|
313
320
|
});
|
|
314
321
|
|
|
@@ -623,10 +630,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
623
630
|
this.isResuming = false;
|
|
624
631
|
this.log.debug('Resumed signal connection');
|
|
625
632
|
this.updateSubscriptions();
|
|
626
|
-
this.emitBufferedEvents();
|
|
627
633
|
if (this.setAndEmitConnectionState(ConnectionState.Connected)) {
|
|
628
634
|
this.emit(RoomEvent.Reconnected);
|
|
629
635
|
}
|
|
636
|
+
this.emitBufferedEvents();
|
|
630
637
|
})
|
|
631
638
|
.on(EngineEvent.SignalResumed, () => {
|
|
632
639
|
this.bufferedEvents = [];
|
|
@@ -971,11 +978,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
971
978
|
autoSubscribe: connectOptions.autoSubscribe,
|
|
972
979
|
adaptiveStream:
|
|
973
980
|
typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
|
|
974
|
-
clientInfoCapabilities:
|
|
975
|
-
isFrameMetadataSupported(roomOptions.frameMetadata ?? roomOptions.packetTrailer) ||
|
|
976
|
-
!!this.e2eeManager
|
|
977
|
-
? [ClientInfo_Capability.CAP_PACKET_TRAILER]
|
|
978
|
-
: undefined,
|
|
981
|
+
clientInfoCapabilities: this.getClientInfoCapabilities(roomOptions),
|
|
979
982
|
maxRetries: connectOptions.maxRetries,
|
|
980
983
|
e2eeEnabled: !!this.e2eeManager,
|
|
981
984
|
websocketTimeout: connectOptions.websocketTimeout,
|
|
@@ -1383,7 +1386,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1383
1386
|
await Promise.all([
|
|
1384
1387
|
this.acquireAudioContext(),
|
|
1385
1388
|
...elements.map((e) => {
|
|
1386
|
-
|
|
1389
|
+
// when webAudioMix is enabled, attached elements are deliberately kept muted by
|
|
1390
|
+
// RemoteAudioTrack.attach() and audio is routed through the web audio graph instead.
|
|
1391
|
+
// unmuting them here would cause double playback on platforms where element.volume
|
|
1392
|
+
// has no effect (e.g. iOS Safari)
|
|
1393
|
+
if (!this.options.webAudioMix) {
|
|
1394
|
+
e.muted = false;
|
|
1395
|
+
}
|
|
1387
1396
|
return e.play();
|
|
1388
1397
|
}),
|
|
1389
1398
|
]);
|
|
@@ -2499,10 +2508,39 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2499
2508
|
}
|
|
2500
2509
|
}
|
|
2501
2510
|
|
|
2511
|
+
/** The client capabilities this SDK advertises to other participants in its `ClientInfo`. */
|
|
2512
|
+
private getClientInfoCapabilities(
|
|
2513
|
+
roomOptions: InternalRoomOptions,
|
|
2514
|
+
): Array<ClientInfo_Capability> {
|
|
2515
|
+
const capabilities: Array<ClientInfo_Capability> = [];
|
|
2516
|
+
if (
|
|
2517
|
+
isFrameMetadataSupported(roomOptions.frameMetadata ?? roomOptions.packetTrailer) ||
|
|
2518
|
+
!!this.e2eeManager
|
|
2519
|
+
) {
|
|
2520
|
+
capabilities.push(ClientInfo_Capability.CAP_PACKET_TRAILER);
|
|
2521
|
+
}
|
|
2522
|
+
// Advertise deflate-raw decompression support so peers know they can send us compressed data
|
|
2523
|
+
// streams (gated separately from clientProtocol — see the data streams v2 spec).
|
|
2524
|
+
if (isCompressionStreamSupported()) {
|
|
2525
|
+
capabilities.push(ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW);
|
|
2526
|
+
}
|
|
2527
|
+
return capabilities;
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2502
2530
|
private getRemoteParticipantClientProtocol = (identity: Participant['identity']) => {
|
|
2503
2531
|
return this.remoteParticipants.get(identity)?.clientProtocol ?? CLIENT_PROTOCOL_DEFAULT;
|
|
2504
2532
|
};
|
|
2505
2533
|
|
|
2534
|
+
private getRemoteParticipantCapabilities = (
|
|
2535
|
+
identity: Participant['identity'],
|
|
2536
|
+
): Array<ClientInfo_Capability> => {
|
|
2537
|
+
return this.remoteParticipants.get(identity)?.capabilities ?? [];
|
|
2538
|
+
};
|
|
2539
|
+
|
|
2540
|
+
private getAllRemoteParticipantIdentities = () => {
|
|
2541
|
+
return Array.from(this.remoteParticipants.keys());
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2506
2544
|
private registerRpcDataStreamHandler() {
|
|
2507
2545
|
this.incomingDataStreamManager.registerTextStreamHandler(
|
|
2508
2546
|
RPC_REQUEST_DATA_STREAM_TOPIC,
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { PCTransportManager } from '../PCTransportManager';
|
|
3
|
+
import { UnexpectedConnectionState } from '../errors';
|
|
4
|
+
import { DataChannelManager, type DataChannelManagerOptions } from './DataChannelManager';
|
|
5
|
+
import { DataChannelKind } from './types';
|
|
6
|
+
|
|
7
|
+
class FakeDataChannel extends EventTarget {
|
|
8
|
+
bufferedAmount = 0;
|
|
9
|
+
|
|
10
|
+
bufferedAmountLowThreshold = 0;
|
|
11
|
+
|
|
12
|
+
onmessage: ((message: MessageEvent) => void) | null = null;
|
|
13
|
+
|
|
14
|
+
onerror: ((event: Event) => void) | null = null;
|
|
15
|
+
|
|
16
|
+
onclose: (() => void) | null = null;
|
|
17
|
+
|
|
18
|
+
onbufferedamountlow: (() => void) | null = null;
|
|
19
|
+
|
|
20
|
+
send = vi.fn();
|
|
21
|
+
|
|
22
|
+
close = vi.fn();
|
|
23
|
+
|
|
24
|
+
constructor(public label: string) {
|
|
25
|
+
super();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
30
|
+
|
|
31
|
+
function makeManager(overrides?: Partial<DataChannelManagerOptions>) {
|
|
32
|
+
const opts = {
|
|
33
|
+
isEngineClosed: () => false,
|
|
34
|
+
isReconnecting: () => false,
|
|
35
|
+
onDataMessage: vi.fn(),
|
|
36
|
+
onDataTrackMessage: vi.fn(),
|
|
37
|
+
onDataError: vi.fn(),
|
|
38
|
+
onChannelClose: vi.fn(),
|
|
39
|
+
onBufferStatusChanged: vi.fn(),
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
const manager = new DataChannelManager(opts);
|
|
43
|
+
const created: Record<string, FakeDataChannel> = {};
|
|
44
|
+
const pcManager = {
|
|
45
|
+
createPublisherDataChannel: vi.fn((label: string) => {
|
|
46
|
+
const dc = new FakeDataChannel(label);
|
|
47
|
+
created[label] = dc;
|
|
48
|
+
return dc as unknown as RTCDataChannel;
|
|
49
|
+
}),
|
|
50
|
+
} as unknown as PCTransportManager;
|
|
51
|
+
return { manager, opts, pcManager, created };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('DataChannelManager', () => {
|
|
55
|
+
it('creates and wires the three publisher channels', () => {
|
|
56
|
+
const { manager, opts, pcManager, created } = makeManager();
|
|
57
|
+
|
|
58
|
+
manager.createPublisherChannels(pcManager);
|
|
59
|
+
|
|
60
|
+
expect(Object.keys(created).sort()).toEqual(['_data_track', '_lossy', '_reliable']);
|
|
61
|
+
expect(manager.getHandle(DataChannelKind.RELIABLE)).toBe(created._reliable);
|
|
62
|
+
expect(manager.hasPublisherChannels).toBe(true);
|
|
63
|
+
for (const dc of Object.values(created)) {
|
|
64
|
+
expect(dc.onmessage).toBeTruthy();
|
|
65
|
+
expect(dc.onerror).toBeTruthy();
|
|
66
|
+
expect(dc.onclose).toBeTruthy();
|
|
67
|
+
expect(dc.onbufferedamountlow).toBeTruthy();
|
|
68
|
+
expect(dc.bufferedAmountLowThreshold).toBeGreaterThan(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Close handler reports the right kind.
|
|
72
|
+
created._reliable.onclose!();
|
|
73
|
+
expect(opts.onChannelClose).toHaveBeenCalledWith(DataChannelKind.RELIABLE);
|
|
74
|
+
|
|
75
|
+
// A drain event on the data-track channel refreshes its status; since the buffer starts empty
|
|
76
|
+
// (below the low mark) and the wrapper's initial status is already "low", no change fires —
|
|
77
|
+
// fill it first so the drain is an observable not-low → low transition.
|
|
78
|
+
const dataTrackDc = created._data_track;
|
|
79
|
+
dataTrackDc.bufferedAmount = 10 * 1024 * 1024;
|
|
80
|
+
dataTrackDc.onbufferedamountlow!(); // refresh: now "not low"
|
|
81
|
+
dataTrackDc.bufferedAmount = 0;
|
|
82
|
+
dataTrackDc.onbufferedamountlow!(); // refresh: back to "low"
|
|
83
|
+
expect(opts.onBufferStatusChanged).toHaveBeenCalledWith(
|
|
84
|
+
DataChannelKind.DATA_TRACK_LOSSY,
|
|
85
|
+
false,
|
|
86
|
+
);
|
|
87
|
+
expect(opts.onBufferStatusChanged).toHaveBeenCalledWith(DataChannelKind.DATA_TRACK_LOSSY, true);
|
|
88
|
+
|
|
89
|
+
manager.lossy.stopThresholdTuning();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('recreating channels rejects waiters parked on the replaced handles', async () => {
|
|
93
|
+
const { manager, pcManager, created } = makeManager();
|
|
94
|
+
manager.createPublisherChannels(pcManager);
|
|
95
|
+
|
|
96
|
+
// Park a reliable sender on the first-generation channel.
|
|
97
|
+
created._reliable.bufferedAmount = 2 * 1024 * 1024;
|
|
98
|
+
const parked = manager.reliable.waitForHeadroomWithLock();
|
|
99
|
+
parked.catch(() => {});
|
|
100
|
+
await tick();
|
|
101
|
+
|
|
102
|
+
// Safari null-id path: channels recreated without closing the old ones.
|
|
103
|
+
manager.createPublisherChannels(pcManager);
|
|
104
|
+
|
|
105
|
+
await expect(parked).rejects.toBeInstanceOf(UnexpectedConnectionState);
|
|
106
|
+
// The gate recovers against the fresh (empty) channel.
|
|
107
|
+
await expect(manager.reliable.waitForHeadroomWithLock()).resolves.toBeUndefined();
|
|
108
|
+
|
|
109
|
+
manager.lossy.stopThresholdTuning();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('adopts subscriber channels by label and rejects unknown labels', () => {
|
|
113
|
+
const { manager, opts } = makeManager();
|
|
114
|
+
|
|
115
|
+
const sub = new FakeDataChannel('_reliable');
|
|
116
|
+
expect(manager.adoptSubscriberChannel(sub as unknown as RTCDataChannel)).toBe(true);
|
|
117
|
+
expect(manager.getHandle(DataChannelKind.RELIABLE, true)).toBe(sub);
|
|
118
|
+
expect(sub.onmessage).toBe(opts.onDataMessage);
|
|
119
|
+
|
|
120
|
+
const dataTrackSub = new FakeDataChannel('_data_track');
|
|
121
|
+
expect(manager.adoptSubscriberChannel(dataTrackSub as unknown as RTCDataChannel)).toBe(true);
|
|
122
|
+
expect(dataTrackSub.onmessage).toBe(opts.onDataTrackMessage);
|
|
123
|
+
|
|
124
|
+
expect(
|
|
125
|
+
manager.adoptSubscriberChannel(new FakeDataChannel('_other') as unknown as RTCDataChannel),
|
|
126
|
+
).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('teardown rejects parked waiters, closes all handles, and detaches', async () => {
|
|
130
|
+
const { manager, pcManager, created } = makeManager();
|
|
131
|
+
manager.createPublisherChannels(pcManager);
|
|
132
|
+
const sub = new FakeDataChannel('_lossy');
|
|
133
|
+
manager.adoptSubscriberChannel(sub as unknown as RTCDataChannel);
|
|
134
|
+
|
|
135
|
+
created._reliable.bufferedAmount = 2 * 1024 * 1024;
|
|
136
|
+
const parked = manager.reliable.waitForHeadroomWithLock();
|
|
137
|
+
parked.catch(() => {});
|
|
138
|
+
await tick();
|
|
139
|
+
|
|
140
|
+
manager.teardown();
|
|
141
|
+
|
|
142
|
+
await expect(parked).rejects.toBeInstanceOf(UnexpectedConnectionState);
|
|
143
|
+
for (const dc of [...Object.values(created), sub]) {
|
|
144
|
+
expect(dc.close).toHaveBeenCalled();
|
|
145
|
+
expect(dc.onmessage).toBeNull();
|
|
146
|
+
}
|
|
147
|
+
expect(manager.hasPublisherChannels).toBe(false);
|
|
148
|
+
expect(manager.getHandle(DataChannelKind.LOSSY, true)).toBeUndefined();
|
|
149
|
+
|
|
150
|
+
manager.lossy.stopThresholdTuning();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type { PCTransportManager } from '../PCTransportManager';
|
|
2
|
+
import { FlowControlledDataChannel } from './FlowControlledDataChannel';
|
|
3
|
+
import { LossyDataChannel } from './LossyDataChannel';
|
|
4
|
+
import { ReliableDataChannel } from './ReliableDataChannel';
|
|
5
|
+
import { DataChannelKind, dataChannelHighWaterMark, dataChannelLowWaterMark } from './types';
|
|
6
|
+
|
|
7
|
+
const lossyDataChannelLabel = '_lossy';
|
|
8
|
+
const reliableDataChannelLabel = '_reliable';
|
|
9
|
+
const dataTrackDataChannelLabel = '_data_track';
|
|
10
|
+
|
|
11
|
+
export interface DataChannelManagerOptions {
|
|
12
|
+
/** Whether the owning engine has been closed — a closed engine rejects headroom waiters. */
|
|
13
|
+
isEngineClosed: () => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Whether a reconnect attempt is underway: reliable sends defer to the resume replay and lossy
|
|
16
|
+
* sends are skipped while this is true.
|
|
17
|
+
*/
|
|
18
|
+
isReconnecting: () => boolean;
|
|
19
|
+
onDataMessage: (message: MessageEvent) => void;
|
|
20
|
+
onDataTrackMessage: (message: MessageEvent) => void;
|
|
21
|
+
onDataError: (event: Event) => void;
|
|
22
|
+
onChannelClose: (kind: DataChannelKind) => void;
|
|
23
|
+
/** A channel's buffer crossed its low-water mark (debounced). Drives DCBufferStatusChanged. */
|
|
24
|
+
onBufferStatusChanged: (kind: DataChannelKind, isLow: boolean) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Owns the engine's data channels: the three flow-controlled publisher channel wrappers (which
|
|
29
|
+
* live for the engine's lifetime and have RTCDataChannel handles attached/detached as peer
|
|
30
|
+
* connections come and go) plus the subscriber-side receive handles adopted by label.
|
|
31
|
+
*
|
|
32
|
+
* Handle turnover goes through {@link FlowControlledDataChannel.attach}/`detach`, which reject
|
|
33
|
+
* parked headroom waiters as a built-in — there is no separate invalidation step to forget.
|
|
34
|
+
*/
|
|
35
|
+
export class DataChannelManager {
|
|
36
|
+
readonly reliable: ReliableDataChannel;
|
|
37
|
+
|
|
38
|
+
readonly lossy: LossyDataChannel;
|
|
39
|
+
|
|
40
|
+
readonly dataTrack: LossyDataChannel;
|
|
41
|
+
|
|
42
|
+
private reliableSub?: RTCDataChannel;
|
|
43
|
+
|
|
44
|
+
private lossySub?: RTCDataChannel;
|
|
45
|
+
|
|
46
|
+
private dataTrackSub?: RTCDataChannel;
|
|
47
|
+
|
|
48
|
+
private opts: DataChannelManagerOptions;
|
|
49
|
+
|
|
50
|
+
constructor(opts: DataChannelManagerOptions) {
|
|
51
|
+
this.opts = opts;
|
|
52
|
+
const flowControlOptions = (kind: DataChannelKind) => ({
|
|
53
|
+
kind,
|
|
54
|
+
lowWaterMark: dataChannelLowWaterMark(kind),
|
|
55
|
+
highWaterMark: dataChannelHighWaterMark(kind),
|
|
56
|
+
isEngineClosed: opts.isEngineClosed,
|
|
57
|
+
onBufferStatusChanged: (isLow: boolean) => opts.onBufferStatusChanged(kind, isLow),
|
|
58
|
+
});
|
|
59
|
+
this.reliable = new ReliableDataChannel({
|
|
60
|
+
...flowControlOptions(DataChannelKind.RELIABLE),
|
|
61
|
+
isDeferringSends: opts.isReconnecting,
|
|
62
|
+
});
|
|
63
|
+
this.lossy = new LossyDataChannel({
|
|
64
|
+
...flowControlOptions(DataChannelKind.LOSSY),
|
|
65
|
+
// Classic lossy user data: a stale packet is worthless, so drop instead of queueing.
|
|
66
|
+
bufferFullBehavior: 'drop',
|
|
67
|
+
shouldSkipSends: opts.isReconnecting,
|
|
68
|
+
});
|
|
69
|
+
this.dataTrack = new LossyDataChannel({
|
|
70
|
+
...flowControlOptions(DataChannelKind.DATA_TRACK_LOSSY),
|
|
71
|
+
// Data tracks backpressure the producer instead — it decides what to skip at frame
|
|
72
|
+
// granularity rather than the engine dropping arbitrary chunks out of frames.
|
|
73
|
+
bufferFullBehavior: 'wait',
|
|
74
|
+
shouldSkipSends: opts.isReconnecting,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The flow-control wrapper for `kind`. */
|
|
79
|
+
channelFor(kind: DataChannelKind): FlowControlledDataChannel {
|
|
80
|
+
switch (kind) {
|
|
81
|
+
case DataChannelKind.RELIABLE:
|
|
82
|
+
return this.reliable;
|
|
83
|
+
case DataChannelKind.LOSSY:
|
|
84
|
+
return this.lossy;
|
|
85
|
+
case DataChannelKind.DATA_TRACK_LOSSY:
|
|
86
|
+
return this.dataTrack;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The raw RTCDataChannel handle for `kind`, publisher side by default. */
|
|
91
|
+
getHandle(kind: DataChannelKind, subscriber: boolean = false): RTCDataChannel | undefined {
|
|
92
|
+
if (!subscriber) {
|
|
93
|
+
return this.channelFor(kind).channelHandle;
|
|
94
|
+
}
|
|
95
|
+
switch (kind) {
|
|
96
|
+
case DataChannelKind.RELIABLE:
|
|
97
|
+
return this.reliableSub;
|
|
98
|
+
case DataChannelKind.LOSSY:
|
|
99
|
+
return this.lossySub;
|
|
100
|
+
case DataChannelKind.DATA_TRACK_LOSSY:
|
|
101
|
+
return this.dataTrackSub;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get hasPublisherChannels(): boolean {
|
|
106
|
+
return Boolean(
|
|
107
|
+
this.reliable.channelHandle || this.lossy.channelHandle || this.dataTrack.channelHandle,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Creates the three publisher data channels on the given transport, wires their handlers, and
|
|
113
|
+
* attaches them to the wrappers — attaching rejects any waiters still parked on replaced
|
|
114
|
+
* channel objects.
|
|
115
|
+
*/
|
|
116
|
+
createPublisherChannels(pcManager: PCTransportManager) {
|
|
117
|
+
// clear old data channel callbacks if recreate
|
|
118
|
+
for (const channel of [this.lossy, this.reliable, this.dataTrack]) {
|
|
119
|
+
const old = channel.channelHandle;
|
|
120
|
+
if (old) {
|
|
121
|
+
old.onmessage = null;
|
|
122
|
+
old.onerror = null;
|
|
123
|
+
old.onclose = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const wire = (
|
|
128
|
+
channel: FlowControlledDataChannel,
|
|
129
|
+
dc: RTCDataChannel,
|
|
130
|
+
onMessage: (message: MessageEvent) => void,
|
|
131
|
+
) => {
|
|
132
|
+
// also handle messages over the pub channel, for backwards compatibility
|
|
133
|
+
dc.onmessage = onMessage;
|
|
134
|
+
// handle datachannel errors
|
|
135
|
+
dc.onerror = this.opts.onDataError;
|
|
136
|
+
// detect unexpected publisher data channel closes
|
|
137
|
+
dc.onclose = () => this.opts.onChannelClose(channel.kind);
|
|
138
|
+
// set up dc buffer threshold - if this is not set, it will default to 0
|
|
139
|
+
dc.bufferedAmountLowThreshold = channel.lowWaterMark;
|
|
140
|
+
// handle buffer amount low events
|
|
141
|
+
dc.onbufferedamountlow = () => channel.refreshBufferStatus();
|
|
142
|
+
channel.attach(dc);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
wire(
|
|
146
|
+
this.lossy,
|
|
147
|
+
pcManager.createPublisherDataChannel(lossyDataChannelLabel, {
|
|
148
|
+
ordered: false,
|
|
149
|
+
maxRetransmits: 0,
|
|
150
|
+
}),
|
|
151
|
+
this.opts.onDataMessage,
|
|
152
|
+
);
|
|
153
|
+
wire(
|
|
154
|
+
this.reliable,
|
|
155
|
+
pcManager.createPublisherDataChannel(reliableDataChannelLabel, {
|
|
156
|
+
ordered: true,
|
|
157
|
+
}),
|
|
158
|
+
this.opts.onDataMessage,
|
|
159
|
+
);
|
|
160
|
+
wire(
|
|
161
|
+
this.dataTrack,
|
|
162
|
+
pcManager.createPublisherDataChannel(dataTrackDataChannelLabel, {
|
|
163
|
+
ordered: false,
|
|
164
|
+
maxRetransmits: 0,
|
|
165
|
+
}),
|
|
166
|
+
this.opts.onDataTrackMessage,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
this.lossy.startThresholdTuning();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Adopts a subscriber-side data channel by label, wiring the matching receive handler.
|
|
174
|
+
* Returns false for labels this manager doesn't own.
|
|
175
|
+
*/
|
|
176
|
+
adoptSubscriberChannel(channel: RTCDataChannel): boolean {
|
|
177
|
+
let handler: (message: MessageEvent) => void;
|
|
178
|
+
if (channel.label === reliableDataChannelLabel) {
|
|
179
|
+
this.reliableSub = channel;
|
|
180
|
+
handler = this.opts.onDataMessage;
|
|
181
|
+
} else if (channel.label === lossyDataChannelLabel) {
|
|
182
|
+
this.lossySub = channel;
|
|
183
|
+
handler = this.opts.onDataMessage;
|
|
184
|
+
} else if (channel.label === dataTrackDataChannelLabel) {
|
|
185
|
+
this.dataTrackSub = channel;
|
|
186
|
+
handler = this.opts.onDataTrackMessage;
|
|
187
|
+
} else {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
channel.onmessage = handler;
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Tears down all channels for a peer-connection cleanup: rejects parked waiters (detach — the
|
|
196
|
+
* spec allows `pc.close()` to transition channels to 'closed' without firing events, so waiting
|
|
197
|
+
* for browser close events is not an option), strips handlers, closes the handles, and resets
|
|
198
|
+
* the reliable session state.
|
|
199
|
+
*/
|
|
200
|
+
teardown() {
|
|
201
|
+
const dcCleanup = (dc: RTCDataChannel | undefined) => {
|
|
202
|
+
if (!dc) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Detach the data channel handlers before closing anything. Closing a peer connection tears
|
|
207
|
+
// down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
|
|
208
|
+
// channels; if our handlers are still attached at that point, handleDataError logs a spurious
|
|
209
|
+
// "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
|
|
210
|
+
// before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
|
|
211
|
+
// browser dispatches those teardown events. See livekit/client-sdk-js#1953.
|
|
212
|
+
dc.onbufferedamountlow = null;
|
|
213
|
+
dc.onclose = null;
|
|
214
|
+
dc.onclosing = null;
|
|
215
|
+
dc.onerror = null;
|
|
216
|
+
dc.onmessage = null;
|
|
217
|
+
dc.onopen = null;
|
|
218
|
+
|
|
219
|
+
dc.close();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
for (const channel of [this.lossy, this.reliable, this.dataTrack]) {
|
|
223
|
+
const dc = channel.channelHandle;
|
|
224
|
+
channel.detach('peer connections cleaned up');
|
|
225
|
+
dcCleanup(dc);
|
|
226
|
+
}
|
|
227
|
+
dcCleanup(this.lossySub);
|
|
228
|
+
dcCleanup(this.reliableSub);
|
|
229
|
+
dcCleanup(this.dataTrackSub);
|
|
230
|
+
this.lossySub = undefined;
|
|
231
|
+
this.reliableSub = undefined;
|
|
232
|
+
this.dataTrackSub = undefined;
|
|
233
|
+
|
|
234
|
+
// Full teardown starts the session over: drop replay state and restart sequencing.
|
|
235
|
+
this.reliable.reset();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { UnexpectedConnectionState } from '../errors';
|
|
3
|
+
import { FlowControlledDataChannel } from './FlowControlledDataChannel';
|
|
4
|
+
import { DataChannelKind } from './types';
|
|
5
|
+
|
|
6
|
+
class FakeDataChannel extends EventTarget {
|
|
7
|
+
bufferedAmount = 0;
|
|
8
|
+
|
|
9
|
+
bufferedAmountLowThreshold = 64;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
13
|
+
|
|
14
|
+
// Pass `dc: null` for a handle-less channel; omit it to get a fresh one. `null` (not `undefined`)
|
|
15
|
+
// is deliberate — a destructuring default fills in on `undefined`, so only `null` passes through.
|
|
16
|
+
function makeChannel({
|
|
17
|
+
dc = new FakeDataChannel(),
|
|
18
|
+
engineClosed = false,
|
|
19
|
+
}: { dc?: FakeDataChannel | null; engineClosed?: boolean } = {}) {
|
|
20
|
+
const state = { engineClosed };
|
|
21
|
+
const onBufferStatusChanged = vi.fn();
|
|
22
|
+
const channel = new FlowControlledDataChannel({
|
|
23
|
+
kind: DataChannelKind.RELIABLE,
|
|
24
|
+
lowWaterMark: 64,
|
|
25
|
+
highWaterMark: 1024,
|
|
26
|
+
isEngineClosed: () => state.engineClosed,
|
|
27
|
+
onBufferStatusChanged,
|
|
28
|
+
});
|
|
29
|
+
if (dc) {
|
|
30
|
+
channel.attach(dc as unknown as RTCDataChannel);
|
|
31
|
+
}
|
|
32
|
+
return { channel, dc: dc as FakeDataChannel, state, onBufferStatusChanged };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('FlowControlledDataChannel', () => {
|
|
36
|
+
describe('refreshBufferStatus', () => {
|
|
37
|
+
it('notifies only on a change and in both directions', () => {
|
|
38
|
+
const { channel, dc, onBufferStatusChanged } = makeChannel();
|
|
39
|
+
|
|
40
|
+
// Starts "low" (empty buffer); a refresh that stays low emits nothing.
|
|
41
|
+
channel.refreshBufferStatus();
|
|
42
|
+
expect(onBufferStatusChanged).not.toHaveBeenCalled();
|
|
43
|
+
|
|
44
|
+
// Cross above the low mark → one not-low notification.
|
|
45
|
+
dc.bufferedAmount = 512;
|
|
46
|
+
channel.refreshBufferStatus();
|
|
47
|
+
channel.refreshBufferStatus(); // debounced: no second fire
|
|
48
|
+
expect(onBufferStatusChanged.mock.calls).toEqual([[false]]);
|
|
49
|
+
|
|
50
|
+
// Drain back below → one low notification.
|
|
51
|
+
dc.bufferedAmount = 0;
|
|
52
|
+
channel.refreshBufferStatus();
|
|
53
|
+
expect(onBufferStatusChanged.mock.calls).toEqual([[false], [true]]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('is a no-op without a handle (no throw, no notification)', () => {
|
|
57
|
+
const { channel, onBufferStatusChanged } = makeChannel({ dc: null });
|
|
58
|
+
expect(() => channel.refreshBufferStatus()).not.toThrow();
|
|
59
|
+
expect(onBufferStatusChanged).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('reports watermark status against the given channel', () => {
|
|
64
|
+
const { channel, dc } = makeChannel();
|
|
65
|
+
const handle = dc as unknown as RTCDataChannel;
|
|
66
|
+
dc.bufferedAmount = 0;
|
|
67
|
+
expect(channel.isBelowHighWaterMark(handle)).toBe(true);
|
|
68
|
+
expect(channel.isBelowLowWaterMark(handle)).toBe(true);
|
|
69
|
+
|
|
70
|
+
dc.bufferedAmount = 512; // between low (64) and high (1024)
|
|
71
|
+
expect(channel.isBelowHighWaterMark(handle)).toBe(true);
|
|
72
|
+
expect(channel.isBelowLowWaterMark(handle)).toBe(false);
|
|
73
|
+
|
|
74
|
+
dc.bufferedAmount = 2048;
|
|
75
|
+
expect(channel.isBelowHighWaterMark(handle)).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('waiting for headroom without a handle rejects with a connection error', async () => {
|
|
79
|
+
const { channel } = makeChannel({ dc: null });
|
|
80
|
+
await expect(channel.waitForHeadroomWithLock()).rejects.toBeInstanceOf(
|
|
81
|
+
UnexpectedConnectionState,
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('resolves immediately while below the high-water mark', async () => {
|
|
86
|
+
const { channel, dc } = makeChannel();
|
|
87
|
+
dc.bufferedAmount = 1024;
|
|
88
|
+
await expect(channel.waitForHeadroomWithLock()).resolves.toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('parks above the high-water mark and resumes on bufferedamountlow', async () => {
|
|
92
|
+
const { channel, dc } = makeChannel();
|
|
93
|
+
dc.bufferedAmount = 2048;
|
|
94
|
+
const resolved = vi.fn();
|
|
95
|
+
const wait = channel.waitForHeadroomWithLock().then(resolved);
|
|
96
|
+
await tick();
|
|
97
|
+
expect(resolved).not.toHaveBeenCalled();
|
|
98
|
+
|
|
99
|
+
dc.bufferedAmount = 32;
|
|
100
|
+
dc.dispatchEvent(new Event('bufferedamountlow'));
|
|
101
|
+
await wait;
|
|
102
|
+
expect(resolved).toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('rejects a parked waiter when the channel closes', async () => {
|
|
106
|
+
const { channel, dc } = makeChannel();
|
|
107
|
+
dc.bufferedAmount = 2048;
|
|
108
|
+
const wait = channel.waitForHeadroomWithLock();
|
|
109
|
+
wait.catch(() => {});
|
|
110
|
+
await tick();
|
|
111
|
+
|
|
112
|
+
dc.dispatchEvent(new Event('close'));
|
|
113
|
+
await expect(wait).rejects.toBeInstanceOf(UnexpectedConnectionState);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('rejects a parked waiter on invalidateWaiters and recovers with a fresh controller', async () => {
|
|
117
|
+
const { channel, dc } = makeChannel();
|
|
118
|
+
dc.bufferedAmount = 2048;
|
|
119
|
+
const wait = channel.waitForHeadroomWithLock();
|
|
120
|
+
wait.catch(() => {});
|
|
121
|
+
await tick();
|
|
122
|
+
|
|
123
|
+
channel.invalidateWaiters('channel replaced');
|
|
124
|
+
await expect(wait).rejects.toBeInstanceOf(UnexpectedConnectionState);
|
|
125
|
+
|
|
126
|
+
// Fresh controller: the gate is usable again and the lock was released.
|
|
127
|
+
dc.bufferedAmount = 0;
|
|
128
|
+
await expect(channel.waitForHeadroomWithLock()).resolves.toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('rejects immediately when the engine is closed', async () => {
|
|
132
|
+
const { channel, state } = makeChannel();
|
|
133
|
+
state.engineClosed = true;
|
|
134
|
+
await expect(channel.waitForHeadroomWithLock()).rejects.toBeInstanceOf(
|
|
135
|
+
UnexpectedConnectionState,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('serializes waiters FIFO through the headroom lock', async () => {
|
|
140
|
+
const { channel, dc } = makeChannel();
|
|
141
|
+
dc.bufferedAmount = 2048;
|
|
142
|
+
const order: number[] = [];
|
|
143
|
+
const first = channel.waitForHeadroomWithLock().then(() => order.push(1));
|
|
144
|
+
const second = channel.waitForHeadroomWithLock().then(() => order.push(2));
|
|
145
|
+
await tick();
|
|
146
|
+
|
|
147
|
+
// One drain event wakes the head waiter; the second re-checks under the lock and, with the
|
|
148
|
+
// buffer now low, proceeds right after — strictly in arrival order.
|
|
149
|
+
dc.bufferedAmount = 0;
|
|
150
|
+
dc.dispatchEvent(new Event('bufferedamountlow'));
|
|
151
|
+
await Promise.all([first, second]);
|
|
152
|
+
expect(order).toEqual([1, 2]);
|
|
153
|
+
});
|
|
154
|
+
});
|