-
Notifications
You must be signed in to change notification settings - Fork 118
Expand file tree
/
Copy pathregional-cache.ts
More file actions
264 lines (227 loc) · 8.15 KB
/
regional-cache.ts
File metadata and controls
264 lines (227 loc) · 8.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
import { error } from "@opennextjs/aws/adapters/logger.js";
import {
CacheEntryType,
CacheValue,
IncrementalCache,
WithLastModified,
} from "@opennextjs/aws/types/overrides.js";
import { getCloudflareContext } from "../../cloudflare-context.js";
import { debugCache, FALLBACK_BUILD_ID, IncrementalCacheEntry, isPurgeCacheEnabled } from "../internal.js";
const ONE_MINUTE_IN_SECONDS = 60;
const THIRTY_MINUTES_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 30;
type Options = {
/**
* The mode to use for the regional cache.
*
* - `short-lived`: Re-use a cache entry for up to a minute after it has been retrieved.
* - `long-lived`: Re-use a fetch cache entry until it is revalidated (per-region),
* or an ISR/SSG entry for up to 30 minutes.
*/
mode: "short-lived" | "long-lived";
/**
* The default TTL of long-lived cache entries.
* When no revalidate is provided, the default age will be used.
*
* @default `THIRTY_MINUTES_IN_SECONDS`
*/
defaultLongLivedTtlSec?: number;
/**
* Whether the regional cache entry should be updated in the background or not when it experiences
* a cache hit.
*
* @default `true` in `long-lived` mode when cache purge is not used, `false` otherwise.
*/
shouldLazilyUpdateOnCacheHit?: boolean;
/**
* Whether on cache hits the tagCache should be skipped or not. Skipping the tagCache allows requests to be
* handled faster,
*
* Note: When this is enabled, make sure that the cache gets purged
* either by enabling the auto cache purging feature or manually.
*
* @default `true` if the auto cache purging is enabled, `false` otherwise.
*/
bypassTagCacheOnCacheHit?: boolean;
};
interface PutToCacheInput {
key: string;
cacheType?: CacheEntryType;
entry: IncrementalCacheEntry<CacheEntryType>;
}
/**
* Wrapper adding a regional cache on an `IncrementalCache` implementation.
*
* Using a the `RegionalCache` does not directly improves the performance much.
* However it allows bypassing the tag cache (see `bypassTagCacheOnCacheHit`) on hits.
* That's where bigger perf gain happens.
*
* We recommend using cache purge.
* When cache purge is not enabled, there is a possibility that the Cache API (local to a Data Center)
* is out of sync with the cache store (i.e. R2). That's why when cache purge is not enabled the Cache
* API is refreshed from the cache store on cache hits (for the long-lived mode).
*/
class RegionalCache implements IncrementalCache {
public name: string;
protected localCache: Cache | undefined;
constructor(
private store: IncrementalCache,
private opts: Options
) {
this.name = this.store.name;
// `shouldLazilyUpdateOnCacheHit` is not needed when cache purge is enabled.
this.opts.shouldLazilyUpdateOnCacheHit ??= this.opts.mode === "long-lived" && !isPurgeCacheEnabled();
}
get #bypassTagCacheOnCacheHit(): boolean {
if (this.opts.bypassTagCacheOnCacheHit !== undefined) {
return this.opts.bypassTagCacheOnCacheHit;
}
// When `bypassTagCacheOnCacheHit` is not set, we default to whether the automatic cache purging is enabled or not
return isPurgeCacheEnabled();
}
async get<CacheType extends CacheEntryType = "cache">(
key: string,
cacheType?: CacheType
): Promise<WithLastModified<CacheValue<CacheType>> | null> {
try {
const cache = await this.getCacheInstance();
const urlKey = this.getCacheUrlKey(key, cacheType);
// Check for a cached entry as this will be faster than the store response.
const cachedResponse = await cache.match(urlKey);
if (cachedResponse) {
debugCache("RegionalCache", `get ${key} -> cached response`);
// Re-fetch from the store and update the regional cache in the background.
// Note: this is only useful when the Cache API is not purged automatically.
if (this.opts.shouldLazilyUpdateOnCacheHit) {
getCloudflareContext().ctx.waitUntil(
this.store.get(key, cacheType).then(async (rawEntry) => {
const { value, lastModified } = rawEntry ?? {};
if (value && typeof lastModified === "number") {
await this.putToCache({ key, cacheType, entry: { value, lastModified } });
}
})
);
}
const responseJson: Record<string, unknown> = await cachedResponse.json();
return {
...responseJson,
shouldBypassTagCache: this.#bypassTagCacheOnCacheHit,
};
}
const rawEntry = await this.store.get(key, cacheType);
const { value, lastModified } = rawEntry ?? {};
if (!value || typeof lastModified !== "number") return null;
debugCache("RegionalCache", `get ${key} -> put to cache`);
// Update the locale cache after retrieving from the store.
getCloudflareContext().ctx.waitUntil(
this.putToCache({ key, cacheType, entry: { value, lastModified } })
);
return { value, lastModified };
} catch (e) {
error("Failed to get from regional cache", e);
return null;
}
}
async set<CacheType extends CacheEntryType = "cache">(
key: string,
value: CacheValue<CacheType>,
cacheType?: CacheType
): Promise<void> {
try {
debugCache("RegionalCache", `set ${key}`);
await this.store.set(key, value, cacheType);
await this.putToCache({
key,
cacheType,
entry: {
value,
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
// See https://developers.cloudflare.com/workers/reference/security-model/
lastModified: Date.now(),
},
});
} catch (e) {
error(`Failed to set the regional cache`, e);
}
}
async delete(key: string): Promise<void> {
debugCache("RegionalCache", `delete ${key}`);
try {
await this.store.delete(key);
const cache = await this.getCacheInstance();
await cache.delete(this.getCacheUrlKey(key));
} catch (e) {
error("Failed to delete from regional cache", e);
}
}
protected async getCacheInstance(): Promise<Cache> {
if (this.localCache) return this.localCache;
this.localCache = await caches.open("incremental-cache");
return this.localCache;
}
protected getCacheUrlKey(key: string, cacheType?: CacheEntryType): string {
const buildId = process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
return "http://cache.local" + `/${buildId}/${key}`.replace(/\/+/g, "/") + `.${cacheType ?? "cache"}`;
}
protected async putToCache({ key, cacheType, entry }: PutToCacheInput): Promise<void> {
const urlKey = this.getCacheUrlKey(key, cacheType);
const cache = await this.getCacheInstance();
const age =
this.opts.mode === "short-lived"
? ONE_MINUTE_IN_SECONDS
: entry.value.revalidate || this.opts.defaultLongLivedTtlSec || THIRTY_MINUTES_IN_SECONDS;
// We default to the entry key if no tags are found.
// so that we can also revalidate page router based entry this way.
const tags = getTagsFromCacheEntry(entry) ?? [key];
await cache.put(
urlKey,
new Response(JSON.stringify(entry), {
headers: new Headers({
"cache-control": `max-age=${age}`,
...(tags.length > 0
? {
"cache-tag": tags.join(","),
}
: {}),
}),
})
);
}
}
/**
* A regional cache will wrap an incremental cache and provide faster cache lookups for an entry
* when making requests within the region.
*
* The regional cache uses the Cache API.
*
* **WARNING:**
* If an entry is revalidated on demand in one region (using either `revalidateTag`, `revalidatePath` or `res.revalidate` ), it will trigger an additional revalidation if
* a request is made to another region that has an entry stored in its regional cache.
*
* @param cache Incremental cache instance.
* @param opts Options for the regional cache.
*/
export function withRegionalCache(cache: IncrementalCache, opts: Options): RegionalCache {
return new RegionalCache(cache, opts);
}
/**
* Extract the list of tags from a cache entry.
*/
function getTagsFromCacheEntry(entry: IncrementalCacheEntry<CacheEntryType>): string[] | undefined {
if ("tags" in entry.value && entry.value.tags) {
return entry.value.tags;
}
if (
"meta" in entry.value &&
entry.value.meta &&
"headers" in entry.value.meta &&
entry.value.meta.headers
) {
const rawTags = entry.value.meta.headers["x-next-cache-tags"];
if (typeof rawTags === "string") {
return rawTags.split(",");
}
}
if ("value" in entry.value) {
return entry.value.tags;
}
}