@@ -36,7 +36,7 @@ describe('runDetached', () => {
3636
3737describe ( 'createSingleFlight' , ( ) => {
3838 it ( 'starts work and reports active while in flight' , async ( ) => {
39- const guard = createSingleFlight ( )
39+ const guard = createSingleFlight ( { staleAfterMs : 60_000 } )
4040 let release : ( ) => void = ( ) => { }
4141 const gate = new Promise < void > ( ( resolve ) => {
4242 release = resolve
@@ -52,7 +52,7 @@ describe('createSingleFlight', () => {
5252 } )
5353
5454 it ( 'refuses a second run while one is already in flight' , async ( ) => {
55- const guard = createSingleFlight ( )
55+ const guard = createSingleFlight ( { staleAfterMs : 60_000 } )
5656 let release : ( ) => void = ( ) => { }
5757 const gate = new Promise < void > ( ( resolve ) => {
5858 release = resolve
@@ -67,10 +67,54 @@ describe('createSingleFlight', () => {
6767 } )
6868
6969 it ( 'clears the active flag even when work rejects' , async ( ) => {
70- const guard = createSingleFlight ( )
70+ const guard = createSingleFlight ( { staleAfterMs : 60_000 } )
7171
7272 expect ( guard . run ( 'task' , ( ) => Promise . reject ( new Error ( 'boom' ) ) ) ) . toBe ( true )
7373 await flushMicrotasks ( )
7474 expect ( guard . isActive ( ) ) . toBe ( false )
7575 } )
76+
77+ it ( 'takes over a stale run whose work never settles' , async ( ) => {
78+ const guard = createSingleFlight ( { staleAfterMs : 10 } )
79+
80+ // A run whose promise never settles — its `finally` never fires.
81+ expect ( guard . run ( 'task' , ( ) => new Promise < void > ( ( ) => { } ) ) ) . toBe ( true )
82+ expect ( guard . run ( 'task' , ( ) => Promise . resolve ( ) ) ) . toBe ( false )
83+
84+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) )
85+
86+ const second = vi . fn ( ) . mockResolvedValue ( undefined )
87+ expect ( guard . run ( 'task' , second ) ) . toBe ( true )
88+ await flushMicrotasks ( )
89+ expect ( second ) . toHaveBeenCalledTimes ( 1 )
90+ expect ( guard . isActive ( ) ) . toBe ( false )
91+ } )
92+
93+ it ( 'does not let a late stale run clear a newer run slot' , async ( ) => {
94+ const guard = createSingleFlight ( { staleAfterMs : 10 } )
95+
96+ let releaseStale : ( ) => void = ( ) => { }
97+ const stale = new Promise < void > ( ( resolve ) => {
98+ releaseStale = resolve
99+ } )
100+ expect ( guard . run ( 'task' , ( ) => stale ) ) . toBe ( true )
101+
102+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) )
103+
104+ // New run takes over the stale slot.
105+ let releaseFresh : ( ) => void = ( ) => { }
106+ const fresh = new Promise < void > ( ( resolve ) => {
107+ releaseFresh = resolve
108+ } )
109+ expect ( guard . run ( 'task' , ( ) => fresh ) ) . toBe ( true )
110+
111+ // The original stale run settling late must not release the newer slot.
112+ releaseStale ( )
113+ await flushMicrotasks ( )
114+ expect ( guard . isActive ( ) ) . toBe ( true )
115+
116+ releaseFresh ( )
117+ await flushMicrotasks ( )
118+ expect ( guard . isActive ( ) ) . toBe ( false )
119+ } )
76120} )
0 commit comments