|
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