@@ -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 manifestsOrNulls = 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 = manifestsOrNulls
38+ . filter ( ( manifest ) => manifest !== null )
39+ . map ( ( manifest ) => manifest . expireAt )
40+ if ( expirationTimestamps . length === 0 ) {
4441 return 0
4542 }
46- return Math . max ( ...timestamps )
43+ return Math . max ( ...expirationTimestamps )
4744}
4845
46+ export type TagStaleOrExpiredStatus =
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 < TagStaleOrExpiredStatus > {
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 < TagStaleOrExpiredStatus > ( ( resolve , reject ) => {
76+ const tagManifestPromises : Promise < TagStaleOrExpiredStatus > [ ] = [ ]
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 }
7886 }
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
87+ const stale = tagManifest . staleAt >= timestamp
88+ const expired = tagManifest . expireAt >= timestamp && tagManifest . expireAt <= Date . now ( )
89+
90+ if ( expired && stale ) {
91+ const expiredResult : TagStaleOrExpiredStatus = {
92+ stale,
93+ expired,
94+ }
95+ // resolve outer promise immediately if any of the tags is expired
96+ resolve ( expiredResult )
97+ return expiredResult
8498 }
85- return false
99+
100+ if ( stale ) {
101+ const staleResult : TagStaleOrExpiredStatus = {
102+ stale,
103+ expired,
104+ expireAt : tagManifest . expireAt ,
105+ }
106+ return staleResult
107+ }
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 : TagStaleOrExpiredStatus = { 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,30 @@ 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+ // shape of this type comes from Next.js https://github.com/vercel/next.js/blob/fffa2831b61fa74852736eeaad2f17fbdd553bce/packages/next/src/server/lib/incremental-cache/index.ts#L78
171+ // and we use it internally
172+ export type RevalidateTagDurations = {
173+ /**
174+ * Number of seconds after which tagged cache entries should no longer serve stale content.
175+ */
176+ expire ?: number
177+ }
178+
179+ async function doRevalidateTagAndPurgeEdgeCache (
180+ tags : string [ ] ,
181+ durations ?: RevalidateTagDurations ,
182+ ) : Promise < void > {
183+ getLogger ( ) . withFields ( { tags, durations } ) . debug ( 'doRevalidateTagAndPurgeEdgeCache' )
127184
128185 if ( tags . length === 0 ) {
129186 return
130187 }
131188
189+ const now = Date . now ( )
190+
132191 const tagManifest : TagManifest = {
133- revalidatedAt : Date . now ( ) ,
192+ staleAt : now ,
193+ expireAt : now + ( durations ?. expire ? durations . expire * 1000 : 0 ) ,
134194 }
135195
136196 const cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore ( { consistency : 'strong' } )
@@ -148,10 +208,13 @@ async function doRevalidateTagAndPurgeEdgeCache(tags: string[]): Promise<void> {
148208 await purgeEdgeCache ( tags )
149209}
150210
151- export function markTagsAsStaleAndPurgeEdgeCache ( tagOrTags : string | string [ ] ) {
211+ export function markTagsAsStaleAndPurgeEdgeCache (
212+ tagOrTags : string | string [ ] ,
213+ durations ?: RevalidateTagDurations ,
214+ ) {
152215 const tags = getCacheTagsFromTagOrTags ( tagOrTags )
153216
154- const revalidateTagPromise = doRevalidateTagAndPurgeEdgeCache ( tags )
217+ const revalidateTagPromise = doRevalidateTagAndPurgeEdgeCache ( tags , durations )
155218
156219 const requestContext = getRequestContext ( )
157220 if ( requestContext ) {
0 commit comments