From a87f6b3713ccfc0bf6c3cf2276b285fd250c8ffa Mon Sep 17 00:00:00 2001 From: James Date: Sun, 17 Nov 2024 19:29:51 +0000 Subject: [PATCH 1/3] refactor: move kv cache to use separate store interface --- .../templates/cache-handler/cache-store.ts | 32 +++++++++++++++++++ .../cache-handler/open-next-cache-handler.ts | 22 ++++++------- 2 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 packages/cloudflare/src/cli/templates/cache-handler/cache-store.ts 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 0000000..68b4b3a --- /dev/null +++ b/packages/cloudflare/src/cli/templates/cache-handler/cache-store.ts @@ -0,0 +1,32 @@ +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 (kvName && process.env[kvName]) { + return new KVStore(process.env[kvName] as unknown as KVNamespace); + } +} + +const defaultTTL = 31536000; // 1 year + +class KVStore implements CacheStore { + constructor(private store: KVNamespace) {} + + get(key: string) { + return this.store.get(key, "json"); + } + + put(key: string, entry: CacheEntry, ttl = defaultTTL) { + return this.store.put(key, JSON.stringify(entry), { expirationTtl: ttl }); + } +} 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 0f7de47..558415f 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 | undefined; 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,9 +32,9 @@ export class OpenNextCacheHandler implements CacheHandler { if (this.debug) console.log(`cache - get: ${key}, ${ctx?.kind}`); - if (this.kv !== undefined) { + if (this.cache !== undefined) { try { - const value = await this.kv.get(key, "json"); + const value = await this.cache.get(key); if (value) return value; } catch (e) { console.error(`Failed to get value for key = ${key}: ${e}`); @@ -118,7 +114,7 @@ export class OpenNextCacheHandler implements CacheHandler { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [key, entry, _ctx] = args; - if (this.kv === undefined) { + if (this.cache === undefined) { return; } @@ -130,7 +126,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,7 +134,7 @@ export class OpenNextCacheHandler implements CacheHandler { async revalidateTag(...args: Parameters) { const [tags] = args; - if (this.kv === undefined) { + if (this.cache === undefined) { return; } From cc9ad269e297be38b39936c35aaf32f4d4aa3d3b Mon Sep 17 00:00:00 2001 From: James Date: Sun, 17 Nov 2024 19:32:48 +0000 Subject: [PATCH 2/3] feat: use the cache api if there is no kv cache available --- .changeset/gold-onions-lick.md | 7 ++++ .../templates/cache-handler/cache-store.ts | 37 ++++++++++++++++++- .../cache-handler/open-next-cache-handler.ts | 21 +++-------- 3 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 .changeset/gold-onions-lick.md diff --git a/.changeset/gold-onions-lick.md b/.changeset/gold-onions-lick.md new file mode 100644 index 0000000..0501272 --- /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 index 68b4b3a..1f19090 100644 --- a/packages/cloudflare/src/cli/templates/cache-handler/cache-store.ts +++ b/packages/cloudflare/src/cli/templates/cache-handler/cache-store.ts @@ -12,9 +12,11 @@ export type CacheStore = { export function getCacheStore() { const kvName = process.env.__OPENNEXT_KV_BINDING_NAME; - if (kvName && process.env[kvName]) { + if (process.env[kvName]) { return new KVStore(process.env[kvName] as unknown as KVNamespace); } + + return new CacheAPIStore(); } const defaultTTL = 31536000; // 1 year @@ -27,6 +29,37 @@ class KVStore implements CacheStore { } put(key: string, entry: CacheEntry, ttl = defaultTTL) { - return this.store.put(key, JSON.stringify(entry), { expirationTtl: ttl }); + 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 = defaultTTL) { + 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 558415f..f4774cc 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 @@ -18,7 +18,7 @@ import { getCacheStore } from "./cache-store"; import { getSeedBodyFile, getSeedMetaFile, getSeedTextFile, parseCtx } from "./utils"; export class OpenNextCacheHandler implements CacheHandler { - protected cache: CacheStore | undefined; + protected cache: CacheStore; protected debug: boolean = !!process.env.NEXT_PRIVATE_DEBUG_CACHE; @@ -32,13 +32,11 @@ export class OpenNextCacheHandler implements CacheHandler { if (this.debug) console.log(`cache - get: ${key}, ${ctx?.kind}`); - if (this.cache !== undefined) { - try { - const value = await this.cache.get(key); - 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. @@ -114,10 +112,6 @@ export class OpenNextCacheHandler implements CacheHandler { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [key, entry, _ctx] = args; - if (this.cache === undefined) { - return; - } - if (this.debug) console.log(`cache - set: ${key}`); const data: CacheEntry = { @@ -134,9 +128,6 @@ export class OpenNextCacheHandler implements CacheHandler { async revalidateTag(...args: Parameters) { const [tags] = args; - if (this.cache === undefined) { - return; - } if (this.debug) console.log(`cache - revalidateTag: ${JSON.stringify([tags].flat())}`); } From 5cd472445a59599cd282c4e42b78fdb452ca78d5 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 18 Nov 2024 20:39:10 +0000 Subject: [PATCH 3/3] rename `defaultTTL` to `oneYearInMs` --- .../src/cli/templates/cache-handler/cache-store.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cloudflare/src/cli/templates/cache-handler/cache-store.ts b/packages/cloudflare/src/cli/templates/cache-handler/cache-store.ts index 1f19090..89643e1 100644 --- a/packages/cloudflare/src/cli/templates/cache-handler/cache-store.ts +++ b/packages/cloudflare/src/cli/templates/cache-handler/cache-store.ts @@ -19,7 +19,7 @@ export function getCacheStore() { return new CacheAPIStore(); } -const defaultTTL = 31536000; // 1 year +const oneYearInMs = 31536000; class KVStore implements CacheStore { constructor(private store: KVNamespace) {} @@ -28,7 +28,7 @@ class KVStore implements CacheStore { return this.store.get(key, "json"); } - put(key: string, entry: CacheEntry, ttl = defaultTTL) { + put(key: string, entry: CacheEntry, ttl = oneYearInMs) { return this.store.put(key, JSON.stringify(entry), { expirationTtl: ttl, }); @@ -49,7 +49,7 @@ class CacheAPIStore implements CacheStore { return null; } - async put(key: string, entry: CacheEntry, ttl = defaultTTL) { + async put(key: string, entry: CacheEntry, ttl = oneYearInMs) { const cache = await caches.open(this.name); const response = new Response(JSON.stringify(entry), {