|
1 | | -import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides"; |
2 | | -import { IgnorableError, RecoverableError } from "@opennextjs/aws/utils/error.js"; |
3 | | - |
4 | | -import { getCloudflareContext } from "./cloudflare-context.js"; |
5 | | - |
6 | | -export const CACHE_ASSET_DIR = "cdn-cgi/_next_cache"; |
7 | | - |
8 | | -export const STATUS_DELETED = 1; |
| 1 | +import cache from "./kv-cache.js"; |
9 | 2 |
|
10 | 3 | /** |
11 | | - * Open Next cache based on cloudflare KV and Assets. |
12 | | - * |
13 | | - * Note: The class is instantiated outside of the request context. |
14 | | - * The cloudflare context and process.env are not initialzed yet |
15 | | - * when the constructor is called. |
| 4 | + * @deprecated Please import from `kv-cache` instead of `kvCache`. |
16 | 5 | */ |
17 | | -class Cache implements IncrementalCache { |
18 | | - readonly name = "cloudflare-kv"; |
19 | | - |
20 | | - async get<IsFetch extends boolean = false>( |
21 | | - key: string, |
22 | | - isFetch?: IsFetch |
23 | | - ): Promise<WithLastModified<CacheValue<IsFetch>> | null> { |
24 | | - const cfEnv = getCloudflareContext().env; |
25 | | - const kv = cfEnv.NEXT_CACHE_WORKERS_KV; |
26 | | - const assets = cfEnv.ASSETS; |
27 | | - |
28 | | - if (!(kv || assets)) { |
29 | | - throw new IgnorableError(`No KVNamespace nor Fetcher`); |
30 | | - } |
31 | | - |
32 | | - this.debug(`Get ${key}`); |
33 | | - |
34 | | - try { |
35 | | - let entry: { |
36 | | - value?: CacheValue<IsFetch>; |
37 | | - lastModified?: number; |
38 | | - status?: number; |
39 | | - } | null = null; |
40 | | - |
41 | | - if (kv) { |
42 | | - this.debug(`- From KV`); |
43 | | - const kvKey = this.getKVKey(key, isFetch); |
44 | | - entry = await kv.get(kvKey, "json"); |
45 | | - if (entry?.status === STATUS_DELETED) { |
46 | | - return null; |
47 | | - } |
48 | | - } |
49 | | - |
50 | | - if (!entry && assets) { |
51 | | - this.debug(`- From Assets`); |
52 | | - const url = this.getAssetUrl(key, isFetch); |
53 | | - const response = await assets.fetch(url); |
54 | | - if (response.ok) { |
55 | | - // TODO: consider populating KV with the asset value if faster. |
56 | | - // This could be optional as KV writes are $$. |
57 | | - // See https://github.com/opennextjs/opennextjs-cloudflare/pull/194#discussion_r1893166026 |
58 | | - entry = { |
59 | | - value: await response.json(), |
60 | | - // __BUILD_TIMESTAMP_MS__ is injected by ESBuild. |
61 | | - lastModified: (globalThis as { __BUILD_TIMESTAMP_MS__?: number }).__BUILD_TIMESTAMP_MS__, |
62 | | - }; |
63 | | - } |
64 | | - if (!kv) { |
65 | | - // The cache can not be updated when there is no KV |
66 | | - // As we don't want to keep serving stale data for ever, |
67 | | - // we pretend the entry is not in cache |
68 | | - if ( |
69 | | - entry?.value && |
70 | | - "kind" in entry.value && |
71 | | - entry.value.kind === "FETCH" && |
72 | | - entry.value.data?.headers?.expires |
73 | | - ) { |
74 | | - const expiresTime = new Date(entry.value.data.headers.expires).getTime(); |
75 | | - if (!isNaN(expiresTime) && expiresTime <= Date.now()) { |
76 | | - this.debug(`found expired entry (expire time: ${entry.value.data.headers.expires})`); |
77 | | - return null; |
78 | | - } |
79 | | - } |
80 | | - } |
81 | | - } |
82 | | - |
83 | | - this.debug(entry ? `-> hit` : `-> miss`); |
84 | | - return { value: entry?.value, lastModified: entry?.lastModified }; |
85 | | - } catch { |
86 | | - throw new RecoverableError(`Failed to get cache [${key}]`); |
87 | | - } |
88 | | - } |
89 | | - |
90 | | - async set<IsFetch extends boolean = false>( |
91 | | - key: string, |
92 | | - value: CacheValue<IsFetch>, |
93 | | - isFetch?: IsFetch |
94 | | - ): Promise<void> { |
95 | | - const kv = getCloudflareContext().env.NEXT_CACHE_WORKERS_KV; |
96 | | - |
97 | | - if (!kv) { |
98 | | - throw new IgnorableError(`No KVNamespace`); |
99 | | - } |
100 | | - |
101 | | - this.debug(`Set ${key}`); |
102 | | - |
103 | | - try { |
104 | | - const kvKey = this.getKVKey(key, isFetch); |
105 | | - // Note: We can not set a TTL as we might fallback to assets, |
106 | | - // still removing old data (old BUILD_ID) could help avoiding |
107 | | - // the cache growing too big. |
108 | | - await kv.put( |
109 | | - kvKey, |
110 | | - JSON.stringify({ |
111 | | - value, |
112 | | - // Note: `Date.now()` returns the time of the last IO rather than the actual time. |
113 | | - // See https://developers.cloudflare.com/workers/reference/security-model/ |
114 | | - lastModified: Date.now(), |
115 | | - }) |
116 | | - ); |
117 | | - } catch { |
118 | | - throw new RecoverableError(`Failed to set cache [${key}]`); |
119 | | - } |
120 | | - } |
121 | | - |
122 | | - async delete(key: string): Promise<void> { |
123 | | - const kv = getCloudflareContext().env.NEXT_CACHE_WORKERS_KV; |
124 | | - |
125 | | - if (!kv) { |
126 | | - throw new IgnorableError(`No KVNamespace`); |
127 | | - } |
128 | | - |
129 | | - this.debug(`Delete ${key}`); |
130 | | - |
131 | | - try { |
132 | | - const kvKey = this.getKVKey(key, /* isFetch= */ false); |
133 | | - // Do not delete the key as we would then fallback to the assets. |
134 | | - await kv.put(kvKey, JSON.stringify({ status: STATUS_DELETED })); |
135 | | - } catch { |
136 | | - throw new RecoverableError(`Failed to delete cache [${key}]`); |
137 | | - } |
138 | | - } |
139 | | - |
140 | | - protected getKVKey(key: string, isFetch?: boolean): string { |
141 | | - return `${this.getBuildId()}/${key}.${isFetch ? "fetch" : "cache"}`; |
142 | | - } |
143 | | - |
144 | | - protected getAssetUrl(key: string, isFetch?: boolean): string { |
145 | | - return isFetch |
146 | | - ? `http://assets.local/${CACHE_ASSET_DIR}/__fetch/${this.getBuildId()}/${key}` |
147 | | - : `http://assets.local/${CACHE_ASSET_DIR}/${this.getBuildId()}/${key}.cache`; |
148 | | - } |
149 | | - |
150 | | - protected debug(...args: unknown[]) { |
151 | | - if (process.env.NEXT_PRIVATE_DEBUG_CACHE) { |
152 | | - console.log(`[Cache ${this.name}] `, ...args); |
153 | | - } |
154 | | - } |
155 | | - |
156 | | - protected getBuildId() { |
157 | | - return process.env.NEXT_BUILD_ID ?? "no-build-id"; |
158 | | - } |
159 | | -} |
160 | | - |
161 | | -export default new Cache(); |
| 6 | +export default cache; |
0 commit comments