livekit-client 2.20.2 → 2.22.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/README.md +10 -10
- 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 +6 -1
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2588 -1056
- 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 +6 -1
- 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/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +49 -2
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +48 -65
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +6 -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/errors.d.ts +3 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +10 -2
- package/dist/src/room/participant/LocalParticipant.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/rpc/client/RpcClientManager.d.ts.map +1 -1
- package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
- package/dist/src/room/token-source/TokenSource.d.ts +19 -10
- package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrack.d.ts +3 -0
- package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.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 +28 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/promiseState.d.ts +12 -0
- package/dist/src/test/promiseState.d.ts.map +1 -0
- package/dist/src/test/signalToken.d.ts.map +1 -1
- package/dist/src/utils/AsyncQueue.d.ts +3 -3
- package/dist/src/utils/AsyncQueue.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/room/PCTransport.d.ts +49 -2
- package/dist/ts4.2/room/RTCEngine.d.ts +48 -65
- package/dist/ts4.2/room/Room.d.ts +6 -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/errors.d.ts +3 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +10 -2
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
- package/dist/ts4.2/room/types.d.ts +17 -0
- package/dist/ts4.2/room/utils.d.ts +28 -0
- package/dist/ts4.2/test/promiseState.d.ts +12 -0
- package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
- package/dist/ts4.2/version.d.ts +5 -1
- package/package.json +14 -13
- package/src/api/SignalClient.e2e.test.ts +16 -9
- package/src/e2ee/utils.ts +1 -1
- package/src/index.ts +2 -0
- package/src/room/PCTransport.test.ts +243 -1
- package/src/room/PCTransport.ts +181 -80
- package/src/room/RTCEngine.test.ts +397 -101
- package/src/room/RTCEngine.ts +244 -545
- package/src/room/Room.test.ts +138 -4
- package/src/room/Room.ts +92 -25
- 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 +9 -3
- 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/errors.ts +7 -2
- package/src/room/participant/LocalParticipant.ts +13 -0
- package/src/room/participant/RemoteParticipant.ts +19 -5
- 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/token-source/TokenSource.ts +25 -12
- package/src/room/track/LocalVideoTrack.test.ts +105 -2
- package/src/room/track/LocalVideoTrack.ts +36 -10
- package/src/room/track/RemoteTrack.test.ts +144 -0
- package/src/room/track/RemoteTrack.ts +39 -12
- package/src/room/track/Track.ts +2 -1
- package/src/room/types.ts +18 -0
- package/src/room/utils.test.ts +71 -2
- package/src/room/utils.ts +119 -0
- package/src/test/promiseState.ts +23 -0
- package/src/test/signalServerSetup.ts +2 -1
- package/src/test/signalToken.ts +17 -13
- package/src/type-polyfills/header-extensions.d.ts +13 -0
- package/src/utils/AsyncQueue.ts +3 -3
- package/src/version.ts +5 -1
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
DataPacket,
|
|
3
|
+
DataPacket_Kind,
|
|
4
|
+
ConnectionQuality as ProtoConnectionQuality,
|
|
5
|
+
UserPacket,
|
|
6
|
+
} from '@livekit/protocol';
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import type { DataPacketBuffer } from '../utils/dataPacketBuffer';
|
|
9
|
+
import { PCTransportState } from './PCTransportManager';
|
|
4
10
|
import RTCEngine, { DataChannelKind } from './RTCEngine';
|
|
5
11
|
import { roomOptionDefaults } from './defaults';
|
|
6
12
|
import { PublishDataError, UnexpectedConnectionState } from './errors';
|
|
@@ -228,17 +234,16 @@ describe('RTCEngine', () => {
|
|
|
228
234
|
engine: RTCEngine,
|
|
229
235
|
maxDataPacketSize: number = MAX_DATA_PACKET_SIZE,
|
|
230
236
|
) {
|
|
231
|
-
const
|
|
237
|
+
const dc = new FakeDataChannel();
|
|
232
238
|
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
239
|
+
_isClosed: false,
|
|
233
240
|
ensurePublisherConnected: vi.fn().mockResolvedValue(undefined),
|
|
234
|
-
waitForBufferHeadroomWithLock: vi.fn().mockResolvedValue(undefined),
|
|
235
|
-
updateAndEmitDCBufferStatus: vi.fn(),
|
|
236
|
-
dataChannelForKind: vi.fn(() => ({ send })),
|
|
237
241
|
pcManager: {
|
|
238
242
|
getMaxPublisherMessageSize: vi.fn(() => maxDataPacketSize),
|
|
239
243
|
},
|
|
240
244
|
});
|
|
241
|
-
|
|
245
|
+
attachFakeChannel(engine, 'reliableChannel', dc);
|
|
246
|
+
return dc.send;
|
|
242
247
|
}
|
|
243
248
|
|
|
244
249
|
it('rejects packets larger than the max data packet size', async () => {
|
|
@@ -309,6 +314,23 @@ describe('RTCEngine', () => {
|
|
|
309
314
|
|
|
310
315
|
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
311
316
|
|
|
317
|
+
/** The reliable channel's private replay buffer — reached through casts, as tests do for engine privates. */
|
|
318
|
+
const reliableBuffer = (engine: RTCEngine) =>
|
|
319
|
+
(engine as unknown as { reliableChannel: { messageBuffer: DataPacketBuffer } }).reliableChannel
|
|
320
|
+
.messageBuffer;
|
|
321
|
+
|
|
322
|
+
type ChannelField = 'reliableChannel' | 'lossyChannel' | 'dataTrackChannel';
|
|
323
|
+
const engineChannel = (engine: RTCEngine, field: ChannelField) =>
|
|
324
|
+
(
|
|
325
|
+
engine as unknown as Record<
|
|
326
|
+
ChannelField,
|
|
327
|
+
{ attach(dc: RTCDataChannel): void; invalidateWaiters(reason: string): void }
|
|
328
|
+
>
|
|
329
|
+
)[field];
|
|
330
|
+
/** Attach a fake handle to one of the engine's flow-control wrappers. */
|
|
331
|
+
const attachFakeChannel = (engine: RTCEngine, field: ChannelField, dc: FakeDataChannel) =>
|
|
332
|
+
engineChannel(engine, field).attach(dc as unknown as RTCDataChannel);
|
|
333
|
+
|
|
312
334
|
describe('resendReliableMessagesForResume', () => {
|
|
313
335
|
it('does not let a concurrent reliable send interleave into the resume replay', async () => {
|
|
314
336
|
const engine = new RTCEngine(roomOptionDefaults);
|
|
@@ -316,18 +338,17 @@ describe('RTCEngine', () => {
|
|
|
316
338
|
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
317
339
|
_isClosed: false,
|
|
318
340
|
ensurePublisherConnected: vi.fn().mockResolvedValue(undefined),
|
|
319
|
-
dataChannelForKind: vi.fn(() => dc),
|
|
320
341
|
pcManager: {
|
|
321
342
|
getMaxPublisherMessageSize: vi.fn(() => 64 * 1024 - 1),
|
|
322
343
|
},
|
|
323
344
|
});
|
|
345
|
+
attachFakeChannel(engine, 'reliableChannel', dc);
|
|
324
346
|
|
|
325
|
-
// Two messages queued for replay, and a full buffer so the
|
|
347
|
+
// Two messages queued for replay, and a full buffer so the replay parks on
|
|
348
|
+
// waitForBufferHeadroom before its first send.
|
|
326
349
|
const replayed1 = new Uint8Array([1]);
|
|
327
350
|
const replayed2 = new Uint8Array([2]);
|
|
328
|
-
const buffer = (
|
|
329
|
-
engine as unknown as { reliableMessageBuffer: { push: (item: DataPacketItem) => void } }
|
|
330
|
-
).reliableMessageBuffer;
|
|
351
|
+
const buffer = reliableBuffer(engine);
|
|
331
352
|
buffer.push({ data: replayed1, sequence: 1, sent: true });
|
|
332
353
|
buffer.push({ data: replayed2, sequence: 2, sent: true });
|
|
333
354
|
dc.bufferedAmount = 2 * 1024 * 1024; // above the reliable high-water mark
|
|
@@ -359,59 +380,6 @@ describe('RTCEngine', () => {
|
|
|
359
380
|
expect(dc.send.mock.calls[2][0]).not.toBe(replayed1);
|
|
360
381
|
expect(dc.send.mock.calls[2][0]).not.toBe(replayed2);
|
|
361
382
|
});
|
|
362
|
-
|
|
363
|
-
it('transmits a packet deferred mid-replay instead of marking it sent without sending', async () => {
|
|
364
|
-
const engine = new RTCEngine(roomOptionDefaults);
|
|
365
|
-
const dc = new FakeDataChannel();
|
|
366
|
-
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
367
|
-
_isClosed: false,
|
|
368
|
-
// Reconnect still active: a send arriving during replay takes the deferral path.
|
|
369
|
-
attemptingReconnect: true,
|
|
370
|
-
ensurePublisherConnected: vi.fn().mockResolvedValue(undefined),
|
|
371
|
-
dataChannelForKind: vi.fn(() => dc),
|
|
372
|
-
pcManager: {
|
|
373
|
-
getMaxPublisherMessageSize: vi.fn(() => 64 * 1024 - 1),
|
|
374
|
-
},
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
const buffer = (engine as unknown as { reliableMessageBuffer: DataPacketBuffer })
|
|
378
|
-
.reliableMessageBuffer;
|
|
379
|
-
const replayed = new Uint8Array([1]);
|
|
380
|
-
buffer.push({ data: replayed, sequence: 1, sent: true });
|
|
381
|
-
// Full buffer so replay parks on waitForBufferHeadroomWithLock before its first send.
|
|
382
|
-
dc.bufferedAmount = 2 * 1024 * 1024;
|
|
383
|
-
|
|
384
|
-
const replay = (
|
|
385
|
-
engine as unknown as { resendReliableMessagesForResume: (seq: number) => Promise<void> }
|
|
386
|
-
).resendReliableMessagesForResume(0);
|
|
387
|
-
await tick();
|
|
388
|
-
|
|
389
|
-
// A reliable send arrives while replay is parked; attemptingReconnect defers it into the
|
|
390
|
-
// buffer as sent:false, after the replay's first drain pass already started.
|
|
391
|
-
const deferred = new Uint8Array([2]);
|
|
392
|
-
const deferredSend = engine.sendDataPacket(
|
|
393
|
-
new DataPacket({
|
|
394
|
-
kind: DataPacket_Kind.RELIABLE,
|
|
395
|
-
value: { case: 'user', value: new UserPacket({ payload: deferred }) },
|
|
396
|
-
}),
|
|
397
|
-
DataChannelKind.RELIABLE,
|
|
398
|
-
);
|
|
399
|
-
await tick();
|
|
400
|
-
|
|
401
|
-
dc.bufferedAmount = 0;
|
|
402
|
-
dc.dispatchEvent(new Event('bufferedamountlow'));
|
|
403
|
-
await Promise.all([replay, deferredSend]);
|
|
404
|
-
|
|
405
|
-
// Pre-fix, replay sends only its snapshot ([replayed]) and blanket-marks the deferred packet
|
|
406
|
-
// sent without ever transmitting it — one send. The drain loop must transmit both (the
|
|
407
|
-
// deferred packet is serialized through sendDataPacket, so it's distinct from `replayed`),
|
|
408
|
-
// leaving nothing unsent for a later align to strand.
|
|
409
|
-
const sent = dc.send.mock.calls.map(([d]) => d);
|
|
410
|
-
expect(sent).toHaveLength(2);
|
|
411
|
-
expect(sent[0]).toBe(replayed);
|
|
412
|
-
expect(sent[1]).not.toBe(replayed);
|
|
413
|
-
expect(buffer.getUnsent()).toHaveLength(0);
|
|
414
|
-
});
|
|
415
383
|
});
|
|
416
384
|
|
|
417
385
|
describe('reliable sends during teardown windows', () => {
|
|
@@ -425,13 +393,12 @@ describe('RTCEngine', () => {
|
|
|
425
393
|
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
426
394
|
_isClosed: false,
|
|
427
395
|
ensurePublisherConnected: vi.fn().mockResolvedValue(undefined),
|
|
428
|
-
dataChannelForKind: vi.fn(() => dc),
|
|
429
396
|
pcManager: {
|
|
430
397
|
getMaxPublisherMessageSize: vi.fn(() => 64 * 1024 - 1),
|
|
431
398
|
},
|
|
432
399
|
});
|
|
433
|
-
|
|
434
|
-
|
|
400
|
+
attachFakeChannel(engine, 'reliableChannel', dc);
|
|
401
|
+
return reliableBuffer(engine);
|
|
435
402
|
}
|
|
436
403
|
|
|
437
404
|
it('resolves and queues the packet for replay when the wait is torn down transiently', async () => {
|
|
@@ -443,9 +410,7 @@ describe('RTCEngine', () => {
|
|
|
443
410
|
dc.bufferedAmount = 2 * 1024 * 1024;
|
|
444
411
|
const send = engine.sendDataPacket(makePacket(1), DataChannelKind.RELIABLE);
|
|
445
412
|
await tick();
|
|
446
|
-
(
|
|
447
|
-
engine as unknown as { invalidateDataChannelWaiters: (reason: string) => void }
|
|
448
|
-
).invalidateDataChannelWaiters('data channels recreated');
|
|
413
|
+
engineChannel(engine, 'reliableChannel').invalidateWaiters('data channels recreated');
|
|
449
414
|
|
|
450
415
|
// The send must not surface the teardown — the packet is queued for the resume replay.
|
|
451
416
|
await expect(send).resolves.toBeUndefined();
|
|
@@ -471,9 +436,7 @@ describe('RTCEngine', () => {
|
|
|
471
436
|
const send = engine.sendDataPacket(makePacket(1), DataChannelKind.RELIABLE);
|
|
472
437
|
await tick();
|
|
473
438
|
Object.assign(engine as unknown as Record<string, unknown>, { _isClosed: true });
|
|
474
|
-
(
|
|
475
|
-
engine as unknown as { invalidateDataChannelWaiters: (reason: string) => void }
|
|
476
|
-
).invalidateDataChannelWaiters('engine closed');
|
|
439
|
+
engineChannel(engine, 'reliableChannel').invalidateWaiters('engine closed');
|
|
477
440
|
|
|
478
441
|
await expect(send).rejects.toBeInstanceOf(UnexpectedConnectionState);
|
|
479
442
|
expect(dc.send).not.toHaveBeenCalled();
|
|
@@ -496,78 +459,80 @@ describe('RTCEngine', () => {
|
|
|
496
459
|
});
|
|
497
460
|
});
|
|
498
461
|
|
|
499
|
-
describe('
|
|
462
|
+
describe('sendDataTrackFrame', () => {
|
|
500
463
|
it('ensures the publisher is connected before sending (direct data-track path)', async () => {
|
|
501
464
|
const engine = new RTCEngine(roomOptionDefaults);
|
|
502
465
|
const dc = new FakeDataChannel();
|
|
503
466
|
// The channel only becomes available once the publisher connection has been established —
|
|
504
467
|
// mirroring the lazily negotiated publisher case that Room's packetAvailable path hits.
|
|
505
|
-
let connected = false;
|
|
506
468
|
const ensurePublisherConnected = vi.fn(async () => {
|
|
507
|
-
|
|
469
|
+
attachFakeChannel(engine, 'dataTrackChannel', dc);
|
|
508
470
|
});
|
|
509
471
|
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
510
472
|
_isClosed: false,
|
|
511
473
|
ensurePublisherConnected,
|
|
512
|
-
dataChannelForKind: vi.fn(() => (connected ? dc : undefined)),
|
|
513
474
|
});
|
|
514
475
|
|
|
515
|
-
await engine.
|
|
476
|
+
await engine.sendDataTrackFrame(new Uint8Array([1]));
|
|
516
477
|
|
|
517
478
|
expect(ensurePublisherConnected).toHaveBeenCalledWith(DataChannelKind.DATA_TRACK_LOSSY);
|
|
518
479
|
expect(dc.send).toHaveBeenCalledTimes(1);
|
|
519
480
|
});
|
|
520
481
|
|
|
521
|
-
it('
|
|
482
|
+
it('keeps each channel’s byterate stat isolated from the other’s traffic', async () => {
|
|
522
483
|
const engine = new RTCEngine(roomOptionDefaults);
|
|
523
484
|
const dc = new FakeDataChannel();
|
|
524
485
|
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
525
486
|
_isClosed: false,
|
|
526
487
|
ensurePublisherConnected: vi.fn().mockResolvedValue(undefined),
|
|
527
|
-
dataChannelForKind: vi.fn(() => dc),
|
|
528
488
|
});
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
489
|
+
attachFakeChannel(engine, 'lossyChannel', dc);
|
|
490
|
+
attachFakeChannel(engine, 'dataTrackChannel', dc);
|
|
491
|
+
const lossyStat = () =>
|
|
492
|
+
(engine as unknown as { lossyChannel: { statCurrentBytes: number } }).lossyChannel
|
|
493
|
+
.statCurrentBytes;
|
|
494
|
+
|
|
495
|
+
// Data-track traffic (sendDataTrackFrame → data-track channel) must not move the LOSSY channel's
|
|
496
|
+
// stat — it would inflate the lossy channel's dynamically tuned drop threshold with traffic
|
|
497
|
+
// that channel never carries.
|
|
498
|
+
await engine.sendDataTrackFrame(new Uint8Array(1000));
|
|
499
|
+
expect(lossyStat()).toBe(0);
|
|
500
|
+
|
|
501
|
+
// A plain lossy publishData packet goes through sendDataPacket → lossy channel.
|
|
502
|
+
const lossyPacket = new DataPacket({
|
|
503
|
+
kind: DataPacket_Kind.LOSSY,
|
|
504
|
+
value: { case: 'user', value: new UserPacket({ payload: new Uint8Array(100) }) },
|
|
505
|
+
});
|
|
506
|
+
await engine.sendDataPacket(lossyPacket, DataChannelKind.LOSSY);
|
|
507
|
+
expect(lossyStat()).toBeGreaterThan(0);
|
|
539
508
|
});
|
|
540
509
|
});
|
|
541
510
|
|
|
542
|
-
describe('
|
|
511
|
+
describe('waitForBufferHeadroom', () => {
|
|
543
512
|
it('rejects parked waiters and releases the lock when the data channels are invalidated', async () => {
|
|
544
513
|
const engine = new RTCEngine(roomOptionDefaults);
|
|
545
514
|
const dc = new FakeDataChannel();
|
|
546
515
|
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
547
516
|
_isClosed: false,
|
|
548
|
-
dataChannelForKind: vi.fn(() => dc),
|
|
549
517
|
});
|
|
518
|
+
attachFakeChannel(engine, 'reliableChannel', dc);
|
|
550
519
|
|
|
551
520
|
// Park a waiter: buffer above the reliable high-water mark, holding the headroom lock.
|
|
552
521
|
dc.bufferedAmount = 2 * 1024 * 1024;
|
|
553
|
-
const parked = engine.
|
|
522
|
+
const parked = engine.waitForBufferHeadroom(DataChannelKind.RELIABLE);
|
|
554
523
|
// Swallow the expected rejection so it can't surface as unhandled before we assert on it.
|
|
555
524
|
parked.catch(() => {});
|
|
556
525
|
await tick();
|
|
557
526
|
|
|
558
527
|
// The channel object gets abandoned (e.g. createDataChannels on the Safari resume path).
|
|
559
|
-
(
|
|
560
|
-
engine as unknown as { invalidateDataChannelWaiters: (reason: string) => void }
|
|
561
|
-
).invalidateDataChannelWaiters('data channels recreated');
|
|
528
|
+
engineChannel(engine, 'reliableChannel').invalidateWaiters('data channels recreated');
|
|
562
529
|
|
|
563
530
|
await expect(parked).rejects.toBeInstanceOf(UnexpectedConnectionState);
|
|
564
531
|
|
|
565
532
|
// The lock must be free again: a wait against the fresh, drained channel resolves instead
|
|
566
533
|
// of queueing forever behind the stranded waiter.
|
|
567
534
|
dc.bufferedAmount = 0;
|
|
568
|
-
await expect(
|
|
569
|
-
engine.waitForBufferHeadroomWithLock(DataChannelKind.RELIABLE),
|
|
570
|
-
).resolves.toBeUndefined();
|
|
535
|
+
await expect(engine.waitForBufferHeadroom(DataChannelKind.RELIABLE)).resolves.toBeUndefined();
|
|
571
536
|
});
|
|
572
537
|
});
|
|
573
538
|
|
|
@@ -623,4 +588,335 @@ describe('RTCEngine', () => {
|
|
|
623
588
|
expect(error).not.toHaveBeenCalled();
|
|
624
589
|
});
|
|
625
590
|
});
|
|
591
|
+
|
|
592
|
+
describe('local connection quality Lost handling', () => {
|
|
593
|
+
// The engine reacts to the server's own verdict: a sustained local `LOST` while
|
|
594
|
+
// connected and publishing means our media isn't reaching the server, so it forces
|
|
595
|
+
// a full reconnect. (A genuine `LOST` can't be produced from a browser page — any
|
|
596
|
+
// live sender keeps RTCP flowing — so the behavior is unit tested here rather than
|
|
597
|
+
// in the e2e suite.) `connectionQualityLostTimeout` in RTCEngine.ts is 5s.
|
|
598
|
+
const LOST_TIMEOUT_MS = 10_000;
|
|
599
|
+
const LOCAL_SID = 'PA_local';
|
|
600
|
+
|
|
601
|
+
beforeEach(() => {
|
|
602
|
+
vi.useFakeTimers();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
afterEach(() => {
|
|
606
|
+
vi.useRealTimers();
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
/** An engine primed to satisfy the reconnect guard: connected, publishing, not closed. */
|
|
610
|
+
function primeEngine(overrides: { activeSenders?: boolean; pcState?: number } = {}) {
|
|
611
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
612
|
+
const internals = engine as unknown as {
|
|
613
|
+
_isClosed: boolean;
|
|
614
|
+
participantSid: string;
|
|
615
|
+
// PCState is a private enum; Connected is 1, Reconnecting is 3.
|
|
616
|
+
pcState: number;
|
|
617
|
+
attemptingReconnect: boolean;
|
|
618
|
+
pcManager: unknown;
|
|
619
|
+
handleDisconnect: (connection: string, reason?: number) => void;
|
|
620
|
+
handleLocalConnectionQuality: (update: unknown) => void;
|
|
621
|
+
};
|
|
622
|
+
internals._isClosed = false;
|
|
623
|
+
internals.participantSid = LOCAL_SID;
|
|
624
|
+
internals.pcState = overrides.pcState ?? 1; // PCState.Connected
|
|
625
|
+
internals.attemptingReconnect = false;
|
|
626
|
+
internals.pcManager = {
|
|
627
|
+
publisher: {
|
|
628
|
+
getSenders: () =>
|
|
629
|
+
overrides.activeSenders === false ? [] : [{ track: { readyState: 'live' } }],
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
const handleDisconnect = vi.fn();
|
|
633
|
+
internals.handleDisconnect = handleDisconnect;
|
|
634
|
+
return { engine, internals, handleDisconnect };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function qualityUpdate(sid: string, quality: ProtoConnectionQuality) {
|
|
638
|
+
return { updates: [{ participantSid: sid, quality }] };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
it('forces a full reconnect after a sustained local Lost while publishing', () => {
|
|
642
|
+
const { engine, internals, handleDisconnect } = primeEngine();
|
|
643
|
+
|
|
644
|
+
internals.handleLocalConnectionQuality(qualityUpdate(LOCAL_SID, ProtoConnectionQuality.LOST));
|
|
645
|
+
|
|
646
|
+
// still pending — the reconnect only fires once the timeout elapses
|
|
647
|
+
expect(engine.fullReconnectOnNext).toBe(false);
|
|
648
|
+
expect(handleDisconnect).not.toHaveBeenCalled();
|
|
649
|
+
|
|
650
|
+
vi.advanceTimersByTime(LOST_TIMEOUT_MS);
|
|
651
|
+
|
|
652
|
+
expect(engine.fullReconnectOnNext).toBe(true);
|
|
653
|
+
expect(handleDisconnect).toHaveBeenCalledTimes(1);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('cancels the pending reconnect when quality recovers before the timeout', () => {
|
|
657
|
+
const { engine, internals, handleDisconnect } = primeEngine();
|
|
658
|
+
|
|
659
|
+
internals.handleLocalConnectionQuality(qualityUpdate(LOCAL_SID, ProtoConnectionQuality.LOST));
|
|
660
|
+
vi.advanceTimersByTime(LOST_TIMEOUT_MS / 2);
|
|
661
|
+
internals.handleLocalConnectionQuality(
|
|
662
|
+
qualityUpdate(LOCAL_SID, ProtoConnectionQuality.EXCELLENT),
|
|
663
|
+
);
|
|
664
|
+
vi.advanceTimersByTime(LOST_TIMEOUT_MS);
|
|
665
|
+
|
|
666
|
+
expect(engine.fullReconnectOnNext).toBe(false);
|
|
667
|
+
expect(handleDisconnect).not.toHaveBeenCalled();
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('does not reconnect on Lost when there are no active publisher senders', () => {
|
|
671
|
+
const { engine, internals, handleDisconnect } = primeEngine({ activeSenders: false });
|
|
672
|
+
|
|
673
|
+
internals.handleLocalConnectionQuality(qualityUpdate(LOCAL_SID, ProtoConnectionQuality.LOST));
|
|
674
|
+
vi.advanceTimersByTime(LOST_TIMEOUT_MS);
|
|
675
|
+
|
|
676
|
+
expect(engine.fullReconnectOnNext).toBe(false);
|
|
677
|
+
expect(handleDisconnect).not.toHaveBeenCalled();
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('does not reconnect on Lost when the pc is not connected', () => {
|
|
681
|
+
const { engine, internals, handleDisconnect } = primeEngine({ pcState: 3 }); // Reconnecting
|
|
682
|
+
|
|
683
|
+
internals.handleLocalConnectionQuality(qualityUpdate(LOCAL_SID, ProtoConnectionQuality.LOST));
|
|
684
|
+
vi.advanceTimersByTime(LOST_TIMEOUT_MS);
|
|
685
|
+
|
|
686
|
+
expect(engine.fullReconnectOnNext).toBe(false);
|
|
687
|
+
expect(handleDisconnect).not.toHaveBeenCalled();
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it('ignores Lost quality reported for other participants', () => {
|
|
691
|
+
const { engine, internals, handleDisconnect } = primeEngine();
|
|
692
|
+
|
|
693
|
+
internals.handleLocalConnectionQuality(
|
|
694
|
+
qualityUpdate('PA_other', ProtoConnectionQuality.LOST),
|
|
695
|
+
);
|
|
696
|
+
vi.advanceTimersByTime(LOST_TIMEOUT_MS);
|
|
697
|
+
|
|
698
|
+
expect(engine.fullReconnectOnNext).toBe(false);
|
|
699
|
+
expect(handleDisconnect).not.toHaveBeenCalled();
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
describe('reconnect requested mid-attempt', () => {
|
|
704
|
+
// A full reconnect requested while a resume is already in flight (e.g. a server
|
|
705
|
+
// RECONNECT leave racing the resume) sets `fullReconnectOnNext` mid-attempt. A
|
|
706
|
+
// successful resume must not swallow it: it survives and is dispatched afterwards.
|
|
707
|
+
interface ReconnectInternals {
|
|
708
|
+
_isClosed: boolean;
|
|
709
|
+
attemptingReconnect: boolean;
|
|
710
|
+
clientConfiguration: unknown;
|
|
711
|
+
pcManager: unknown;
|
|
712
|
+
resumeConnection: (reason?: number) => Promise<void>;
|
|
713
|
+
restartConnection: (regionUrl?: string) => Promise<void>;
|
|
714
|
+
clearPendingReconnect: () => void;
|
|
715
|
+
handleDisconnect: (connection: string, reason?: number) => void;
|
|
716
|
+
attemptReconnect: (reason?: number) => Promise<void>;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function primeEngine() {
|
|
720
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
721
|
+
const internals = engine as unknown as ReconnectInternals;
|
|
722
|
+
internals._isClosed = false;
|
|
723
|
+
internals.attemptingReconnect = false;
|
|
724
|
+
// avoid the "resume disabled / pcManager is NEW -> force full reconnect" escalation
|
|
725
|
+
internals.clientConfiguration = undefined;
|
|
726
|
+
internals.pcManager = { currentState: PCTransportState.CONNECTED };
|
|
727
|
+
internals.clearPendingReconnect = vi.fn();
|
|
728
|
+
const handleDisconnect = vi.fn();
|
|
729
|
+
internals.handleDisconnect = handleDisconnect;
|
|
730
|
+
const restartConnection = vi.fn(async () => {});
|
|
731
|
+
internals.restartConnection = restartConnection;
|
|
732
|
+
return { engine, internals, handleDisconnect, restartConnection };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
it('dispatches a full reconnect when a resume succeeds but one was requested mid-attempt', async () => {
|
|
736
|
+
const { engine, internals, handleDisconnect, restartConnection } = primeEngine();
|
|
737
|
+
engine.fullReconnectOnNext = false;
|
|
738
|
+
// the resume succeeds, but a RECONNECT leave arrives while it is in flight
|
|
739
|
+
internals.resumeConnection = vi.fn(async () => {
|
|
740
|
+
engine.fullReconnectOnNext = true;
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
await internals.attemptReconnect();
|
|
744
|
+
|
|
745
|
+
expect(internals.resumeConnection).toHaveBeenCalledTimes(1);
|
|
746
|
+
expect(restartConnection).not.toHaveBeenCalled();
|
|
747
|
+
// the mid-attempt request survived the successful resume and was dispatched
|
|
748
|
+
expect(engine.fullReconnectOnNext).toBe(true);
|
|
749
|
+
expect(handleDisconnect).toHaveBeenCalledTimes(1);
|
|
750
|
+
expect(handleDisconnect).toHaveBeenCalledWith('reconnect');
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it('does not dispatch a follow-up after an ordinary successful resume', async () => {
|
|
754
|
+
const { engine, internals, handleDisconnect, restartConnection } = primeEngine();
|
|
755
|
+
engine.fullReconnectOnNext = false;
|
|
756
|
+
internals.resumeConnection = vi.fn(async () => {});
|
|
757
|
+
|
|
758
|
+
await internals.attemptReconnect();
|
|
759
|
+
|
|
760
|
+
expect(internals.resumeConnection).toHaveBeenCalledTimes(1);
|
|
761
|
+
expect(restartConnection).not.toHaveBeenCalled();
|
|
762
|
+
expect(engine.fullReconnectOnNext).toBe(false);
|
|
763
|
+
expect(handleDisconnect).not.toHaveBeenCalled();
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it('clears the flag and does not re-dispatch after a successful full reconnect', async () => {
|
|
767
|
+
const { engine, internals, handleDisconnect, restartConnection } = primeEngine();
|
|
768
|
+
engine.fullReconnectOnNext = true; // enters as a full reconnect
|
|
769
|
+
internals.resumeConnection = vi.fn(async () => {});
|
|
770
|
+
|
|
771
|
+
await internals.attemptReconnect();
|
|
772
|
+
|
|
773
|
+
expect(restartConnection).toHaveBeenCalledTimes(1);
|
|
774
|
+
expect(internals.resumeConnection).not.toHaveBeenCalled();
|
|
775
|
+
expect(engine.fullReconnectOnNext).toBe(false);
|
|
776
|
+
expect(handleDisconnect).not.toHaveBeenCalled();
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it('dispatches a follow-up when a full reconnect succeeds but one was requested mid-attempt', async () => {
|
|
780
|
+
const { engine, internals, handleDisconnect, restartConnection } = primeEngine();
|
|
781
|
+
engine.fullReconnectOnNext = true; // enters as a full reconnect
|
|
782
|
+
// a new RECONNECT request arrives while restartConnection is running
|
|
783
|
+
restartConnection.mockImplementationOnce(async () => {
|
|
784
|
+
engine.fullReconnectOnNext = true;
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
await internals.attemptReconnect();
|
|
788
|
+
|
|
789
|
+
expect(restartConnection).toHaveBeenCalledTimes(1);
|
|
790
|
+
// the mid-restart request survived the successful full reconnect and was dispatched
|
|
791
|
+
expect(engine.fullReconnectOnNext).toBe(true);
|
|
792
|
+
expect(handleDisconnect).toHaveBeenCalledTimes(1);
|
|
793
|
+
expect(handleDisconnect).toHaveBeenCalledWith('reconnect');
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it('does not add a dispatch on top of the failure path retry', async () => {
|
|
797
|
+
const { engine, internals, handleDisconnect } = primeEngine();
|
|
798
|
+
engine.fullReconnectOnNext = false;
|
|
799
|
+
// resume fails after a mid-attempt request; the catch path schedules the retry
|
|
800
|
+
internals.resumeConnection = vi.fn(async () => {
|
|
801
|
+
engine.fullReconnectOnNext = true;
|
|
802
|
+
throw new Error('resume failed');
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
await internals.attemptReconnect();
|
|
806
|
+
|
|
807
|
+
// exactly one dispatch (from the catch), not a second one from the finally
|
|
808
|
+
expect(handleDisconnect).toHaveBeenCalledTimes(1);
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
describe('verifyTransport stuck-connecting bound', () => {
|
|
813
|
+
interface VerifyInternals {
|
|
814
|
+
pcManager: unknown;
|
|
815
|
+
client: unknown;
|
|
816
|
+
transportConnectingSince?: number;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function primeEngine(currentState: PCTransportState) {
|
|
820
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
821
|
+
const internals = engine as unknown as VerifyInternals;
|
|
822
|
+
internals.pcManager = { currentState };
|
|
823
|
+
internals.client = { ws: { readyState: WebSocket.OPEN } };
|
|
824
|
+
return { engine, internals };
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
it('reports the transport stuck when connecting longer than peerConnectionTimeout', () => {
|
|
828
|
+
const { engine, internals } = primeEngine(PCTransportState.CONNECTING);
|
|
829
|
+
internals.transportConnectingSince = Date.now() - (engine.peerConnectionTimeout + 1_000);
|
|
830
|
+
|
|
831
|
+
expect(engine.verifyTransport()).toBe(false);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('tolerates a transport still within the connecting window', () => {
|
|
835
|
+
const { engine, internals } = primeEngine(PCTransportState.CONNECTING);
|
|
836
|
+
internals.transportConnectingSince = Date.now();
|
|
837
|
+
|
|
838
|
+
expect(engine.verifyTransport()).toBe(true);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('fails open (and does not record a timestamp) when connecting is untracked', () => {
|
|
842
|
+
// verifyTransport is a pure read now: an unrecorded CONNECTING must not be treated as
|
|
843
|
+
// stuck, and the method must not seed a timestamp that could later leak across teardown.
|
|
844
|
+
const { engine, internals } = primeEngine(PCTransportState.CONNECTING);
|
|
845
|
+
internals.transportConnectingSince = undefined;
|
|
846
|
+
|
|
847
|
+
expect(engine.verifyTransport()).toBe(true);
|
|
848
|
+
expect(internals.transportConnectingSince).toBeUndefined();
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('does not measure a stale connecting timestamp while connected', () => {
|
|
852
|
+
const { engine, internals } = primeEngine(PCTransportState.CONNECTED);
|
|
853
|
+
// a leftover timestamp must not affect the CONNECTED verdict, and stays for the
|
|
854
|
+
// state-change handler to clear rather than being mutated here
|
|
855
|
+
internals.transportConnectingSince = Date.now() - 10 * engine.peerConnectionTimeout;
|
|
856
|
+
|
|
857
|
+
expect(engine.verifyTransport()).toBe(true);
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
describe('Lost-quality countdown across reconnects', () => {
|
|
862
|
+
// A Lost-quality countdown armed by the previous session must not survive a reconnect
|
|
863
|
+
// and fire against the new session before the server has re-evaluated it.
|
|
864
|
+
const LOST_TIMEOUT_MS = 5_000;
|
|
865
|
+
|
|
866
|
+
beforeEach(() => {
|
|
867
|
+
vi.useFakeTimers();
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
afterEach(() => {
|
|
871
|
+
vi.useRealTimers();
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it('cancels a pending Lost countdown when a reconnect attempt begins', async () => {
|
|
875
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
876
|
+
const internals = engine as unknown as {
|
|
877
|
+
_isClosed: boolean;
|
|
878
|
+
participantSid: string;
|
|
879
|
+
pcState: number;
|
|
880
|
+
attemptingReconnect: boolean;
|
|
881
|
+
clientConfiguration: unknown;
|
|
882
|
+
pcManager: unknown;
|
|
883
|
+
lostQualityTimeout?: ReturnType<typeof setTimeout>;
|
|
884
|
+
resumeConnection: (reason?: number) => Promise<void>;
|
|
885
|
+
restartConnection: () => Promise<void>;
|
|
886
|
+
clearPendingReconnect: () => void;
|
|
887
|
+
handleDisconnect: (connection: string, reason?: number) => void;
|
|
888
|
+
handleLocalConnectionQuality: (update: unknown) => void;
|
|
889
|
+
attemptReconnect: (reason?: number) => Promise<void>;
|
|
890
|
+
};
|
|
891
|
+
internals._isClosed = false;
|
|
892
|
+
internals.participantSid = 'PA_local';
|
|
893
|
+
internals.pcState = 1; // PCState.Connected — the guards the countdown checks would pass
|
|
894
|
+
internals.attemptingReconnect = false;
|
|
895
|
+
internals.clientConfiguration = undefined;
|
|
896
|
+
internals.pcManager = {
|
|
897
|
+
currentState: PCTransportState.CONNECTED,
|
|
898
|
+
publisher: { getSenders: () => [{ track: { readyState: 'live' } }] },
|
|
899
|
+
};
|
|
900
|
+
internals.clearPendingReconnect = vi.fn();
|
|
901
|
+
const handleDisconnect = vi.fn();
|
|
902
|
+
internals.handleDisconnect = handleDisconnect;
|
|
903
|
+
internals.resumeConnection = vi.fn(async () => {});
|
|
904
|
+
internals.restartConnection = vi.fn(async () => {});
|
|
905
|
+
|
|
906
|
+
// a LOST verdict from the (soon-to-be-previous) session arms the countdown
|
|
907
|
+
internals.handleLocalConnectionQuality({
|
|
908
|
+
updates: [{ participantSid: 'PA_local', quality: ProtoConnectionQuality.LOST }],
|
|
909
|
+
});
|
|
910
|
+
expect(internals.lostQualityTimeout).toBeDefined();
|
|
911
|
+
|
|
912
|
+
// a reconnect begins and completes (resume) before the countdown elapses
|
|
913
|
+
engine.fullReconnectOnNext = false;
|
|
914
|
+
await internals.attemptReconnect();
|
|
915
|
+
|
|
916
|
+
// the stale countdown was cancelled and cannot fire against the reconnected session
|
|
917
|
+
expect(internals.lostQualityTimeout).toBeUndefined();
|
|
918
|
+
vi.advanceTimersByTime(LOST_TIMEOUT_MS);
|
|
919
|
+
expect(handleDisconnect).not.toHaveBeenCalled();
|
|
920
|
+
});
|
|
921
|
+
});
|
|
626
922
|
});
|