-
Notifications
You must be signed in to change notification settings - Fork 119
Expand file tree
/
Copy pathkv-incremental-cache.ts
More file actions
101 lines (83 loc) · 3.12 KB
/
kv-incremental-cache.ts
File metadata and controls
101 lines (83 loc) · 3.12 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
import { error } from "@opennextjs/aws/adapters/logger.js";
import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js";
import { IgnorableError } from "@opennextjs/aws/utils/error.js";
import { getCloudflareContext } from "../../cloudflare-context.js";
import { debugCache, FALLBACK_BUILD_ID, IncrementalCacheEntry } from "../internal.js";
export const NAME = "cf-kv-incremental-cache";
export const BINDING_NAME = "NEXT_INC_CACHE_KV";
/**
* Open Next cache based on Cloudflare KV.
*
* Note: The class is instantiated outside of the request context.
* The cloudflare context and process.env are not initialized yet
* when the constructor is called.
*/
class KVIncrementalCache implements IncrementalCache {
readonly name = NAME;
async get<IsFetch extends boolean = false>(
key: string,
isFetch?: IsFetch
): Promise<WithLastModified<CacheValue<IsFetch>> | null> {
const kv = getCloudflareContext().env[BINDING_NAME];
if (!kv) throw new IgnorableError("No KV Namespace");
debugCache(`Get ${key}`);
try {
const entry = await kv.get<IncrementalCacheEntry<IsFetch> | CacheValue<IsFetch>>(
this.getKVKey(key, isFetch),
"json"
);
if (!entry) return null;
if ("lastModified" in entry) {
return entry;
}
// if there is no lastModified property, the file was stored during build-time cache population.
return {
value: entry,
// __BUILD_TIMESTAMP_MS__ is injected by ESBuild.
lastModified: (globalThis as { __BUILD_TIMESTAMP_MS__?: number }).__BUILD_TIMESTAMP_MS__,
};
} catch (e) {
error("Failed to get from cache", e);
return null;
}
}
async set<IsFetch extends boolean = false>(
key: string,
value: CacheValue<IsFetch>,
isFetch?: IsFetch
): Promise<void> {
const kv = getCloudflareContext().env[BINDING_NAME];
if (!kv) throw new IgnorableError("No KV Namespace");
debugCache(`Set ${key}`);
try {
await kv.put(
this.getKVKey(key, isFetch),
JSON.stringify({
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(),
})
// TODO: Figure out how to best leverage KV's TTL.
// NOTE: Ideally, the cache should operate in an SWR-like manner.
);
} catch (e) {
error("Failed to set to cache", e);
}
}
async delete(key: string): Promise<void> {
const kv = getCloudflareContext().env[BINDING_NAME];
if (!kv) throw new IgnorableError("No KV Namespace");
debugCache(`Delete ${key}`);
try {
await kv.delete(this.getKVKey(key, /* isFetch= */ false));
} catch (e) {
error("Failed to delete from cache", e);
}
}
protected getKVKey(key: string, isFetch?: boolean): string {
const buildId = process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
return `${buildId}/${key}.${isFetch ? "fetch" : "cache"}`.replace(/\/+/g, "/");
}
}
export default new KVIncrementalCache();