Skip to content

Commit 6791cea

Browse files
authored
chore: use kebab-case for kv cache import (#353)
* rename file * add deprecated export * rename a couple imports
1 parent a19b34d commit 6791cea

File tree

7 files changed

+173
-162
lines changed

7 files changed

+173
-162
lines changed

.changeset/nervous-crews-yawn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/cloudflare": patch
3+
---
4+
5+
Use kebab-case for the KV Cache.

examples/e2e/app-router/open-next.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
2-
import cache from "@opennextjs/cloudflare/kvCache";
2+
import cache from "@opennextjs/cloudflare/kv-cache";
33

44
const config: OpenNextConfig = {
55
default: {

examples/vercel-blog-starter/open-next.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
2-
import cache from "@opennextjs/cloudflare/kvCache";
2+
import cache from "@opennextjs/cloudflare/kv-cache";
33

44
const config: OpenNextConfig = {
55
default: {
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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;
9+
10+
/**
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.
16+
*/
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();
Lines changed: 3 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,161 +1,6 @@
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";
92

103
/**
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`.
165
*/
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;

packages/cloudflare/src/cli/build/open-next/copyCacheAssets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { join } from "node:path";
33

44
import * as buildHelper from "@opennextjs/aws/build/helper.js";
55

6-
import { CACHE_ASSET_DIR } from "../../../api/kvCache.js";
6+
import { CACHE_ASSET_DIR } from "../../../api/kv-cache.js";
77

88
export function copyCacheAssets(options: buildHelper.BuildOptions) {
99
const { outputDir } = options;

packages/cloudflare/templates/defaults/open-next.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// default open-next.config.ts file created by @opennextjs/cloudflare
22

3-
import cache from "@opennextjs/cloudflare/kvCache";
3+
import cache from "@opennextjs/cloudflare/kv-cache";
44

55
const config = {
66
default: {

0 commit comments

Comments
 (0)