@@ -11,47 +11,55 @@ import { getLogger, getRequestContext } from './request-context.cjs'
1111
1212const  purgeCacheUserAgent  =  `${ nextRuntimePkgName } ${ nextRuntimePkgVersion }  
1313
14- /** 
15-  * Get timestamp of the last revalidation for a tag 
16-  */ 
17- async  function  getTagRevalidatedAt ( 
14+ async  function  getTagManifest ( 
1815  tag : string , 
1916  cacheStore : MemoizedKeyValueStoreBackedByRegionalBlobStore , 
20- ) : Promise < number  |  null >  { 
17+ ) : Promise < TagManifest  |  null >  { 
2118  const  tagManifest  =  await  cacheStore . get < TagManifest > ( tag ,  'tagManifest.get' ) 
2219  if  ( ! tagManifest )  { 
2320    return  null 
2421  } 
25-   return  tagManifest . revalidatedAt 
22+   return  tagManifest 
2623} 
2724
2825/** 
2926 * Get the most recent revalidation timestamp for a list of tags 
3027 */ 
31- export  async  function  getMostRecentTagRevalidationTimestamp ( tags : string [ ] )  { 
28+ export  async  function  getMostRecentTagExpirationTimestamp ( tags : string [ ] )  { 
3229  if  ( tags . length  ===  0 )  { 
3330    return  0 
3431  } 
3532
3633  const  cacheStore  =  getMemoizedKeyValueStoreBackedByRegionalBlobStore ( {  consistency : 'strong'  } ) 
3734
38-   const  timestampsOrNulls  =  await  Promise . all ( 
39-     tags . map ( ( tag )  =>  getTagRevalidatedAt ( tag ,  cacheStore ) ) , 
40-   ) 
35+   const  timestampsOrNulls  =  await  Promise . all ( tags . map ( ( tag )  =>  getTagManifest ( tag ,  cacheStore ) ) ) 
4136
42-   const  timestamps  =  timestampsOrNulls . filter ( ( timestamp )  =>  timestamp  !==  null ) 
43-   if  ( timestamps . length  ===  0 )  { 
37+   const  expirationTimestamps  =  timestampsOrNulls 
38+     . filter ( ( timestamp )  =>  timestamp  !==  null ) 
39+     . map ( ( manifest )  =>  manifest . expiredAt ) 
40+   if  ( expirationTimestamps . length  ===  0 )  { 
4441    return  0 
4542  } 
46-   return  Math . max ( ...timestamps ) 
43+   return  Math . max ( ...expirationTimestamps ) 
4744} 
4845
46+ export  type  TagStaleOrExpired  = 
47+   // FRESH 
48+   |  {  stale : false ;  expired : false  } 
49+   // STALE 
50+   |  {  stale : true ;  expired : false ;  expireAt : number  } 
51+   // EXPIRED (should be treated similarly to MISS) 
52+   |  {  stale : true ;  expired : true  } 
53+ 
4954/** 
50-  * Check if any of the tags were invalidated  since the given timestamp 
55+  * Check if any of the tags expired  since the given timestamp 
5156 */ 
52- export  function  isAnyTagStale ( tags : string [ ] ,  timestamp : number ) : Promise < boolean >  { 
57+ export  function  isAnyTagStaleOrExpired ( 
58+   tags : string [ ] , 
59+   timestamp : number , 
60+ ) : Promise < TagStaleOrExpired >  { 
5361  if  ( tags . length  ===  0  ||  ! timestamp )  { 
54-     return  Promise . resolve ( false ) 
62+     return  Promise . resolve ( {   stale :  false ,   expired :  false   } ) 
5563  } 
5664
5765  const  cacheStore  =  getMemoizedKeyValueStoreBackedByRegionalBlobStore ( {  consistency : 'strong'  } ) 
@@ -60,37 +68,74 @@ export function isAnyTagStale(tags: string[], timestamp: number): Promise<boolea
6068  //    but we will only do actual blob read once withing a single request due to cacheStore 
6169  //    memoization. 
6270  //    Additionally, we will resolve the promise as soon as we find first 
63-   //    stale  tag, so that we don't wait for all of them to resolve (but keep all 
71+   //    expired  tag, so that we don't wait for all of them to resolve (but keep all 
6472  //    running in case future `CacheHandler.get` calls would be able to use results). 
65-   //    "Worst case" scenario is none of tag was invalidated  in which case we need to wait 
66-   //    for all blob store checks to finish before we can be certain that no tag is stale . 
67-   return  new  Promise < boolean > ( ( resolve ,  reject )  =>  { 
68-     const  tagManifestPromises : Promise < boolean > [ ]  =  [ ] 
73+   //    "Worst case" scenario is none of tag was expired  in which case we need to wait 
74+   //    for all blob store checks to finish before we can be certain that no tag is expired . 
75+   return  new  Promise < TagStaleOrExpired > ( ( resolve ,  reject )  =>  { 
76+     const  tagManifestPromises : Promise < TagStaleOrExpired > [ ]  =  [ ] 
6977
7078    for  ( const  tag  of  tags )  { 
71-       const  lastRevalidationTimestampPromise  =  getTagRevalidatedAt ( tag ,  cacheStore ) 
79+       const  tagManifestPromise  =  getTagManifest ( tag ,  cacheStore ) 
7280
7381      tagManifestPromises . push ( 
74-         lastRevalidationTimestampPromise . then ( ( lastRevalidationTimestamp )  =>  { 
75-           if  ( ! lastRevalidationTimestamp )  { 
82+         tagManifestPromise . then ( ( tagManifest )  =>  { 
83+           if  ( ! tagManifest )  { 
7684            // tag was never revalidated 
77-             return  false 
85+             return  {  stale : false ,  expired : false  } 
86+           } 
87+           const  stale  =  tagManifest . staleAt  >=  timestamp 
88+           const  expired  =  tagManifest . expiredAt  >=  timestamp  &&  tagManifest . expiredAt  <=  Date . now ( ) 
89+ 
90+           if  ( expired  &&  stale )  { 
91+             const  expiredResult : TagStaleOrExpired  =  { 
92+               stale, 
93+               expired, 
94+             } 
95+             // resolve outer promise immediately if any of the tags is expired 
96+             resolve ( expiredResult ) 
97+             return  expiredResult 
7898          } 
79-           const  isStale  =  lastRevalidationTimestamp  >=  timestamp 
80-           if  ( isStale )  { 
81-             // resolve outer promise immediately if any of the tags is stale 
82-             resolve ( true ) 
83-             return  true 
99+ 
100+           if  ( stale )  { 
101+             const  staleResult : TagStaleOrExpired  =  { 
102+               stale, 
103+               expired, 
104+               expireAt : tagManifest . expiredAt , 
105+             } 
106+             return  staleResult 
84107          } 
85-           return  false 
108+           return  {   stale :  false ,   expired :  false   } 
86109        } ) , 
87110      ) 
88111    } 
89112
90-     // make sure we resolve promise after all blobs are checked (if we didn't resolve as stale  yet) 
113+     // make sure we resolve promise after all blobs are checked (if we didn't resolve as expired  yet) 
91114    Promise . all ( tagManifestPromises ) 
92-       . then ( ( tagManifestAreStale )  =>  { 
93-         resolve ( tagManifestAreStale . some ( ( tagIsStale )  =>  tagIsStale ) ) 
115+       . then ( ( tagManifestsAreStaleOrExpired )  =>  { 
116+         let  result : TagStaleOrExpired  =  {  stale : false ,  expired : false  } 
117+ 
118+         for  ( const  tagResult  of  tagManifestsAreStaleOrExpired )  { 
119+           if  ( tagResult . expired )  { 
120+             // if any of the tags is expired, the whole thing is expired 
121+             result  =  tagResult 
122+             break 
123+           } 
124+ 
125+           if  ( tagResult . stale )  { 
126+             result  =  { 
127+               stale : true , 
128+               expired : false , 
129+               expireAt :
130+                 // make sure to use expireAt that is lowest of all tags 
131+                 result . stale  &&  ! result . expired  &&  typeof  result . expireAt  ===  'number' 
132+                   ? Math . min ( result . expireAt ,  tagResult . expireAt ) 
133+                   : tagResult . expireAt , 
134+             } 
135+           } 
136+         } 
137+ 
138+         resolve ( result ) 
94139      } ) 
95140      . catch ( reject ) 
96141  } ) 
@@ -122,15 +167,21 @@ export function purgeEdgeCache(tagOrTags: string | string[]): Promise<void> {
122167  } ) 
123168} 
124169
125- async  function  doRevalidateTagAndPurgeEdgeCache ( tags : string [ ] ) : Promise < void >  { 
126-   getLogger ( ) . withFields ( {  tags } ) . debug ( 'doRevalidateTagAndPurgeEdgeCache' ) 
170+ async  function  doRevalidateTagAndPurgeEdgeCache ( 
171+   tags : string [ ] , 
172+   durations ?: {  expire ?: number  } , 
173+ ) : Promise < void >  { 
174+   getLogger ( ) . withFields ( {  tags,  durations } ) . debug ( 'doRevalidateTagAndPurgeEdgeCache' ) 
127175
128176  if  ( tags . length  ===  0 )  { 
129177    return 
130178  } 
131179
180+   const  now  =  Date . now ( ) 
181+ 
132182  const  tagManifest : TagManifest  =  { 
133-     revalidatedAt : Date . now ( ) , 
183+     staleAt : now , 
184+     expiredAt : now  +  ( durations ?. expire  ? durations . expire  *  1000  : 0 ) , 
134185  } 
135186
136187  const  cacheStore  =  getMemoizedKeyValueStoreBackedByRegionalBlobStore ( {  consistency : 'strong'  } ) 
@@ -148,10 +199,13 @@ async function doRevalidateTagAndPurgeEdgeCache(tags: string[]): Promise<void> {
148199  await  purgeEdgeCache ( tags ) 
149200} 
150201
151- export  function  markTagsAsStaleAndPurgeEdgeCache ( tagOrTags : string  |  string [ ] )  { 
202+ export  function  markTagsAsStaleAndPurgeEdgeCache ( 
203+   tagOrTags : string  |  string [ ] , 
204+   durations ?: {  expire ?: number  } , 
205+ )  { 
152206  const  tags  =  getCacheTagsFromTagOrTags ( tagOrTags ) 
153207
154-   const  revalidateTagPromise  =  doRevalidateTagAndPurgeEdgeCache ( tags ) 
208+   const  revalidateTagPromise  =  doRevalidateTagAndPurgeEdgeCache ( tags ,   durations ) 
155209
156210  const  requestContext  =  getRequestContext ( ) 
157211  if  ( requestContext )  { 
0 commit comments