@@ -828,4 +828,179 @@ describe('RegionUrlProvider', () => {
828828 expect ( region2 ) . toBeNull ( ) ; // Filtered out because same URL was already attempted
829829 } ) ;
830830 } ) ;
831+
832+ describe ( 'connection tracking and auto-refetch cleanup' , ( ) => {
833+ beforeEach ( ( ) => {
834+ // Reset connection tracking maps
835+ // @ts -ignore - accessing private static field for testing
836+ RegionUrlProvider . connectionTrackers = new Map ( ) ;
837+ } ) ;
838+
839+ it ( 'stops auto-refetch 30s after last connection disconnects' , async ( ) => {
840+ const provider = new RegionUrlProvider ( 'wss://test.livekit.cloud' , 'token' ) ;
841+ const mockSettings = createMockRegionSettings ( [
842+ { region : 'us-west' , url : 'wss://us-west.livekit.cloud' } ,
843+ ] ) ;
844+
845+ fetchMock . mockResolvedValue (
846+ createMockResponse ( 200 , mockSettings , { 'Cache-Control' : 'max-age=100' } ) ,
847+ ) ;
848+
849+ // Initial fetch to start auto-refetch
850+ await provider . getNextBestRegionUrl ( ) ;
851+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 ) ;
852+
853+ const hostname = provider . getServerUrl ( ) . hostname ;
854+
855+ // Simulate connection
856+ provider . notifyConnected ( ) ;
857+
858+ // Verify auto-refetch is running
859+ const timersBeforeDisconnect = vi . getTimerCount ( ) ;
860+ expect ( timersBeforeDisconnect ) . toBeGreaterThan ( 0 ) ;
861+
862+ // Simulate disconnect
863+ provider . notifyDisconnected ( ) ;
864+
865+ // Should schedule cleanup timeout (30s)
866+ // Advance time by 29s - refetch should still be running
867+ vi . advanceTimersByTime ( 29000 ) ;
868+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 ) ; // No additional fetches
869+
870+ // Advance by 1 more second (total 30s) - cleanup should trigger
871+ await vi . advanceTimersByTimeAsync ( 1000 ) ;
872+
873+ // Auto-refetch should be stopped, so no new timers for refetch
874+ // @ts -ignore - accessing private static field for testing
875+ const refetchTimeout = RegionUrlProvider . settingsTimeouts . get ( hostname ) ;
876+ expect ( refetchTimeout ) . toBeUndefined ( ) ;
877+ } ) ;
878+
879+ it ( 'cancels cleanup when reconnecting before 30s delay' , async ( ) => {
880+ const provider = new RegionUrlProvider ( 'wss://test.livekit.cloud' , 'token' ) ;
881+ const mockSettings = createMockRegionSettings ( [
882+ { region : 'us-west' , url : 'wss://us-west.livekit.cloud' } ,
883+ ] ) ;
884+
885+ fetchMock . mockResolvedValue (
886+ createMockResponse ( 200 , mockSettings , { 'Cache-Control' : 'max-age=100' } ) ,
887+ ) ;
888+
889+ await provider . getNextBestRegionUrl ( ) ;
890+ const hostname = provider . getServerUrl ( ) . hostname ;
891+
892+ // Connect and disconnect
893+ provider . notifyConnected ( ) ;
894+ provider . notifyDisconnected ( ) ;
895+
896+ // Advance time by 15s (less than 30s)
897+ vi . advanceTimersByTime ( 15000 ) ;
898+
899+ // Reconnect before cleanup triggers
900+ provider . notifyConnected ( ) ;
901+
902+ // @ts -ignore - accessing private static field for testing
903+ const tracker = RegionUrlProvider . connectionTrackers . get ( hostname ) ;
904+ expect ( tracker ?. cleanupTimeout ) . toBeUndefined ( ) ; // Cleanup should be cancelled
905+
906+ // Advance past the original 30s mark
907+ vi . advanceTimersByTime ( 20000 ) ;
908+
909+ // Auto-refetch should still be running
910+ // @ts -ignore - accessing private static field for testing
911+ const refetchTimeout = RegionUrlProvider . settingsTimeouts . get ( hostname ) ;
912+ expect ( refetchTimeout ) . toBeDefined ( ) ;
913+ } ) ;
914+
915+ it ( 'tracks multiple connections correctly' , async ( ) => {
916+ const provider = new RegionUrlProvider ( 'wss://test.livekit.cloud' , 'token' ) ;
917+ const mockSettings = createMockRegionSettings ( [
918+ { region : 'us-west' , url : 'wss://us-west.livekit.cloud' } ,
919+ ] ) ;
920+
921+ fetchMock . mockResolvedValue (
922+ createMockResponse ( 200 , mockSettings , { 'Cache-Control' : 'max-age=100' } ) ,
923+ ) ;
924+
925+ await provider . getNextBestRegionUrl ( ) ;
926+ const hostname = provider . getServerUrl ( ) . hostname ;
927+
928+ // Simulate 3 connections
929+ provider . notifyConnected ( ) ;
930+ provider . notifyConnected ( ) ;
931+ provider . notifyConnected ( ) ;
932+
933+ // @ts -ignore - accessing private static field for testing
934+ const tracker = RegionUrlProvider . connectionTrackers . get ( hostname ) ;
935+ expect ( tracker ?. connectionCount ) . toBe ( 3 ) ;
936+
937+ // Disconnect first connection
938+ provider . notifyDisconnected ( ) ;
939+
940+ // @ts -ignore - accessing private static field for testing
941+ expect ( tracker ?. connectionCount ) . toBe ( 2 ) ;
942+
943+ // Should NOT schedule cleanup yet (still have active connections)
944+ expect ( tracker ?. cleanupTimeout ) . toBeUndefined ( ) ;
945+
946+ // Disconnect second connection
947+ provider . notifyDisconnected ( ) ;
948+ // @ts -ignore - accessing private static field for testing
949+ expect ( tracker ?. connectionCount ) . toBe ( 1 ) ;
950+
951+ // Disconnect last connection
952+ provider . notifyDisconnected ( ) ;
953+ // @ts -ignore - accessing private static field for testing
954+ expect ( tracker ?. connectionCount ) . toBe ( 0 ) ;
955+
956+ // NOW cleanup should be scheduled
957+ expect ( tracker ?. cleanupTimeout ) . toBeDefined ( ) ;
958+ } ) ;
959+
960+ it ( 'handles disconnect without prior connect gracefully' , ( ) => {
961+ const provider = new RegionUrlProvider ( 'wss://test.livekit.cloud' , 'token' ) ;
962+ const hostname = provider . getServerUrl ( ) . hostname ;
963+
964+ // Disconnect without connect should not throw
965+ expect ( ( ) => {
966+ provider . notifyDisconnected ( ) ;
967+ } ) . not . toThrow ( ) ;
968+
969+ // Should not create a tracker
970+ // @ts -ignore - accessing private static field for testing
971+ const tracker = RegionUrlProvider . connectionTrackers . get ( hostname ) ;
972+ expect ( tracker ) . toBeUndefined ( ) ;
973+ } ) ;
974+
975+ it ( 'clears cleanup timeout when scheduling new cleanup' , async ( ) => {
976+ const provider = new RegionUrlProvider ( 'wss://test.livekit.cloud' , 'token' ) ;
977+ const mockSettings = createMockRegionSettings ( [
978+ { region : 'us-west' , url : 'wss://us-west.livekit.cloud' } ,
979+ ] ) ;
980+
981+ fetchMock . mockResolvedValue (
982+ createMockResponse ( 200 , mockSettings , { 'Cache-Control' : 'max-age=100' } ) ,
983+ ) ;
984+
985+ await provider . getNextBestRegionUrl ( ) ;
986+ const hostname = provider . getServerUrl ( ) . hostname ;
987+
988+ // Connect and disconnect (schedules cleanup)
989+ provider . notifyConnected ( ) ;
990+ provider . notifyDisconnected ( ) ;
991+
992+ // @ts -ignore - accessing private static field for testing
993+ const tracker = RegionUrlProvider . connectionTrackers . get ( hostname ) ;
994+ const firstCleanupTimeout = tracker ?. cleanupTimeout ;
995+ expect ( firstCleanupTimeout ) . toBeDefined ( ) ;
996+
997+ // Reconnect and disconnect again (should cancel first and schedule new)
998+ provider . notifyConnected ( ) ;
999+ provider . notifyDisconnected ( ) ;
1000+
1001+ const secondCleanupTimeout = tracker ?. cleanupTimeout ;
1002+ expect ( secondCleanupTimeout ) . toBeDefined ( ) ;
1003+ expect ( secondCleanupTimeout ) . not . toBe ( firstCleanupTimeout ) ;
1004+ } ) ;
1005+ } ) ;
8311006} ) ;
0 commit comments