Skip to content

Commit 8841641

Browse files
authored
Set "cache-tag" on the regional cache entries (#551)
1 parent 930ab83 commit 8841641

File tree

2 files changed

+76
-20
lines changed

2 files changed

+76
-20
lines changed

.changeset/happy-bananas-buy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/cloudflare": patch
3+
---
4+
5+
Set `Cache-Tag` on the entries created by the regional cache, to be purged using the Cloudflare API or dashboard.

packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ type Options = {
1818
*/
1919
mode: "short-lived" | "long-lived";
2020

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+
2129
/**
2230
* Whether the regional cache entry should be updated in the background or not when it experiences
2331
* a cache hit.
@@ -27,6 +35,12 @@ type Options = {
2735
shouldLazilyUpdateOnCacheHit?: boolean;
2836
};
2937

38+
interface PutToCacheInput {
39+
key: string;
40+
isFetch: boolean | undefined;
41+
entry: IncrementalCacheEntry<boolean>;
42+
}
43+
3044
/**
3145
* Wrapper adding a regional cache on an `IncrementalCache` implementation
3246
*/
@@ -52,10 +66,10 @@ class RegionalCache implements IncrementalCache {
5266
): Promise<WithLastModified<CacheValue<IsFetch>> | null> {
5367
try {
5468
const cache = await this.getCacheInstance();
55-
const localCacheKey = this.getCacheKey(key, isFetch);
69+
const urlKey = this.getCacheUrlKey(key, isFetch);
5670

5771
// 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);
5973
if (cachedResponse) {
6074
debugCache("Get - cached response");
6175

@@ -66,7 +80,7 @@ class RegionalCache implements IncrementalCache {
6680
const { value, lastModified } = rawEntry ?? {};
6781

6882
if (value && typeof lastModified === "number") {
69-
await this.putToCache(localCacheKey, { value, lastModified });
83+
await this.putToCache({ key, isFetch, entry: { value, lastModified } });
7084
}
7185
})
7286
);
@@ -80,7 +94,7 @@ class RegionalCache implements IncrementalCache {
8094
if (!value || typeof lastModified !== "number") return null;
8195

8296
// 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 } }));
8498

8599
return { value, lastModified };
86100
} catch (e) {
@@ -97,11 +111,15 @@ class RegionalCache implements IncrementalCache {
97111
try {
98112
await this.store.set(key, value, isFetch);
99113

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+
},
105123
});
106124
} catch (e) {
107125
error(`Failed to get from regional cache`, e);
@@ -113,7 +131,7 @@ class RegionalCache implements IncrementalCache {
113131
await this.store.delete(key);
114132

115133
const cache = await this.getCacheInstance();
116-
await cache.delete(this.getCacheKey(key));
134+
await cache.delete(this.getCacheUrlKey(key));
117135
} catch (e) {
118136
error("Failed to delete from regional cache", e);
119137
}
@@ -126,27 +144,36 @@ class RegionalCache implements IncrementalCache {
126144
return this.localCache;
127145
}
128146

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"}`
135151
);
136152
}
137153

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);
139156
const cache = await this.getCacheInstance();
140157

141158
const age =
142159
this.opts.mode === "short-lived"
143160
? ONE_MINUTE_IN_SECONDS
144-
: entry.value.revalidate || THIRTY_MINUTES_IN_SECONDS;
161+
: entry.value.revalidate || this.opts.defaultLongLivedTtlSec || THIRTY_MINUTES_IN_SECONDS;
145162

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];
146166
await cache.put(
147-
key,
167+
urlKey,
148168
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+
}),
150177
})
151178
);
152179
}
@@ -169,9 +196,33 @@ class RegionalCache implements IncrementalCache {
169196
* or an ISR/SSG entry for up to 30 minutes.
170197
* @param opts.shouldLazilyUpdateOnCacheHit Whether the regional cache entry should be updated in
171198
* 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`
172202
*
173203
* @default `false` for the `short-lived` mode, and `true` for the `long-lived` mode.
174204
*/
175205
export function withRegionalCache(cache: IncrementalCache, opts: Options) {
176206
return new RegionalCache(cache, opts);
177207
}
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

Comments
 (0)