@@ -313,6 +313,107 @@ describe("cachedFunction", () => {
313313 errorSpy . mockRestore ( ) ;
314314 } ) ;
315315
316+ it ( "SWR with staleMaxAge serves stale within window then expires" , async ( ) => {
317+ let callCount = 0 ;
318+ const fn = defineCachedFunction (
319+ async ( ) => {
320+ callCount ++ ;
321+ await new Promise ( ( r ) => setTimeout ( r , 5 ) ) ;
322+ return `v${ callCount } ` ;
323+ } ,
324+ { maxAge : 0.01 , swr : true , staleMaxAge : 0.02 } ,
325+ ) ;
326+
327+ // Initial call
328+ expect ( await fn ( ) ) . toBe ( "v1" ) ;
329+ expect ( callCount ) . toBe ( 1 ) ;
330+
331+ // Wait for maxAge to expire but within staleMaxAge window
332+ await new Promise ( ( r ) => setTimeout ( r , 15 ) ) ;
333+ // SWR should return stale value while revalidating in background
334+ const r2 = await fn ( ) ;
335+ expect ( r2 ) . toBe ( "v1" ) ; // stale value served
336+ expect ( callCount ) . toBe ( 2 ) ; // resolver triggered in background
337+
338+ // Wait for background resolve to finish
339+ await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
340+
341+ // Wait for both maxAge + staleMaxAge to fully expire
342+ await new Promise ( ( r ) => setTimeout ( r , 40 ) ) ;
343+ // Now entry is fully expired — SWR should NOT serve stale, must await fresh value
344+ const r3 = await fn ( ) ;
345+ expect ( r3 ) . toBe ( "v3" ) ;
346+ expect ( callCount ) . toBe ( 3 ) ;
347+ } ) ;
348+
349+ it ( "SWR without staleMaxAge serves stale indefinitely" , async ( ) => {
350+ let callCount = 0 ;
351+ const fn = defineCachedFunction (
352+ async ( ) => {
353+ callCount ++ ;
354+ await new Promise ( ( r ) => setTimeout ( r , 5 ) ) ;
355+ return `v${ callCount } ` ;
356+ } ,
357+ { maxAge : 0.01 , swr : true } ,
358+ ) ;
359+
360+ expect ( await fn ( ) ) . toBe ( "v1" ) ;
361+ await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
362+ // Even after long time, SWR without staleMaxAge should still serve stale
363+ const r2 = await fn ( ) ;
364+ expect ( r2 ) . toBe ( "v1" ) ; // stale value
365+ expect ( callCount ) . toBe ( 2 ) ; // revalidating in background
366+ } ) ;
367+
368+ it ( "sets storage TTL to maxAge + staleMaxAge when SWR with staleMaxAge" , async ( ) => {
369+ const setSpy = vi . fn ( ) ;
370+ setStorage ( {
371+ get : ( ) => null ,
372+ set : setSpy ,
373+ } ) ;
374+
375+ const fn = defineCachedFunction ( ( ) => "value" , {
376+ maxAge : 60 ,
377+ swr : true ,
378+ staleMaxAge : 120 ,
379+ } ) ;
380+ await fn ( ) ;
381+ expect ( setSpy ) . toHaveBeenCalledWith ( expect . any ( String ) , expect . any ( Object ) , { ttl : 180 } ) ;
382+ } ) ;
383+
384+ it ( "does not set storage TTL when SWR without staleMaxAge" , async ( ) => {
385+ const setSpy = vi . fn ( ) ;
386+ setStorage ( {
387+ get : ( ) => null ,
388+ set : setSpy ,
389+ } ) ;
390+
391+ const fn = defineCachedFunction ( ( ) => "value" , {
392+ maxAge : 60 ,
393+ swr : true ,
394+ } ) ;
395+ await fn ( ) ;
396+ expect ( setSpy ) . toHaveBeenCalledWith ( expect . any ( String ) , expect . any ( Object ) , undefined ) ;
397+ } ) ;
398+
399+ it ( "SWR with staleMaxAge: 0 never serves stale" , async ( ) => {
400+ let callCount = 0 ;
401+ const fn = defineCachedFunction (
402+ async ( ) => {
403+ callCount ++ ;
404+ return `v${ callCount } ` ;
405+ } ,
406+ { maxAge : 0.01 , swr : true , staleMaxAge : 0 } ,
407+ ) ;
408+
409+ expect ( await fn ( ) ) . toBe ( "v1" ) ;
410+ await new Promise ( ( r ) => setTimeout ( r , 20 ) ) ;
411+ // staleMaxAge: 0 means the stale window is zero — entry is fully expired
412+ const r2 = await fn ( ) ;
413+ expect ( r2 ) . toBe ( "v2" ) ;
414+ expect ( callCount ) . toBe ( 2 ) ;
415+ } ) ;
416+
316417 it ( "waitUntil is used for SWR background revalidation" , async ( ) => {
317418 const waitUntilFn = vi . fn ( ) ;
318419 let callCount = 0 ;
0 commit comments