@@ -9,6 +9,26 @@ describe("MultiTabWorkerBroker", () => {
99 let testCounter = 0 ;
1010 let getLockName : ( ) => string ;
1111
12+ // Helper function to wait for a condition with retry logic
13+ async function waitFor < T > (
14+ fn : ( ) => T | Promise < T > ,
15+ predicate : ( value : T ) => boolean ,
16+ options : { timeout ?: number ; interval ?: number ; message ?: string } = { }
17+ ) : Promise < T > {
18+ const { timeout = 5000 , interval = 10 , message = "Condition not met within timeout" } = options ;
19+ const startTime = Date . now ( ) ;
20+
21+ while ( Date . now ( ) - startTime < timeout ) {
22+ const value = await fn ( ) ;
23+ if ( predicate ( value ) ) {
24+ return value ;
25+ }
26+ await new Promise ( ( resolve ) => setTimeout ( resolve , interval ) ) ;
27+ }
28+
29+ throw new Error ( message ) ;
30+ }
31+
1232 beforeEach ( ( ) => {
1333 // Create unique lock name for each test
1434 const lockName = `test-lock-${ testCounter ++ } ` ;
@@ -215,8 +235,14 @@ describe("MultiTabWorkerBroker", () => {
215235
216236 // Wait for follower to become leader and initialize its worker
217237 await broker2LeaderPromise ;
218- // Give a bit more time for worker to be fully ready
219- await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
238+ // Wait for broker2 to be fully ready as leader
239+ await waitFor (
240+ ( ) => broker2 . isLeader ,
241+ ( isLeader ) => isLeader === true ,
242+ { timeout : 500 , message : "Broker2 did not become leader" }
243+ ) ;
244+ // Give worker additional time to be fully ready
245+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
220246 expect ( broker2 . isLeader ) . toBe ( true ) ;
221247
222248 // Verify new leader can communicate
@@ -226,7 +252,13 @@ describe("MultiTabWorkerBroker", () => {
226252 method : "echo" ,
227253 params : { promoted : true } ,
228254 } as any ) ;
229- await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
255+
256+ // Wait for the response with retry logic
257+ await waitFor (
258+ ( ) => followerMessages ,
259+ ( msgs ) => msgs . some ( ( m ) => m . id === 1 && m . result ?. promoted === true ) ,
260+ { timeout : 2000 , message : "Expected response from promoted leader not received" }
261+ ) ;
230262
231263 expect ( followerMessages ) . toContainEqual ( expect . objectContaining ( { id : 1 , result : { promoted : true } } ) ) ;
232264
@@ -518,6 +550,52 @@ describe("MultiTabWorkerBroker", () => {
518550 await broker2 . stop ( ) ;
519551 } ) ;
520552
553+ it ( "should let a follower send messages after leader restarts within same tab" , async ( ) => {
554+ const lockName = getLockName ( ) ;
555+
556+ // Tab A: start, stop (without awaiting), and start again (React Strict Mode pattern)
557+ const leader = new MultiTabWorkerBroker ( lockName , makeWorker ) ;
558+ await leader . start ( ) ;
559+ const stopPromise = leader . stop ( ) ;
560+ await leader . start ( ) ;
561+
562+ expect ( leader . isLeader ) . toBe ( true ) ;
563+
564+ // Tab B: start as follower
565+ const follower = new MultiTabWorkerBroker ( lockName , makeWorker , { timeout : 50 } ) ;
566+ await follower . start ( ) ;
567+ expect ( follower . isLeader ) . toBe ( false ) ;
568+
569+ const followerMessages : any [ ] = [ ] ;
570+ const followerConnection = follower . createConnection ( ) ;
571+ followerConnection . reader . listen ( ( msg ) => followerMessages . push ( msg ) ) ;
572+
573+ // First request from follower should succeed (currently fails with "Broker stopped" due to concurrent stop cleanup)
574+ const writePromise = followerConnection . writer . write ( {
575+ jsonrpc : "2.0" ,
576+ id : 1 ,
577+ method : "echo" ,
578+ params : { tab : "follower" } ,
579+ } as any ) ;
580+
581+ try {
582+ await expect ( writePromise ) . resolves . toBeUndefined ( ) ;
583+
584+ // Wait for response delivery
585+ await waitFor (
586+ ( ) => followerMessages ,
587+ ( msgs ) => msgs . some ( ( msg ) => msg . id === 1 && msg . result ?. tab === "follower" ) ,
588+ { timeout : 2000 , message : "Follower did not receive response after leader restart" }
589+ ) ;
590+
591+ expect ( followerMessages ) . toContainEqual ( expect . objectContaining ( { id : 1 , result : { tab : "follower" } } ) ) ;
592+ } finally {
593+ await follower . stop ( ) ;
594+ await leader . stop ( ) ;
595+ await Promise . race ( [ stopPromise , new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) ) ] ) ;
596+ }
597+ } ) ;
598+
521599 it ( "should timeout when no leader available" , async ( ) => {
522600 const lockName = getLockName ( ) ;
523601 // Use a shorter timeout (100ms) for faster test execution
@@ -1022,10 +1100,14 @@ describe("MultiTabWorkerBroker", () => {
10221100
10231101 const leader = new MultiTabWorkerBroker ( lockName , async ( ) => {
10241102 // Delay creating the worker to widen the window for queued requests
1025- await new Promise ( ( r ) => setTimeout ( r , 100 ) ) ;
1103+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
1104+ return await makeSlowWorker ( ) ;
1105+ } ) ;
1106+ const follower = new MultiTabWorkerBroker ( lockName , async ( ) => {
1107+ // Also delay follower's worker creation to test queuing
1108+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
10261109 return await makeSlowWorker ( ) ;
10271110 } ) ;
1028- const follower = new MultiTabWorkerBroker ( lockName , makeSlowWorker ) ;
10291111
10301112 const followerMsgs : any [ ] = [ ] ;
10311113 const followerConn = follower . createConnection ( ) ;
@@ -1042,15 +1124,28 @@ describe("MultiTabWorkerBroker", () => {
10421124 // Force leader to release so follower acquires lock and begins booting
10431125 const stopPromise = leader . stop ( ) ;
10441126 // Small delay to ensure follower's lock wait proceeds
1045- await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
1127+ await new Promise ( ( r ) => setTimeout ( r , 20 ) ) ;
10461128 // Now, before new worker is ready, send a couple of requests
10471129 const p1 = followerConn . writer . write ( { jsonrpc : "2.0" , id : 1 , method : "echo" , params : { during : 1 } } as any ) ;
10481130 const p2 = followerConn . writer . write ( { jsonrpc : "2.0" , id : 2 , method : "echo" , params : { during : 2 } } as any ) ;
10491131 await stopPromise ;
10501132
10511133 // Give time for follower to acquire lock and broker to create worker, then for queue to drain
10521134 await Promise . all ( [ p1 , p2 ] ) ;
1053- await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
1135+
1136+ // Wait for follower to become leader (needs more time due to 200ms worker delay)
1137+ await waitFor (
1138+ ( ) => follower . isLeader ,
1139+ ( isLeader ) => isLeader === true ,
1140+ { timeout : 3000 , message : "Follower did not become leader" }
1141+ ) ;
1142+
1143+ // Wait for both responses with retry logic (allow more time for queued messages to process)
1144+ await waitFor (
1145+ ( ) => followerMsgs ,
1146+ ( msgs ) => msgs . some ( ( m ) => m . id === 1 && m . result ?. during === 1 ) && msgs . some ( ( m ) => m . id === 2 && m . result ?. during === 2 ) ,
1147+ { timeout : 3000 , message : "Expected responses not received during leader boot" }
1148+ ) ;
10541149
10551150 expect ( follower . isLeader ) . toBe ( true ) ;
10561151 // Both responses should arrive even though they were sent while leader was booting
0 commit comments