-
Notifications
You must be signed in to change notification settings - Fork 119
Expand file tree
/
Copy pathregional-cache.ts
More file actions
177 lines (151 loc) · 6.07 KB
/
regional-cache.ts
File metadata and controls
177 lines (151 loc) · 6.07 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
import { error } from "@opennextjs/aws/adapters/logger.js";
import { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js";
import { getCloudflareContext } from "../../cloudflare-context.js";
import { debugCache, FALLBACK_BUILD_ID, IncrementalCacheEntry } from "../internal.js";
import { NAME as KV_CACHE_NAME } from "./kv-incremental-cache.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";
/**
* Whether the regional cache entry should be updated in the background or not when it experiences
* a cache hit.
*
* @default `false` for the `short-lived` mode, and `true` for the `long-lived` mode.
*/
shouldLazilyUpdateOnCacheHit?: boolean;
};
/**
* Wrapper adding a regional cache on an `IncrementalCache` implementation
*/
class RegionalCache implements IncrementalCache {
public name: string;
protected localCache: Cache | undefined;
constructor(
private store: IncrementalCache,
private opts: Options
) {
if (this.store.name === KV_CACHE_NAME) {
throw new Error("The KV incremental cache does not need a regional cache.");
}
this.name = this.store.name;
this.opts.shouldLazilyUpdateOnCacheHit ??= this.opts.mode === "long-lived";
}
async get<IsFetch extends boolean = false>(
key: string,
isFetch?: IsFetch
): Promise<WithLastModified<CacheValue<IsFetch>> | null> {
try {
const cache = await this.getCacheInstance();
const localCacheKey = this.getCacheKey(key, isFetch);
// Check for a cached entry as this will be faster than the store response.
const cachedResponse = await cache.match(localCacheKey);
if (cachedResponse) {
debugCache("Get - cached response");
// Re-fetch from the store and update the regional cache in the background
if (this.opts.shouldLazilyUpdateOnCacheHit) {
getCloudflareContext().ctx.waitUntil(
this.store.get(key, isFetch).then(async (rawEntry) => {
const { value, lastModified } = rawEntry ?? {};
if (value && typeof lastModified === "number") {
await this.putToCache(localCacheKey, { value, lastModified });
}
})
);
}
return cachedResponse.json();
}
const rawEntry = await this.store.get(key, isFetch);
const { value, lastModified } = rawEntry ?? {};
if (!value || typeof lastModified !== "number") return null;
// Update the locale cache after retrieving from the store.
getCloudflareContext().ctx.waitUntil(this.putToCache(localCacheKey, { value, lastModified }));
return { value, lastModified };
} catch (e) {
error("Failed to get from regional cache", e);
return null;
}
}
async set<IsFetch extends boolean = false>(
key: string,
value: CacheValue<IsFetch>,
isFetch?: IsFetch
): Promise<void> {
try {
await this.store.set(key, value, isFetch);
await this.putToCache(this.getCacheKey(key, isFetch), {
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 get from regional cache`, e);
}
}
async delete(key: string): Promise<void> {
try {
await this.store.delete(key);
const cache = await this.getCacheInstance();
await cache.delete(this.getCacheKey(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 getCacheKey(key: string, isFetch?: boolean) {
return new Request(
new URL(
`${process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID}/${key}.${isFetch ? "fetch" : "cache"}`,
"http://cache.local"
)
);
}
protected async putToCache(key: Request, entry: IncrementalCacheEntry<boolean>): Promise<void> {
const cache = await this.getCacheInstance();
const age =
this.opts.mode === "short-lived"
? ONE_MINUTE_IN_SECONDS
: entry.value.revalidate || THIRTY_MINUTES_IN_SECONDS;
await cache.put(
key,
new Response(JSON.stringify(entry), {
headers: new Headers({ "cache-control": `max-age=${age}` }),
})
);
}
}
/**
* 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.mode 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.
* @param opts.shouldLazilyUpdateOnCacheHit Whether the regional cache entry should be updated in
* the background or not when it experiences a cache hit.
*
* @default `false` for the `short-lived` mode, and `true` for the `long-lived` mode.
*/
export function withRegionalCache(cache: IncrementalCache, opts: Options) {
return new RegionalCache(cache, opts);
}