-
Notifications
You must be signed in to change notification settings - Fork 118
Expand file tree
/
Copy pathkv-incremental-cache.ts
More file actions
111 lines (92 loc) · 3.23 KB
/
kv-incremental-cache.ts
File metadata and controls
111 lines (92 loc) · 3.23 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
import { error } from "@opennextjs/aws/adapters/logger.js";
import type {
CacheEntryType,
CacheValue,
IncrementalCache,
WithLastModified,
} from "@opennextjs/aws/types/overrides.js";
import { IgnorableError } from "@opennextjs/aws/utils/error.js";
import { getCloudflareContext } from "../../cloudflare-context.js";
import { computeCacheKey, debugCache, IncrementalCacheEntry } from "../internal.js";
export const NAME = "cf-kv-incremental-cache";
export const BINDING_NAME = "NEXT_INC_CACHE_KV";
export const PREFIX_ENV_NAME = "NEXT_INC_CACHE_KV_PREFIX";
/**
* Open Next cache based on Cloudflare KV.
*
* The prefix that the cache entries are stored under can be configured with the `NEXT_INC_CACHE_KV_PREFIX`
* environment variable, and defaults to `incremental-cache`.
*
* 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: string = NAME;
async get<CacheType extends CacheEntryType = "cache">(
key: string,
cacheType?: CacheType
): Promise<WithLastModified<CacheValue<CacheType>> | null> {
const kv = getCloudflareContext().env[BINDING_NAME];
if (!kv) throw new IgnorableError("No KV Namespace");
debugCache("KVIncrementalCache", `get ${key}`);
try {
const entry = await kv.get<IncrementalCacheEntry<CacheType>>(this.getKVKey(key, cacheType), "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,
lastModified: globalThis.__BUILD_TIMESTAMP_MS__,
};
} catch (e) {
error("Failed to get from cache", e);
return null;
}
}
async set<CacheType extends CacheEntryType = "cache">(
key: string,
value: CacheValue<CacheType>,
cacheType?: CacheType
): Promise<void> {
const kv = getCloudflareContext().env[BINDING_NAME];
if (!kv) throw new IgnorableError("No KV Namespace");
debugCache("KVIncrementalCache", `set ${key}`);
try {
await kv.put(
this.getKVKey(key, cacheType),
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("KVIncrementalCache", `delete ${key}`);
try {
// Only cache that gets deleted is the ISR/SSG cache.
await kv.delete(this.getKVKey(key, "cache"));
} catch (e) {
error("Failed to delete from cache", e);
}
}
protected getKVKey(key: string, cacheType?: CacheEntryType): string {
return computeCacheKey(key, {
prefix: getCloudflareContext().env[PREFIX_ENV_NAME],
buildId: process.env.NEXT_BUILD_ID,
cacheType,
});
}
}
export default new KVIncrementalCache();