livekit-client 2.22.0 → 2.22.1
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 +187 -1
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +1764 -121
- 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 +187 -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/api/SignalClient.d.ts +25 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/src/api/SignalClientStateMachine.d.ts.map +1 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -1
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +2 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +22 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +2 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +7 -2
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +8 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +1 -1
- package/dist/src/room/statsSummary.d.ts +13 -0
- package/dist/src/room/statsSummary.d.ts.map +1 -0
- package/dist/src/room/token-source/utils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +10 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/utils/machineInspector.d.ts +54 -0
- package/dist/src/utils/machineInspector.d.ts.map +1 -0
- package/dist/ts4.2/api/SignalClient.d.ts +25 -1
- package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/ts4.2/index.d.ts +3 -2
- package/dist/ts4.2/logger.d.ts +2 -1
- package/dist/ts4.2/options.d.ts +22 -0
- package/dist/ts4.2/room/PCTransport.d.ts +2 -1
- package/dist/ts4.2/room/RTCEngine.d.ts +7 -2
- package/dist/ts4.2/room/Room.d.ts +8 -1
- package/dist/ts4.2/room/events.d.ts +1 -1
- package/dist/ts4.2/room/statsSummary.d.ts +13 -0
- package/dist/ts4.2/room/track/Track.d.ts +10 -0
- package/dist/ts4.2/utils/machineInspector.d.ts +54 -0
- package/package.json +7 -1
- package/src/api/SignalClient.test.ts +320 -8
- package/src/api/SignalClient.ts +260 -82
- package/src/api/SignalClientStateMachine.test.ts +472 -0
- package/src/api/SignalClientStateMachine.ts +180 -0
- package/src/api/WebSocketStream.ts +19 -3
- package/src/api/utils.test.ts +20 -1
- package/src/api/utils.ts +5 -0
- package/src/index.ts +5 -0
- package/src/logger.ts +1 -0
- package/src/options.ts +24 -0
- package/src/room/PCTransport.ts +2 -1
- package/src/room/RTCEngine.ts +16 -7
- package/src/room/Room.ts +70 -5
- package/src/room/events.ts +1 -1
- package/src/room/statsSummary.ts +187 -0
- package/src/room/token-source/test-tokens.ts +20 -0
- package/src/room/token-source/utils.test.ts +27 -0
- package/src/room/token-source/utils.ts +12 -5
- package/src/room/track/LocalAudioTrack.ts +9 -3
- package/src/room/track/Track.ts +27 -0
- package/src/room/utils.test.ts +24 -1
- package/src/room/utils.ts +1 -1
- package/src/utils/machineInspector.ts +90 -0
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '@livekit/protocol';
|
|
13
13
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
14
|
import { ConnectionError, ConnectionErrorReason } from '../room/errors';
|
|
15
|
+
import CriticalTimers from '../room/timers';
|
|
15
16
|
import { SignalClient, SignalConnectionState } from './SignalClient';
|
|
16
17
|
import type { WebSocketCloseInfo, WebSocketConnection } from './WebSocketStream';
|
|
17
18
|
import { WebSocketStream } from './WebSocketStream';
|
|
@@ -275,7 +276,10 @@ describe('SignalClient.connect', () => {
|
|
|
275
276
|
.catch((e) => e);
|
|
276
277
|
|
|
277
278
|
expect(error).toBeInstanceOf(ConnectionError);
|
|
278
|
-
|
|
279
|
+
// a stalled connect is a timeout, not a cancellation: the distinction is what lets Room try
|
|
280
|
+
// the next region and record the attempt against the backoff strategy
|
|
281
|
+
expect(error.reason).toBe(ConnectionErrorReason.Timeout);
|
|
282
|
+
expect(error.message).toContain('room connection has timed out');
|
|
279
283
|
});
|
|
280
284
|
});
|
|
281
285
|
|
|
@@ -550,6 +554,260 @@ describe('SignalClient.connect', () => {
|
|
|
550
554
|
});
|
|
551
555
|
});
|
|
552
556
|
|
|
557
|
+
describe('Resuming a client that never joined', () => {
|
|
558
|
+
it('is refused by the missing options rather than by the lifecycle', async () => {
|
|
559
|
+
// The engine can hold a freshly created client (Room.recreateEngine) whose machine is still
|
|
560
|
+
// `new`. Nothing there is resumable, and the options guard is what says so — it runs before the
|
|
561
|
+
// lifecycle input, so the caller gets a warning and undefined rather than a thrown refusal.
|
|
562
|
+
// If session identity ever became rehydratable, `new` would have to accept `reconnect` and this
|
|
563
|
+
// is the test that should change.
|
|
564
|
+
expect((signalClient as any).lifecycleState).toBe('new');
|
|
565
|
+
|
|
566
|
+
await expect(
|
|
567
|
+
signalClient.reconnect('wss://test.livekit.io', 'test-token', 'PA_session'),
|
|
568
|
+
).resolves.toBeUndefined();
|
|
569
|
+
|
|
570
|
+
expect((signalClient as any).lifecycleState).toBe('new');
|
|
571
|
+
expect(WebSocketStream).not.toHaveBeenCalled();
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
describe('Transport closed while connected', () => {
|
|
576
|
+
/** Joins with a `closed` promise the test controls, and reports what onClose saw. */
|
|
577
|
+
async function joinWithControllableClose() {
|
|
578
|
+
let closeTransport: (info: WebSocketCloseInfo) => void = () => {};
|
|
579
|
+
const closed = new Promise<WebSocketCloseInfo>((resolve) => {
|
|
580
|
+
closeTransport = resolve;
|
|
581
|
+
});
|
|
582
|
+
mockWebSocketStream({
|
|
583
|
+
connection: createMockConnection(
|
|
584
|
+
createMockReadableStream([createSignalResponse('join', createJoinResponse())]),
|
|
585
|
+
),
|
|
586
|
+
closed,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const closeReasons: Array<string> = [];
|
|
590
|
+
signalClient.onClose = (reason) => closeReasons.push(reason);
|
|
591
|
+
await signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions);
|
|
592
|
+
expect(signalClient.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
593
|
+
|
|
594
|
+
return { closeTransport, closeReasons };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
it('reports a clean server close, which used to be treated as nothing happening', async () => {
|
|
598
|
+
const { closeTransport, closeReasons } = await joinWithControllableClose();
|
|
599
|
+
|
|
600
|
+
// A server that drops signalling closes cleanly — a migration that never sends its
|
|
601
|
+
// Leave{action=RESUME} does exactly this. Swallowing it leaves Room and RTCEngine believing
|
|
602
|
+
// they are still connected until the connection reconcile forces a full reconnect.
|
|
603
|
+
closeTransport({ closeCode: 1000, reason: 'server dropped signalling' });
|
|
604
|
+
await vi.waitFor(() => expect(closeReasons).toEqual(['server dropped signalling']));
|
|
605
|
+
|
|
606
|
+
expect(signalClient.currentState).toBe(SignalConnectionState.DISCONNECTED);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('ignores a close from a transport that has already been replaced', async () => {
|
|
610
|
+
const { closeTransport, closeReasons } = await joinWithControllableClose();
|
|
611
|
+
|
|
612
|
+
// a newer attempt takes over before the old socket reports its close
|
|
613
|
+
(signalClient as any).sendLifecycleInput({ type: 'reconnect' });
|
|
614
|
+
closeTransport({ closeCode: 1006, reason: 'late close from the old socket' });
|
|
615
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
616
|
+
|
|
617
|
+
expect(closeReasons).toEqual([]);
|
|
618
|
+
expect(signalClient.currentState).toBe(SignalConnectionState.RECONNECTING);
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
describe('Held requests during a resume', () => {
|
|
623
|
+
/**
|
|
624
|
+
* Records the tracks whose mute requests reach the wire, in order. `mute` is session-scoped —
|
|
625
|
+
* it is absent from the pass-through list — so it is the class of request a resume holds.
|
|
626
|
+
* Filtered rather than counted, because keepalive traffic shares the transport.
|
|
627
|
+
*/
|
|
628
|
+
function captureMutedTracks() {
|
|
629
|
+
const muted: Array<string> = [];
|
|
630
|
+
const writable = new WritableStream<ArrayBuffer | string>({
|
|
631
|
+
write(chunk) {
|
|
632
|
+
const request = SignalRequest.fromBinary(new Uint8Array(chunk as ArrayBuffer));
|
|
633
|
+
if (request.message?.case === 'mute') {
|
|
634
|
+
muted.push(request.message.value.sid);
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
return { muted, writable };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
it('releases held requests before ones issued while the engine is still catching up', async () => {
|
|
642
|
+
mockWebSocketStream({
|
|
643
|
+
connection: createMockConnection(
|
|
644
|
+
createMockReadableStream([createSignalResponse('join', createJoinResponse())]),
|
|
645
|
+
),
|
|
646
|
+
});
|
|
647
|
+
await signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions);
|
|
648
|
+
|
|
649
|
+
const { muted, writable } = captureMutedTracks();
|
|
650
|
+
mockWebSocketStream({
|
|
651
|
+
connection: {
|
|
652
|
+
...createMockConnection(
|
|
653
|
+
createMockReadableStream([
|
|
654
|
+
createSignalResponse('reconnect', new ReconnectResponse({ iceServers: [] })),
|
|
655
|
+
]),
|
|
656
|
+
),
|
|
657
|
+
writable,
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const resuming = signalClient.reconnect('wss://test.livekit.io', 'test-token', 'sid-123');
|
|
662
|
+
expect(signalClient.currentState).toBe(SignalConnectionState.RECONNECTING);
|
|
663
|
+
|
|
664
|
+
// issued while the resume is in flight, so it waits rather than racing it
|
|
665
|
+
await signalClient.sendMuteTrack('held-during-resume', true);
|
|
666
|
+
expect(muted).toEqual([]);
|
|
667
|
+
|
|
668
|
+
await resuming;
|
|
669
|
+
expect(signalClient.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
670
|
+
|
|
671
|
+
// The transport is back, but the engine has not called setReconnected yet — a resume is only
|
|
672
|
+
// complete once the media path is back too, which in a real session is seconds later. A
|
|
673
|
+
// request issued in that window must not overtake the one that has been waiting.
|
|
674
|
+
await signalClient.sendMuteTrack('issued-while-catching-up', true);
|
|
675
|
+
expect(muted).toEqual([]);
|
|
676
|
+
|
|
677
|
+
signalClient.setReconnected();
|
|
678
|
+
await signalClient.sendMuteTrack('issued-after-reconnected', true);
|
|
679
|
+
|
|
680
|
+
expect(muted).toEqual([
|
|
681
|
+
'held-during-resume',
|
|
682
|
+
'issued-while-catching-up',
|
|
683
|
+
'issued-after-reconnected',
|
|
684
|
+
]);
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
describe('Failure Case - Closed After Upgrade', () => {
|
|
689
|
+
it('fails fast rather than waiting out the first-message timeout', async () => {
|
|
690
|
+
// The upgrade succeeds and the server then closes without sending a join response. Nothing
|
|
691
|
+
// else will reject here, so the close has to — otherwise the attempt hangs until the
|
|
692
|
+
// first-message timeout and reports a timeout instead of the close that caused it.
|
|
693
|
+
const neverYields = new ReadableStream<ArrayBuffer>({ start() {} });
|
|
694
|
+
mockWebSocketStream({
|
|
695
|
+
connection: createMockConnection(neverYields),
|
|
696
|
+
closed: Promise.resolve({ closeCode: 1011, reason: 'closed before join' }),
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const err = await signalClient
|
|
700
|
+
.join('wss://test.livekit.io', 'test-token', defaultOptions)
|
|
701
|
+
.then(
|
|
702
|
+
() => undefined,
|
|
703
|
+
(e) => e,
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
expect(err).toMatchObject({ reason: ConnectionErrorReason.InternalError });
|
|
707
|
+
expect((err as Error).message).toContain('Websocket got closed during');
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
describe('Failure Case - Upgrade Rejected', () => {
|
|
712
|
+
it('surfaces the classified error, not the close that races it', async () => {
|
|
713
|
+
// A refused token fails the upgrade and closes the socket at once, but classifying the failure
|
|
714
|
+
// needs a round trip to the validate endpoint. The close must not pre-empt that answer, or a
|
|
715
|
+
// caller checking for a 401 sees a generic "websocket got closed" instead.
|
|
716
|
+
vi.mocked(fetch).mockImplementation(
|
|
717
|
+
() =>
|
|
718
|
+
new Promise((resolve) => {
|
|
719
|
+
setTimeout(
|
|
720
|
+
() => resolve({ status: 401, text: async () => 'permission denied' } as Response),
|
|
721
|
+
20,
|
|
722
|
+
);
|
|
723
|
+
}),
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
vi.mocked(WebSocketStream).mockImplementation(function () {
|
|
727
|
+
return {
|
|
728
|
+
url: 'wss://test.livekit.io',
|
|
729
|
+
opened: Promise.reject(new Error('HTTP Authentication failed')),
|
|
730
|
+
closed: Promise.resolve({ closeCode: 1006, reason: '' }),
|
|
731
|
+
close: vi.fn(),
|
|
732
|
+
readyState: 3,
|
|
733
|
+
} as any;
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
await expect(
|
|
737
|
+
signalClient.join('wss://test.livekit.io', 'bad-token', defaultOptions),
|
|
738
|
+
).rejects.toMatchObject({
|
|
739
|
+
reason: ConnectionErrorReason.NotAllowed,
|
|
740
|
+
status: 401,
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
describe('Establishing over an existing session', () => {
|
|
746
|
+
async function joinSuccessfully() {
|
|
747
|
+
mockWebSocketStream({
|
|
748
|
+
connection: createMockConnection(
|
|
749
|
+
createMockReadableStream([createSignalResponse('join', createJoinResponse())]),
|
|
750
|
+
),
|
|
751
|
+
});
|
|
752
|
+
await signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions);
|
|
753
|
+
expect(signalClient.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
it('refuses a join over a live session rather than opening a transport it would discard', async () => {
|
|
757
|
+
await joinSuccessfully();
|
|
758
|
+
const transportsOpened = vi.mocked(WebSocketStream).mock.calls.length;
|
|
759
|
+
|
|
760
|
+
await expect(
|
|
761
|
+
signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions),
|
|
762
|
+
).rejects.toMatchObject({
|
|
763
|
+
message: expect.stringContaining("from 'connected'"),
|
|
764
|
+
reason: ConnectionErrorReason.InternalError,
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// refusing is not a teardown: the live session is untouched and no transport was opened
|
|
768
|
+
expect(signalClient.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
769
|
+
expect(vi.mocked(WebSocketStream).mock.calls.length).toBe(transportsOpened);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it('resumes after a close, which is how the engine recovers from an unexpected one', async () => {
|
|
773
|
+
await joinSuccessfully();
|
|
774
|
+
await signalClient.close();
|
|
775
|
+
expect((signalClient as any).lifecycleState).toBe('closed');
|
|
776
|
+
|
|
777
|
+
mockWebSocketStream({
|
|
778
|
+
connection: createMockConnection(
|
|
779
|
+
createMockReadableStream([
|
|
780
|
+
createSignalResponse('reconnect', new ReconnectResponse({ iceServers: [] })),
|
|
781
|
+
]),
|
|
782
|
+
),
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
await expect(
|
|
786
|
+
signalClient.reconnect('wss://test.livekit.io', 'test-token', 'PA_session'),
|
|
787
|
+
).resolves.toBeDefined();
|
|
788
|
+
expect(signalClient.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it('waits for an in-flight close to settle instead of racing its teardown', async () => {
|
|
792
|
+
await joinSuccessfully();
|
|
793
|
+
mockWebSocketStream({
|
|
794
|
+
connection: createMockConnection(
|
|
795
|
+
createMockReadableStream([createSignalResponse('join', createJoinResponse())]),
|
|
796
|
+
),
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// close and join without awaiting the close in between. The public projection folds
|
|
800
|
+
// `disconnecting` into DISCONNECTED, so the precondition is read off the lifecycle itself.
|
|
801
|
+
const closing = signalClient.close();
|
|
802
|
+
expect((signalClient as any).lifecycleState).toBe('disconnecting');
|
|
803
|
+
const joining = signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions);
|
|
804
|
+
|
|
805
|
+
await closing;
|
|
806
|
+
await expect(joining).resolves.toBeDefined();
|
|
807
|
+
expect(signalClient.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
|
|
553
811
|
describe('Edge Cases and State Management', () => {
|
|
554
812
|
it('should set state to CONNECTING when joining', async () => {
|
|
555
813
|
expect(signalClient.currentState).toBe(SignalConnectionState.DISCONNECTED);
|
|
@@ -626,14 +884,68 @@ describe('SignalClient.handleSignalConnected', () => {
|
|
|
626
884
|
const mockReadable = new ReadableStream<ArrayBuffer>();
|
|
627
885
|
const mockConnection = createMockConnection(mockReadable);
|
|
628
886
|
|
|
887
|
+
// handleSignalConnected only ever runs with an attempt in flight, so establish that first
|
|
888
|
+
(signalClient as any).sendLifecycleInput({ type: 'connect' });
|
|
889
|
+
|
|
629
890
|
// Access the method through a type assertion for testing
|
|
630
891
|
const handleMethod = (signalClient as any).handleSignalConnected;
|
|
631
892
|
if (handleMethod) {
|
|
632
|
-
handleMethod.call(signalClient, mockConnection);
|
|
893
|
+
handleMethod.call(signalClient, mockConnection, undefined, (signalClient as any).attemptId);
|
|
633
894
|
expect(signalClient.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
634
895
|
}
|
|
635
896
|
});
|
|
636
897
|
|
|
898
|
+
it('discards a connection whose attempt was abandoned while awaiting the first message', () => {
|
|
899
|
+
const mockReadable = new ReadableStream<ArrayBuffer>();
|
|
900
|
+
const mockConnection = createMockConnection(mockReadable);
|
|
901
|
+
const setIntervalSpy = vi.spyOn(CriticalTimers, 'setInterval');
|
|
902
|
+
const getReaderSpy = vi.spyOn(mockConnection.readable, 'getReader');
|
|
903
|
+
|
|
904
|
+
// a previous session supplied the keepalive config, so arming really would start a timer
|
|
905
|
+
(signalClient as any).pingIntervalDuration = 10;
|
|
906
|
+
(signalClient as any).pingTimeoutDuration = 30;
|
|
907
|
+
|
|
908
|
+
// an attempt is in flight, and its transport opens...
|
|
909
|
+
(signalClient as any).sendLifecycleInput({ type: 'connect' });
|
|
910
|
+
const abandonedAttemptId = (signalClient as any).attemptId;
|
|
911
|
+
|
|
912
|
+
// ...but the caller gives up before the server's first message arrives
|
|
913
|
+
(signalClient as any).sendLifecycleInput({
|
|
914
|
+
type: 'connectFailed',
|
|
915
|
+
error: new Error('aborted'),
|
|
916
|
+
});
|
|
917
|
+
expect(signalClient.currentState).toBe(SignalConnectionState.DISCONNECTED);
|
|
918
|
+
|
|
919
|
+
// the first message lands anyway: it must not arm a heartbeat or a reader for a dead session
|
|
920
|
+
(signalClient as any).handleSignalConnected(mockConnection, undefined, abandonedAttemptId);
|
|
921
|
+
|
|
922
|
+
expect(signalClient.currentState).toBe(SignalConnectionState.DISCONNECTED);
|
|
923
|
+
expect(setIntervalSpy).not.toHaveBeenCalled();
|
|
924
|
+
expect(getReaderSpy).not.toHaveBeenCalled();
|
|
925
|
+
expect((signalClient as any).pingInterval).toBeUndefined();
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
it('still arms the heartbeat and reader for the attempt that owns the session', () => {
|
|
929
|
+
const mockReadable = new ReadableStream<ArrayBuffer>();
|
|
930
|
+
const mockConnection = createMockConnection(mockReadable);
|
|
931
|
+
const setIntervalSpy = vi.spyOn(CriticalTimers, 'setInterval');
|
|
932
|
+
const getReaderSpy = vi.spyOn(mockConnection.readable, 'getReader');
|
|
933
|
+
|
|
934
|
+
(signalClient as any).pingIntervalDuration = 10;
|
|
935
|
+
(signalClient as any).pingTimeoutDuration = 30;
|
|
936
|
+
|
|
937
|
+
(signalClient as any).sendLifecycleInput({ type: 'connect' });
|
|
938
|
+
(signalClient as any).handleSignalConnected(
|
|
939
|
+
mockConnection,
|
|
940
|
+
undefined,
|
|
941
|
+
(signalClient as any).attemptId,
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
expect(signalClient.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
945
|
+
expect(setIntervalSpy).toHaveBeenCalled();
|
|
946
|
+
expect(getReaderSpy).toHaveBeenCalled();
|
|
947
|
+
});
|
|
948
|
+
|
|
637
949
|
it('should start reading loop without first message', async () => {
|
|
638
950
|
const joinResponse = createJoinResponse();
|
|
639
951
|
const signalResponse = createSignalResponse('join', joinResponse);
|
|
@@ -700,8 +1012,8 @@ describe('SignalClient.validateFirstMessage', () => {
|
|
|
700
1012
|
mockWebSocketStream({ connection: initialMockConnection });
|
|
701
1013
|
await signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions);
|
|
702
1014
|
|
|
703
|
-
//
|
|
704
|
-
(signalClient as any).
|
|
1015
|
+
// Move the lifecycle machine to reconnecting to match the validation logic
|
|
1016
|
+
(signalClient as any).sendLifecycleInput({ type: 'reconnect' });
|
|
705
1017
|
|
|
706
1018
|
const reconnectResponse = new ReconnectResponse({ iceServers: [] });
|
|
707
1019
|
const signalResponse = createSignalResponse('reconnect', reconnectResponse);
|
|
@@ -724,8 +1036,8 @@ describe('SignalClient.validateFirstMessage', () => {
|
|
|
724
1036
|
mockWebSocketStream({ connection: initialMockConnection });
|
|
725
1037
|
await signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions);
|
|
726
1038
|
|
|
727
|
-
//
|
|
728
|
-
(signalClient as any).
|
|
1039
|
+
// Move the lifecycle machine to reconnecting
|
|
1040
|
+
(signalClient as any).sendLifecycleInput({ type: 'reconnect' });
|
|
729
1041
|
|
|
730
1042
|
const updateSignalResponse = createSignalResponse('update', { participants: [] });
|
|
731
1043
|
|
|
@@ -739,8 +1051,8 @@ describe('SignalClient.validateFirstMessage', () => {
|
|
|
739
1051
|
});
|
|
740
1052
|
|
|
741
1053
|
it('should reject leave request during connection attempt', () => {
|
|
742
|
-
//
|
|
743
|
-
(signalClient as any).
|
|
1054
|
+
// Move the lifecycle machine to connecting to be in establishing connection state
|
|
1055
|
+
(signalClient as any).sendLifecycleInput({ type: 'connect' });
|
|
744
1056
|
|
|
745
1057
|
const leaveRequest = new LeaveRequest({ reason: 1 });
|
|
746
1058
|
const signalResponse = createSignalResponse('leave', leaveRequest);
|