diff --git a/.changeset/gold-onions-lick.md b/.changeset/gold-onions-lick.md new file mode 100644 index 00000000..05012722 --- /dev/null +++ b/.changeset/gold-onions-lick.md @@ -0,0 +1,7 @@ +--- +"@opennextjs/cloudflare": minor +--- + +feat: use the cache api if there is no kv cache available + +Instead of requiring a KV cache is available in the environment for Next.js caching to work, the cache handle will default to using the Cache API. diff --git a/packages/cloudflare/src/cli/templates/cache-handler/cache-store.ts b/packages/cloudflare/src/cli/templates/cache-handler/cache-store.ts new file mode 100644 index 00000000..89643e1b --- /dev/null +++ b/packages/cloudflare/src/cli/templates/cache-handler/cache-store.ts @@ -0,0 +1,65 @@ +import type { IncrementalCacheValue } from "next/dist/server/response-cache"; + +export type CacheEntry = { + lastModified: number; + value: IncrementalCacheValue | null; +}; + +export type CacheStore = { + get: (key: string) => Promise; + put: (key: string, entry: CacheEntry, ttl?: number) => Promise; +}; + +export function getCacheStore() { + const kvName = process.env.__OPENNEXT_KV_BINDING_NAME; + if (process.env[kvName]) { + return new KVStore(process.env[kvName] as unknown as KVNamespace); + } + + return new CacheAPIStore(); +} + +const oneYearInMs = 31536000; + +class KVStore implements CacheStore { + constructor(private store: KVNamespace) {} + + get(key: string) { + return this.store.get(key, "json"); + } + + put(key: string, entry: CacheEntry, ttl = oneYearInMs) { + return this.store.put(key, JSON.stringify(entry), { + expirationTtl: ttl, + }); + } +} + +class CacheAPIStore implements CacheStore { + constructor(private name = "__opennext_cache") {} + + async get(key: string) { + const cache = await caches.open(this.name); + const response = await cache.match(this.createCacheKey(key)); + + if (response) { + return response.json(); + } + + return null; + } + + async put(key: string, entry: CacheEntry, ttl = oneYearInMs) { + const cache = await caches.open(this.name); + + const response = new Response(JSON.stringify(entry), { + headers: { "cache-control": `max-age=${ttl}` }, + }); + + return cache.put(this.createCacheKey(key), response); + } + + private createCacheKey(key: string) { + return `https://${this.name}.local/entry/${key}`; + } +} diff --git a/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts b/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts index 0f7de47d..f4774ccc 100644 --- a/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts +++ b/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts @@ -1,4 +1,3 @@ -import type { KVNamespace } from "@cloudflare/workers-types"; import type { CacheHandler, CacheHandlerContext, @@ -14,20 +13,17 @@ import { RSC_SUFFIX, SEED_DATA_DIR, } from "../../constants/incremental-cache"; +import type { CacheEntry, CacheStore } from "./cache-store"; +import { getCacheStore } from "./cache-store"; import { getSeedBodyFile, getSeedMetaFile, getSeedTextFile, parseCtx } from "./utils"; -type CacheEntry = { - lastModified: number; - value: IncrementalCacheValue | null; -}; - export class OpenNextCacheHandler implements CacheHandler { - protected kv: KVNamespace | undefined; + protected cache: CacheStore; protected debug: boolean = !!process.env.NEXT_PRIVATE_DEBUG_CACHE; constructor(protected ctx: CacheHandlerContext) { - this.kv = process.env[process.env.__OPENNEXT_KV_BINDING_NAME] as KVNamespace | undefined; + this.cache = getCacheStore(); } async get(...args: Parameters): Promise { @@ -36,13 +32,11 @@ export class OpenNextCacheHandler implements CacheHandler { if (this.debug) console.log(`cache - get: ${key}, ${ctx?.kind}`); - if (this.kv !== undefined) { - try { - const value = await this.kv.get(key, "json"); - if (value) return value; - } catch (e) { - console.error(`Failed to get value for key = ${key}: ${e}`); - } + try { + const value = await this.cache.get(key); + if (value) return value; + } catch (e) { + console.error(`Failed to get value for key = ${key}: ${e}`); } // Check for seed data from the file-system. @@ -118,10 +112,6 @@ export class OpenNextCacheHandler implements CacheHandler { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [key, entry, _ctx] = args; - if (this.kv === undefined) { - return; - } - if (this.debug) console.log(`cache - set: ${key}`); const data: CacheEntry = { @@ -130,7 +120,7 @@ export class OpenNextCacheHandler implements CacheHandler { }; try { - await this.kv.put(key, JSON.stringify(data)); + await this.cache.put(key, data); } catch (e) { console.error(`Failed to set value for key = ${key}: ${e}`); } @@ -138,9 +128,6 @@ export class OpenNextCacheHandler implements CacheHandler { async revalidateTag(...args: Parameters) { const [tags] = args; - if (this.kv === undefined) { - return; - } if (this.debug) console.log(`cache - revalidateTag: ${JSON.stringify([tags].flat())}`); }