@@ -373,4 +373,126 @@ t.describe("SharedCentrifuge Integration Tests", () => {
373373 t . expect ( context ?. subscriptions . get ( channel ) ) . toBeUndefined ( ) ;
374374 t . expect ( context ?. centrifuge . getSubscription ( channel ) ) . toBeNull ( ) ;
375375 } ) ;
376+
377+ t . test ( "should handle multiple simultaneous reconnect calls from different instances without connection storms" , async ( ) => {
378+ const channel = `session:${ uuid ( ) } ` ;
379+ const numClients = 5 ;
380+ const clients : SharedCentrifuge [ ] = [ ] ;
381+
382+ // Create multiple clients (simulating multiple wallet connections)
383+ for ( let i = 0 ; i < numClients ; i ++ ) {
384+ const client = new SharedCentrifuge ( WEBSOCKET_URL , { websocket : WebSocket } ) ;
385+ clients . push ( client ) ;
386+ instances . push ( client ) ;
387+ }
388+
389+ // Connect all clients
390+ const connectPromises = clients . map ( ( client ) => {
391+ const promise = waitFor ( client , "connected" ) ;
392+ client . connect ( ) ;
393+ return promise ;
394+ } ) ;
395+ await Promise . all ( connectPromises ) ;
396+
397+ // Subscribe all clients to the same channel
398+ const subscriptions = clients . map ( ( client ) => {
399+ const sub = client . newSubscription ( channel ) ;
400+ sub . subscribe ( ) ;
401+ return sub ;
402+ } ) ;
403+ await Promise . all ( subscriptions . map ( ( sub ) => new Promise ( ( resolve ) => sub . once ( "subscribed" , resolve ) ) ) ) ;
404+
405+ // Verify all clients are connected and subscribed
406+ clients . forEach ( ( client ) => {
407+ t . expect ( client . state ) . toBe ( "connected" ) ;
408+ } ) ;
409+
410+ // Access the shared context to verify there's only ONE underlying connection
411+ // @ts -expect-error - accessing private property for test
412+ const context = SharedCentrifuge . contexts . get ( WEBSOCKET_URL ) ;
413+ t . expect ( context ) . toBeDefined ( ) ;
414+ t . expect ( context ?. refcount ) . toBe ( numClients ) ;
415+
416+ // Test multiple reconnect cycles (simulating app going to background/foreground repeatedly)
417+ for ( let cycle = 0 ; cycle < 3 ; cycle ++ ) {
418+ // Call reconnect on ALL clients simultaneously (this is where the bug would manifest)
419+ const reconnectPromises = clients . map ( ( client ) => client . reconnect ( ) ) ;
420+
421+ // All reconnects should succeed and return the same promise (idempotent behavior)
422+ await Promise . all ( reconnectPromises ) ;
423+
424+ // Verify all clients are still connected
425+ clients . forEach ( ( client ) => {
426+ t . expect ( client . state ) . toBe ( "connected" ) ;
427+ } ) ;
428+
429+ // Verify messages can still be sent and received after reconnect
430+ const messagePromise = new Promise ( ( resolve ) => {
431+ subscriptions [ 0 ] . once ( "publication" , ( ctx ) => resolve ( ctx . data ) ) ;
432+ } ) ;
433+
434+ const testPayload = { test : `message-after-reconnect-cycle-${ cycle } ` } ;
435+ await clients [ 0 ] . publish ( channel , JSON . stringify ( testPayload ) ) ;
436+
437+ const received = await messagePromise ;
438+ t . expect ( JSON . parse ( received as string ) ) . toEqual ( testPayload ) ;
439+
440+ // Wait a bit between cycles (simulating time between app suspensions)
441+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
442+ }
443+
444+ // Final verification: send one more message to ensure everything still works
445+ const finalMessagePromise = new Promise ( ( resolve ) => {
446+ subscriptions [ numClients - 1 ] . once ( "publication" , ( ctx ) => resolve ( ctx . data ) ) ;
447+ } ) ;
448+
449+ const finalPayload = { test : "final-message-after-all-reconnects" } ;
450+ await clients [ numClients - 1 ] . publish ( channel , JSON . stringify ( finalPayload ) ) ;
451+
452+ const finalReceived = await finalMessagePromise ;
453+ t . expect ( JSON . parse ( finalReceived as string ) ) . toEqual ( finalPayload ) ;
454+ } ) ;
455+
456+ t . test ( "should handle rapid successive reconnects without causing race conditions" , async ( ) => {
457+ const channel = `session:${ uuid ( ) } ` ;
458+ const client1 = new SharedCentrifuge ( WEBSOCKET_URL , { websocket : WebSocket } ) ;
459+ const client2 = new SharedCentrifuge ( WEBSOCKET_URL , { websocket : WebSocket } ) ;
460+ instances . push ( client1 , client2 ) ;
461+
462+ // Connect both clients
463+ const connectedPromise1 = waitFor ( client1 , "connected" ) ;
464+ const connectedPromise2 = waitFor ( client2 , "connected" ) ;
465+ client1 . connect ( ) ;
466+ await Promise . all ( [ connectedPromise1 , connectedPromise2 ] ) ;
467+
468+ // Subscribe both to the same channel
469+ const sub1 = client1 . newSubscription ( channel ) ;
470+ const sub2 = client2 . newSubscription ( channel ) ;
471+ sub1 . subscribe ( ) ;
472+ sub2 . subscribe ( ) ;
473+ await Promise . all ( [ new Promise ( ( resolve ) => sub1 . once ( "subscribed" , resolve ) ) , new Promise ( ( resolve ) => sub2 . once ( "subscribed" , resolve ) ) ] ) ;
474+
475+ // Fire off many reconnects in rapid succession from both clients
476+ const rapidReconnects : Promise < void > [ ] = [ ] ;
477+ for ( let i = 0 ; i < 10 ; i ++ ) {
478+ rapidReconnects . push ( client1 . reconnect ( ) ) ;
479+ rapidReconnects . push ( client2 . reconnect ( ) ) ;
480+ }
481+
482+ // All should complete successfully
483+ await Promise . all ( rapidReconnects ) ;
484+
485+ // Verify both clients are still connected
486+ t . expect ( client1 . state ) . toBe ( "connected" ) ;
487+ t . expect ( client2 . state ) . toBe ( "connected" ) ;
488+
489+ // Verify messaging still works
490+ const messagePromise = new Promise ( ( resolve ) => {
491+ sub2 . once ( "publication" , ( ctx ) => resolve ( ctx . data ) ) ;
492+ } ) ;
493+
494+ await client1 . publish ( channel , JSON . stringify ( { test : "after-rapid-reconnects" } ) ) ;
495+ const received = await messagePromise ;
496+ t . expect ( JSON . parse ( received as string ) ) . toEqual ( { test : "after-rapid-reconnects" } ) ;
497+ } ) ;
376498} ) ;
0 commit comments