@@ -18,6 +18,14 @@ type Options = {
18
18
*/
19
19
mode : "short-lived" | "long-lived" ;
20
20
21
+ /**
22
+ * The default TTL of long-lived cache entries.
23
+ * When no revalidate is provided, the default age will be used.
24
+ *
25
+ * @default `THIRTY_MINUTES_IN_SECONDS`
26
+ */
27
+ defaultLongLivedTtlSec ?: number ;
28
+
21
29
/**
22
30
* Whether the regional cache entry should be updated in the background or not when it experiences
23
31
* a cache hit.
@@ -27,6 +35,12 @@ type Options = {
27
35
shouldLazilyUpdateOnCacheHit ?: boolean ;
28
36
} ;
29
37
38
+ interface PutToCacheInput {
39
+ key : string ;
40
+ isFetch : boolean | undefined ;
41
+ entry : IncrementalCacheEntry < boolean > ;
42
+ }
43
+
30
44
/**
31
45
* Wrapper adding a regional cache on an `IncrementalCache` implementation
32
46
*/
@@ -52,10 +66,10 @@ class RegionalCache implements IncrementalCache {
52
66
) : Promise < WithLastModified < CacheValue < IsFetch > > | null > {
53
67
try {
54
68
const cache = await this . getCacheInstance ( ) ;
55
- const localCacheKey = this . getCacheKey ( key , isFetch ) ;
69
+ const urlKey = this . getCacheUrlKey ( key , isFetch ) ;
56
70
57
71
// Check for a cached entry as this will be faster than the store response.
58
- const cachedResponse = await cache . match ( localCacheKey ) ;
72
+ const cachedResponse = await cache . match ( urlKey ) ;
59
73
if ( cachedResponse ) {
60
74
debugCache ( "Get - cached response" ) ;
61
75
@@ -66,7 +80,7 @@ class RegionalCache implements IncrementalCache {
66
80
const { value, lastModified } = rawEntry ?? { } ;
67
81
68
82
if ( value && typeof lastModified === "number" ) {
69
- await this . putToCache ( localCacheKey , { value, lastModified } ) ;
83
+ await this . putToCache ( { key , isFetch , entry : { value, lastModified } } ) ;
70
84
}
71
85
} )
72
86
) ;
@@ -80,7 +94,7 @@ class RegionalCache implements IncrementalCache {
80
94
if ( ! value || typeof lastModified !== "number" ) return null ;
81
95
82
96
// Update the locale cache after retrieving from the store.
83
- getCloudflareContext ( ) . ctx . waitUntil ( this . putToCache ( localCacheKey , { value, lastModified } ) ) ;
97
+ getCloudflareContext ( ) . ctx . waitUntil ( this . putToCache ( { key , isFetch , entry : { value, lastModified } } ) ) ;
84
98
85
99
return { value, lastModified } ;
86
100
} catch ( e ) {
@@ -97,11 +111,15 @@ class RegionalCache implements IncrementalCache {
97
111
try {
98
112
await this . store . set ( key , value , isFetch ) ;
99
113
100
- await this . putToCache ( this . getCacheKey ( key , isFetch ) , {
101
- value,
102
- // Note: `Date.now()` returns the time of the last IO rather than the actual time.
103
- // See https://developers.cloudflare.com/workers/reference/security-model/
104
- lastModified : Date . now ( ) ,
114
+ await this . putToCache ( {
115
+ key,
116
+ isFetch,
117
+ entry : {
118
+ value,
119
+ // Note: `Date.now()` returns the time of the last IO rather than the actual time.
120
+ // See https://developers.cloudflare.com/workers/reference/security-model/
121
+ lastModified : Date . now ( ) ,
122
+ } ,
105
123
} ) ;
106
124
} catch ( e ) {
107
125
error ( `Failed to get from regional cache` , e ) ;
@@ -113,7 +131,7 @@ class RegionalCache implements IncrementalCache {
113
131
await this . store . delete ( key ) ;
114
132
115
133
const cache = await this . getCacheInstance ( ) ;
116
- await cache . delete ( this . getCacheKey ( key ) ) ;
134
+ await cache . delete ( this . getCacheUrlKey ( key ) ) ;
117
135
} catch ( e ) {
118
136
error ( "Failed to delete from regional cache" , e ) ;
119
137
}
@@ -126,27 +144,36 @@ class RegionalCache implements IncrementalCache {
126
144
return this . localCache ;
127
145
}
128
146
129
- protected getCacheKey ( key : string , isFetch ?: boolean ) {
130
- return new Request (
131
- new URL (
132
- `${ process . env . NEXT_BUILD_ID ?? FALLBACK_BUILD_ID } /${ key } .${ isFetch ? "fetch" : "cache" } ` ,
133
- "http://cache.local"
134
- )
147
+ protected getCacheUrlKey ( key : string , isFetch ?: boolean ) {
148
+ const buildId = process . env . NEXT_BUILD_ID ?? FALLBACK_BUILD_ID ;
149
+ return (
150
+ "http://cache.local" + `/${ buildId } /${ key } ` . replace ( / \/ + / g, "/" ) + `.${ isFetch ? "fetch" : "cache" } `
135
151
) ;
136
152
}
137
153
138
- protected async putToCache ( key : Request , entry : IncrementalCacheEntry < boolean > ) : Promise < void > {
154
+ protected async putToCache ( { key, isFetch, entry } : PutToCacheInput ) : Promise < void > {
155
+ const urlKey = this . getCacheUrlKey ( key , isFetch ) ;
139
156
const cache = await this . getCacheInstance ( ) ;
140
157
141
158
const age =
142
159
this . opts . mode === "short-lived"
143
160
? ONE_MINUTE_IN_SECONDS
144
- : entry . value . revalidate || THIRTY_MINUTES_IN_SECONDS ;
161
+ : entry . value . revalidate || this . opts . defaultLongLivedTtlSec || THIRTY_MINUTES_IN_SECONDS ;
145
162
163
+ // We default to the entry key if no tags are found.
164
+ // so that we can also revalidate page router based entry this way.
165
+ const tags = getTagsFromCacheEntry ( entry ) ?? [ key ] ;
146
166
await cache . put (
147
- key ,
167
+ urlKey ,
148
168
new Response ( JSON . stringify ( entry ) , {
149
- headers : new Headers ( { "cache-control" : `max-age=${ age } ` } ) ,
169
+ headers : new Headers ( {
170
+ "cache-control" : `max-age=${ age } ` ,
171
+ ...( tags . length > 0
172
+ ? {
173
+ "cache-tag" : tags . join ( "," ) ,
174
+ }
175
+ : { } ) ,
176
+ } ) ,
150
177
} )
151
178
) ;
152
179
}
@@ -169,9 +196,33 @@ class RegionalCache implements IncrementalCache {
169
196
* or an ISR/SSG entry for up to 30 minutes.
170
197
* @param opts.shouldLazilyUpdateOnCacheHit Whether the regional cache entry should be updated in
171
198
* the background or not when it experiences a cache hit.
199
+ * @param opts.defaultLongLivedTtlSec The default age to use for long-lived cache entries.
200
+ * When no revalidate is provided, the default age will be used.
201
+ * @default `THIRTY_MINUTES_IN_SECONDS`
172
202
*
173
203
* @default `false` for the `short-lived` mode, and `true` for the `long-lived` mode.
174
204
*/
175
205
export function withRegionalCache ( cache : IncrementalCache , opts : Options ) {
176
206
return new RegionalCache ( cache , opts ) ;
177
207
}
208
+
209
+ /**
210
+ * Extract the list of tags from a cache entry.
211
+ */
212
+ function getTagsFromCacheEntry ( entry : IncrementalCacheEntry < boolean > ) : string [ ] | undefined {
213
+ if ( "tags" in entry . value && entry . value . tags ) {
214
+ return entry . value . tags ;
215
+ }
216
+
217
+ if (
218
+ "meta" in entry . value &&
219
+ entry . value . meta &&
220
+ "headers" in entry . value . meta &&
221
+ entry . value . meta . headers
222
+ ) {
223
+ const rawTags = entry . value . meta . headers [ "x-next-cache-tags" ] ;
224
+ if ( typeof rawTags === "string" ) {
225
+ return rawTags . split ( "," ) ;
226
+ }
227
+ }
228
+ }
0 commit comments