From ac3f05eeb3ce0acdd793190adf29b6df5dad67b3 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Sun, 24 Dec 2023 15:28:13 +0100 Subject: [PATCH] use custom KV adapter --- cache-handlers/kv.js | 151 +++++++++++++++++++++++++++++++++++++++++++ next.config.js | 21 +++++- package-lock.json | 8 +-- package.json | 4 +- 4 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 cache-handlers/kv.js diff --git a/cache-handlers/kv.js b/cache-handlers/kv.js new file mode 100644 index 0000000..4070ee4 --- /dev/null +++ b/cache-handlers/kv.js @@ -0,0 +1,151 @@ +// ../../Repos/my-repos/next-on-pages/packages/next-on-pages/templates/cache/builtInCacheHandler.ts +var SUSPENSE_CACHE_URL = "INTERNAL_SUSPENSE_CACHE_HOSTNAME.local"; +var NEXT_CACHE_IMPLICIT_TAG_ID = "_N_T_"; +var BuiltInCacheHandler = class { + constructor(ctx) { + this.ctx = ctx; + this.revalidatedTags = new Set(ctx.revalidatedTags); + } + tagsManifest; + tagsManifestKey = "tags-manifest"; + revalidatedTags; + async retrieve(key) { + throw new Error(`Method not implemented - ${key}`); + } + async update(key, value) { + throw new Error(`Method not implemented - ${key}, ${value}`); + } + async set(key, value, extra = {}) { + const newEntry = { + lastModified: Date.now(), + value + }; + await this.update(key, JSON.stringify(newEntry)); + switch (newEntry.value?.kind) { + case "FETCH": { + const tags = getTagsFromEntry(newEntry) ?? extra.tags ?? []; + await this.setTags(tags, { cacheKey: key }); + const derivedTags = getDerivedTags(tags); + const implicitTags = derivedTags.map( + (tag) => `${NEXT_CACHE_IMPLICIT_TAG_ID}${tag}` + ); + [...derivedTags, ...implicitTags].forEach( + (tag) => this.revalidatedTags.delete(tag) + ); + } + } + } + async get(key, { softTags }) { + const entry = await this.retrieve(key); + if (!entry) + return null; + let data; + try { + data = JSON.parse(entry); + } catch (e) { + return null; + } + switch (data.value?.kind) { + case "FETCH": { + await this.loadTagsManifest(); + const tags = getTagsFromEntry(data); + const combinedTags = softTags ? [...tags ?? [], ...softTags] : getDerivedTags(tags ?? []); + const isStale = combinedTags.some((tag) => { + if (this.revalidatedTags.has(tag)) + return true; + const tagEntry = this.tagsManifest?.items?.[tag]; + return tagEntry?.revalidatedAt && tagEntry?.revalidatedAt >= (data.lastModified ?? Date.now()); + }); + return isStale ? null : data; + } + default: { + return data; + } + } + } + async revalidateTag(tag) { + await this.setTags([tag], { revalidatedAt: Date.now() }); + this.revalidatedTags.add(tag); + } + async loadTagsManifest() { + try { + const rawManifest = await this.retrieve(this.tagsManifestKey); + if (rawManifest) { + this.tagsManifest = JSON.parse(rawManifest); + } + } catch (e) { + } + this.tagsManifest ??= { version: 1, items: {} }; + } + async saveTagsManifest() { + if (this.tagsManifest) { + const newValue = JSON.stringify(this.tagsManifest); + await this.update(this.tagsManifestKey, newValue); + } + } + async setTags(tags, { cacheKey, revalidatedAt }) { + await this.loadTagsManifest(); + const tagsManifest = this.tagsManifest; + for (const tag of tags) { + const data = tagsManifest.items[tag] ?? { keys: [] }; + if (cacheKey && !data.keys.includes(cacheKey)) { + data.keys.push(cacheKey); + } + if (revalidatedAt) { + data.revalidatedAt = revalidatedAt; + } + tagsManifest.items[tag] = data; + } + await this.saveTagsManifest(); + } + buildCacheKey(key) { + return `https://${SUSPENSE_CACHE_URL}/entry/${key}`; + } +}; +function getDerivedTags(tags) { + const derivedTags = ["/"]; + for (const tag of tags || []) { + if (tag.startsWith("/")) { + const pathnameParts = tag.split("/"); + for (let i = 1; i < pathnameParts.length + 1; i++) { + const curPathname = pathnameParts.slice(0, i).join("/"); + if (curPathname) { + derivedTags.push(curPathname); + if (!derivedTags.includes(curPathname)) { + derivedTags.push(curPathname); + } + } + } + } else if (!derivedTags.includes(tag)) { + derivedTags.push(tag); + } + } + return derivedTags; +} +function getTagsFromEntry(entry) { + return entry.value?.tags ?? entry.value?.data?.tags; +} + +// ../../Repos/my-repos/next-on-pages/packages/next-on-pages/templates/cache/KVCacheHandler.ts +var KVCacheHandler = class extends BuiltInCacheHandler { + constructor(ctx) { + super(ctx); + } + async retrieve(key) { + const value = await process.env.MY_CUSTOM_SUS_KV.get( + this.buildCacheKey(key) + ); + return value ?? null; + } + async update(key, value) { + await process.env.MY_CUSTOM_SUS_KV.put( + this.buildCacheKey(key), + value + ); + } +}; + +module.exports = KVCacheHandler; +// export { +// KVCacheHandler as default +// }; diff --git a/next.config.js b/next.config.js index 084264a..e6889a3 100644 --- a/next.config.js +++ b/next.config.js @@ -1,15 +1,30 @@ /** @type {import('next').NextConfig} */ module.exports = (phase, { defaultConfig }) => { + /** * @type {import('next').NextConfig} */ const nextConfig = { experimental: { fetchCacheKeyPrefix: "my-unused-fetch-cache-key-prefix", - incrementalCacheHandlerPath: require.resolve( - "./cache-handlers/supabase-rest.js" - ), + incrementalCacheHandlerPath: getIncrementalCacheHandler('kv'), }, }; return nextConfig; }; + +/** + * @param {null|'supabase'|'kv'} handler + */ +function getIncrementalCacheHandler(handler) { + if(handler === null) { + return undefined; + } + + const cachePath = { + supabase: 'supabase-rest.js', + kv: 'kv.js' + }[handler]; + + return require.resolve(`./cache-handlers/${cachePath}`); +} diff --git a/package-lock.json b/package-lock.json index ef96077..1dc341b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "react-dom": "latest" }, "devDependencies": { - "@cloudflare/next-on-pages": "https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/7313865693/npm-package-next-on-pages-606", + "@cloudflare/next-on-pages": "https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/7315046801/npm-package-next-on-pages-606", "@types/node": "^17.0.45", "@types/react": "^17.0.53", "autoprefixer": "^10.4.14", @@ -68,9 +68,9 @@ } }, "node_modules/@cloudflare/next-on-pages": { - "version": "0.0.0-1e10aa0", - "resolved": "https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/7313865693/npm-package-next-on-pages-606", - "integrity": "sha512-DDR8e1wT6n7+gp7bqXhZnQe0P/OHm+rNaHa6sUQSy8m3YkV3oGRDie4nZQ45262T3+E/SQNZQKBEjVrfBj4McQ==", + "version": "0.0.0-e43b0ed", + "resolved": "https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/7315046801/npm-package-next-on-pages-606", + "integrity": "sha512-BqgbIbV7xlTnRFFHHPNxUDIlv4m8AGkexGjn4DpoRJbbv+cBttR42fzhU/IMaCVTl8nF23Gk8Eo4FfwJ7B0yOw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 14ba778..3b75067 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "pages:build": "npx @cloudflare/next-on-pages", "pages:deploy": "wrangler pages deploy .vercel/output/static", "pages:watch": "npx @cloudflare/next-on-pages --watch", - "pages:dev": "npx wrangler pages dev .vercel/output/static --compatibility-date=2023-12-18 --compatibility-flag=nodejs_compat" + "pages:dev": "npx wrangler pages dev .vercel/output/static --compatibility-date=2023-12-18 --compatibility-flag=nodejs_compat --kv MY_CUSTOM_SUS_KV" }, "dependencies": { "next": "latest", @@ -19,7 +19,7 @@ "react-dom": "latest" }, "devDependencies": { - "@cloudflare/next-on-pages": "https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/7313865693/npm-package-next-on-pages-606", + "@cloudflare/next-on-pages": "https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/7315046801/npm-package-next-on-pages-606", "@types/node": "^17.0.45", "@types/react": "^17.0.53", "autoprefixer": "^10.4.14",