From 81aae0e57e6caffc369381f9d28de2f6535464e3 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 3 Mar 2025 22:17:29 +0000 Subject: [PATCH 01/19] feat: auto-populating d1 cache data --- .changeset/tame-icons-shave.md | 5 ++ examples/e2e/app-router/package.json | 3 +- packages/cloudflare/src/cli/args.ts | 63 ++++++++++++------- packages/cloudflare/src/cli/build/build.ts | 10 +++ .../cloudflare/src/cli/build/utils/index.ts | 1 + .../src/cli/build/utils/populate-cache.ts | 61 ++++++++++++++++++ packages/cloudflare/src/cli/index.ts | 3 +- .../cloudflare/src/cli/project-options.ts | 3 + 8 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 .changeset/tame-icons-shave.md create mode 100644 packages/cloudflare/src/cli/build/utils/populate-cache.ts diff --git a/.changeset/tame-icons-shave.md b/.changeset/tame-icons-shave.md new file mode 100644 index 000000000..8dfbe7871 --- /dev/null +++ b/.changeset/tame-icons-shave.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/cloudflare": patch +--- + +feat: auto-populating d1 cache data diff --git a/examples/e2e/app-router/package.json b/examples/e2e/app-router/package.json index 3d1037d12..9d26a4c6e 100644 --- a/examples/e2e/app-router/package.json +++ b/examples/e2e/app-router/package.json @@ -10,8 +10,7 @@ "lint": "next lint", "clean": "rm -rf .turbo node_modules .next .open-next", "d1:clean": "wrangler d1 execute NEXT_CACHE_D1 --command \"DROP TABLE IF EXISTS tags; DROP TABLE IF EXISTS revalidations\"", - "d1:setup": "wrangler d1 execute NEXT_CACHE_D1 --file .open-next/cloudflare/cache-assets-manifest.sql", - "build:worker": "pnpm opennextjs-cloudflare && pnpm d1:clean && pnpm d1:setup", + "build:worker": "pnpm d1:clean && pnpm opennextjs-cloudflare --populateCache=local", "preview": "pnpm build:worker && pnpm wrangler dev", "e2e": "playwright test -c e2e/playwright.config.ts" }, diff --git a/packages/cloudflare/src/cli/args.ts b/packages/cloudflare/src/cli/args.ts index f64d0d731..b29ce67c2 100644 --- a/packages/cloudflare/src/cli/args.ts +++ b/packages/cloudflare/src/cli/args.ts @@ -2,34 +2,45 @@ import { mkdirSync, type Stats, statSync } from "node:fs"; import { resolve } from "node:path"; import { parseArgs } from "node:util"; +import { CacheBindingMode } from "./build/utils/index.js"; + export function getArgs(): { skipNextBuild: boolean; skipWranglerConfigCheck: boolean; outputDir?: string; minify: boolean; + populateCache?: { mode: "local" | "remote"; onlyPopulate: boolean }; } { - const { skipBuild, skipWranglerConfigCheck, output, noMinify } = parseArgs({ - options: { - skipBuild: { - type: "boolean", - short: "s", - default: false, - }, - output: { - type: "string", - short: "o", - }, - noMinify: { - type: "boolean", - default: false, + const { skipBuild, skipWranglerConfigCheck, output, noMinify, populateCache, onlyPopulateCache } = + parseArgs({ + options: { + skipBuild: { + type: "boolean", + short: "s", + default: false, + }, + output: { + type: "string", + short: "o", + }, + noMinify: { + type: "boolean", + default: false, + }, + skipWranglerConfigCheck: { + type: "boolean", + default: false, + }, + populateCache: { + type: "string", + }, + onlyPopulateCache: { + type: "boolean", + default: false, + }, }, - skipWranglerConfigCheck: { - type: "boolean", - default: false, - }, - }, - allowPositionals: false, - }).values; + allowPositionals: false, + }).values; const outputDir = output ? resolve(output) : undefined; @@ -37,6 +48,13 @@ export function getArgs(): { assertDirArg(outputDir, "output", true); } + if ( + (populateCache !== undefined || onlyPopulateCache) && + (!populateCache?.length || !["local", "remote"].includes(populateCache)) + ) { + throw new Error(`Error: missing mode for populate cache flag, expected 'local' | 'remote'`); + } + return { outputDir, skipNextBuild: skipBuild || ["1", "true", "yes"].includes(String(process.env.SKIP_NEXT_APP_BUILD)), @@ -44,6 +62,9 @@ export function getArgs(): { skipWranglerConfigCheck || ["1", "true", "yes"].includes(String(process.env.SKIP_WRANGLER_CONFIG_CHECK)), minify: !noMinify, + populateCache: populateCache + ? { mode: populateCache as CacheBindingMode, onlyPopulate: !!onlyPopulateCache } + : undefined, }; } diff --git a/packages/cloudflare/src/cli/build/build.ts b/packages/cloudflare/src/cli/build/build.ts index 110f563b8..4148b92be 100644 --- a/packages/cloudflare/src/cli/build/build.ts +++ b/packages/cloudflare/src/cli/build/build.ts @@ -20,6 +20,7 @@ import { createOpenNextConfigIfNotExistent, createWranglerConfigIfNotExistent, ensureCloudflareConfig, + populateCache, } from "./utils/index.js"; import { getVersion } from "./utils/version.js"; @@ -62,6 +63,11 @@ export async function build(projectOpts: ProjectOptions): Promise { logger.info(`@opennextjs/cloudflare version: ${cloudflare}`); logger.info(`@opennextjs/aws version: ${aws}`); + if (projectOpts.populateCache?.onlyPopulate) { + populateCache(options, config, projectOpts.populateCache.mode); + return; + } + if (projectOpts.skipNextBuild) { logger.warn("Skipping Next.js build"); } else { @@ -103,6 +109,10 @@ export async function build(projectOpts: ProjectOptions): Promise { await createWranglerConfigIfNotExistent(projectOpts); } + if (projectOpts.populateCache) { + populateCache(options, config, projectOpts.populateCache.mode); + } + logger.info("OpenNext build complete."); } diff --git a/packages/cloudflare/src/cli/build/utils/index.ts b/packages/cloudflare/src/cli/build/utils/index.ts index cca97f023..e7fb383b6 100644 --- a/packages/cloudflare/src/cli/build/utils/index.ts +++ b/packages/cloudflare/src/cli/build/utils/index.ts @@ -4,3 +4,4 @@ export * from "./ensure-cf-config.js"; export * from "./extract-project-env-vars.js"; export * from "./needs-experimental-react.js"; export * from "./normalize-path.js"; +export * from "./populate-cache.js"; diff --git a/packages/cloudflare/src/cli/build/utils/populate-cache.ts b/packages/cloudflare/src/cli/build/utils/populate-cache.ts new file mode 100644 index 000000000..c9aa07335 --- /dev/null +++ b/packages/cloudflare/src/cli/build/utils/populate-cache.ts @@ -0,0 +1,61 @@ +import { spawnSync } from "node:child_process"; +import path from "node:path"; + +import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; +import logger from "@opennextjs/aws/logger.js"; +import type { + IncludedIncrementalCache, + IncludedTagCache, + LazyLoadedOverride, + OpenNextConfig, +} from "@opennextjs/aws/types/open-next.js"; +import type { IncrementalCache, TagCache } from "@opennextjs/aws/types/overrides.js"; + +export type CacheBindingMode = "local" | "remote"; + +async function resolveCacheName( + value: + | IncludedIncrementalCache + | IncludedTagCache + | LazyLoadedOverride + | LazyLoadedOverride +) { + return typeof value === "function" ? (await value()).name : value; +} + +function runWrangler(opts: BuildOptions, mode: CacheBindingMode, args: string[]) { + const result = spawnSync( + opts.packager, + ["exec", "wrangler", ...args, mode === "remote" && "--remote"].filter((v): v is string => !!v), + { + shell: true, + stdio: ["ignore", "ignore", "inherit"], + } + ); + + if (result.status !== 0) { + logger.error("Failed to populate cache"); + } else { + logger.info("Successfully populated cache"); + } +} + +export async function populateCache(opts: BuildOptions, config: OpenNextConfig, mode: CacheBindingMode) { + const { tagCache } = config.default.override ?? {}; + + if (tagCache) { + const name = await resolveCacheName(tagCache); + switch (name) { + case "d1-tag-cache": { + logger.info("\nPopulating D1 tag cache..."); + + runWrangler(opts, mode, [ + "d1 execute", + "NEXT_CACHE_D1", + `--file ${JSON.stringify(path.join(opts.outputDir, "cloudflare/cache-assets-manifest.sql"))}`, + ]); + break; + } + } + } +} diff --git a/packages/cloudflare/src/cli/index.ts b/packages/cloudflare/src/cli/index.ts index 2a6c654e4..be3c1e763 100644 --- a/packages/cloudflare/src/cli/index.ts +++ b/packages/cloudflare/src/cli/index.ts @@ -6,7 +6,7 @@ import { build } from "./build/build.js"; const nextAppDir = process.cwd(); -const { skipNextBuild, skipWranglerConfigCheck, outputDir, minify } = getArgs(); +const { skipNextBuild, skipWranglerConfigCheck, outputDir, minify, populateCache } = getArgs(); await build({ sourceDir: nextAppDir, @@ -14,4 +14,5 @@ await build({ skipNextBuild, skipWranglerConfigCheck, minify, + populateCache, }); diff --git a/packages/cloudflare/src/cli/project-options.ts b/packages/cloudflare/src/cli/project-options.ts index 0ceb3d96c..f83beef4d 100644 --- a/packages/cloudflare/src/cli/project-options.ts +++ b/packages/cloudflare/src/cli/project-options.ts @@ -1,3 +1,5 @@ +import type { CacheBindingMode } from "./build/utils/index.js"; + export type ProjectOptions = { // Next app root folder sourceDir: string; @@ -9,4 +11,5 @@ export type ProjectOptions = { skipWranglerConfigCheck: boolean; // Whether minification of the worker should be enabled minify: boolean; + populateCache?: { mode: CacheBindingMode; onlyPopulate: boolean }; }; From c19f43fea25cf1128f2575ab942ae6e439259672 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 4 Mar 2025 08:57:00 +0000 Subject: [PATCH 02/19] checks for output directory / enabled --- .../src/cli/build/utils/populate-cache.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/cloudflare/src/cli/build/utils/populate-cache.ts b/packages/cloudflare/src/cli/build/utils/populate-cache.ts index c9aa07335..27f5c4ae9 100644 --- a/packages/cloudflare/src/cli/build/utils/populate-cache.ts +++ b/packages/cloudflare/src/cli/build/utils/populate-cache.ts @@ -1,4 +1,5 @@ import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; import path from "node:path"; import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; @@ -35,15 +36,25 @@ function runWrangler(opts: BuildOptions, mode: CacheBindingMode, args: string[]) if (result.status !== 0) { logger.error("Failed to populate cache"); + process.exit(1); } else { logger.info("Successfully populated cache"); } } export async function populateCache(opts: BuildOptions, config: OpenNextConfig, mode: CacheBindingMode) { - const { tagCache } = config.default.override ?? {}; + const { incrementalCache, tagCache } = config.default.override ?? {}; - if (tagCache) { + if (!existsSync(opts.outputDir)) { + logger.error("Unable to populate cache: Open Next build not found"); + process.exit(1); + } + + if (!config.dangerous?.disableIncrementalCache && incrementalCache) { + logger.info("Incremental cache does not need populating"); + } + + if (!config.dangerous?.disableTagCache && tagCache) { const name = await resolveCacheName(tagCache); switch (name) { case "d1-tag-cache": { @@ -56,6 +67,8 @@ export async function populateCache(opts: BuildOptions, config: OpenNextConfig, ]); break; } + default: + logger.info("Tag cache does not need populating"); } } } From 697e2f8e0434c0f6c250d9b201c83bd27616988d Mon Sep 17 00:00:00 2001 From: James Anderson Date: Tue, 4 Mar 2025 09:50:26 +0000 Subject: [PATCH 03/19] Update packages/cloudflare/src/cli/build/utils/populate-cache.ts Co-authored-by: conico974 --- packages/cloudflare/src/cli/build/utils/populate-cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cloudflare/src/cli/build/utils/populate-cache.ts b/packages/cloudflare/src/cli/build/utils/populate-cache.ts index 27f5c4ae9..f5533289d 100644 --- a/packages/cloudflare/src/cli/build/utils/populate-cache.ts +++ b/packages/cloudflare/src/cli/build/utils/populate-cache.ts @@ -54,7 +54,7 @@ export async function populateCache(opts: BuildOptions, config: OpenNextConfig, logger.info("Incremental cache does not need populating"); } - if (!config.dangerous?.disableTagCache && tagCache) { + if (!config.dangerous?.disableTagCache && !config.dangerous?.disableIncrementalCache && tagCache) { const name = await resolveCacheName(tagCache); switch (name) { case "d1-tag-cache": { From 87b39b0e3fbfe53c64094a2bdf312f0f939b2d7e Mon Sep 17 00:00:00 2001 From: James Date: Sat, 8 Mar 2025 20:46:04 +0000 Subject: [PATCH 04/19] add suggestions --- packages/cloudflare/src/cli/args.ts | 11 +++++------ .../cloudflare/src/cli/build/utils/populate-cache.ts | 4 ++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/cloudflare/src/cli/args.ts b/packages/cloudflare/src/cli/args.ts index b29ce67c2..f21907694 100644 --- a/packages/cloudflare/src/cli/args.ts +++ b/packages/cloudflare/src/cli/args.ts @@ -2,14 +2,15 @@ import { mkdirSync, type Stats, statSync } from "node:fs"; import { resolve } from "node:path"; import { parseArgs } from "node:util"; -import { CacheBindingMode } from "./build/utils/index.js"; +import type { CacheBindingMode } from "./build/utils/index.js"; +import { isCacheBindingMode } from "./build/utils/index.js"; export function getArgs(): { skipNextBuild: boolean; skipWranglerConfigCheck: boolean; outputDir?: string; minify: boolean; - populateCache?: { mode: "local" | "remote"; onlyPopulate: boolean }; + populateCache?: { mode: CacheBindingMode; onlyPopulate: boolean }; } { const { skipBuild, skipWranglerConfigCheck, output, noMinify, populateCache, onlyPopulateCache } = parseArgs({ @@ -50,7 +51,7 @@ export function getArgs(): { if ( (populateCache !== undefined || onlyPopulateCache) && - (!populateCache?.length || !["local", "remote"].includes(populateCache)) + (!populateCache?.length || !isCacheBindingMode(populateCache)) ) { throw new Error(`Error: missing mode for populate cache flag, expected 'local' | 'remote'`); } @@ -62,9 +63,7 @@ export function getArgs(): { skipWranglerConfigCheck || ["1", "true", "yes"].includes(String(process.env.SKIP_WRANGLER_CONFIG_CHECK)), minify: !noMinify, - populateCache: populateCache - ? { mode: populateCache as CacheBindingMode, onlyPopulate: !!onlyPopulateCache } - : undefined, + populateCache: populateCache ? { mode: populateCache, onlyPopulate: !!onlyPopulateCache } : undefined, }; } diff --git a/packages/cloudflare/src/cli/build/utils/populate-cache.ts b/packages/cloudflare/src/cli/build/utils/populate-cache.ts index f5533289d..ad38ede79 100644 --- a/packages/cloudflare/src/cli/build/utils/populate-cache.ts +++ b/packages/cloudflare/src/cli/build/utils/populate-cache.ts @@ -72,3 +72,7 @@ export async function populateCache(opts: BuildOptions, config: OpenNextConfig, } } } + +export function isCacheBindingMode(v: string | undefined): v is CacheBindingMode { + return !!v && ["local", "remote"].includes(v); +} From 76e2c5d92e8ab43c8ef83e8833258883d528ff46 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 8 Mar 2025 20:49:16 +0000 Subject: [PATCH 05/19] rename onlyPopulate to onlyPopulateWithoutBuilding --- packages/cloudflare/src/cli/args.ts | 6 ++++-- packages/cloudflare/src/cli/build/build.ts | 2 +- packages/cloudflare/src/cli/project-options.ts | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/cloudflare/src/cli/args.ts b/packages/cloudflare/src/cli/args.ts index f21907694..5a546ad99 100644 --- a/packages/cloudflare/src/cli/args.ts +++ b/packages/cloudflare/src/cli/args.ts @@ -10,7 +10,7 @@ export function getArgs(): { skipWranglerConfigCheck: boolean; outputDir?: string; minify: boolean; - populateCache?: { mode: CacheBindingMode; onlyPopulate: boolean }; + populateCache?: { mode: CacheBindingMode; onlyPopulateWithoutBuilding: boolean }; } { const { skipBuild, skipWranglerConfigCheck, output, noMinify, populateCache, onlyPopulateCache } = parseArgs({ @@ -63,7 +63,9 @@ export function getArgs(): { skipWranglerConfigCheck || ["1", "true", "yes"].includes(String(process.env.SKIP_WRANGLER_CONFIG_CHECK)), minify: !noMinify, - populateCache: populateCache ? { mode: populateCache, onlyPopulate: !!onlyPopulateCache } : undefined, + populateCache: populateCache + ? { mode: populateCache, onlyPopulateWithoutBuilding: !!onlyPopulateCache } + : undefined, }; } diff --git a/packages/cloudflare/src/cli/build/build.ts b/packages/cloudflare/src/cli/build/build.ts index 4148b92be..c2b8de995 100644 --- a/packages/cloudflare/src/cli/build/build.ts +++ b/packages/cloudflare/src/cli/build/build.ts @@ -63,7 +63,7 @@ export async function build(projectOpts: ProjectOptions): Promise { logger.info(`@opennextjs/cloudflare version: ${cloudflare}`); logger.info(`@opennextjs/aws version: ${aws}`); - if (projectOpts.populateCache?.onlyPopulate) { + if (projectOpts.populateCache?.onlyPopulateWithoutBuilding) { populateCache(options, config, projectOpts.populateCache.mode); return; } diff --git a/packages/cloudflare/src/cli/project-options.ts b/packages/cloudflare/src/cli/project-options.ts index f83beef4d..15b23259d 100644 --- a/packages/cloudflare/src/cli/project-options.ts +++ b/packages/cloudflare/src/cli/project-options.ts @@ -11,5 +11,5 @@ export type ProjectOptions = { skipWranglerConfigCheck: boolean; // Whether minification of the worker should be enabled minify: boolean; - populateCache?: { mode: CacheBindingMode; onlyPopulate: boolean }; + populateCache?: { mode: CacheBindingMode; onlyPopulateWithoutBuilding: boolean }; }; From 46d8047e8abcc46cc7d6b6a752978216e944705e Mon Sep 17 00:00:00 2001 From: James Date: Sun, 9 Mar 2025 00:14:27 +0000 Subject: [PATCH 06/19] feat: r2 adapter for the incremental cache --- .changeset/weak-houses-divide.md | 5 + examples/e2e/app-router/open-next.config.ts | 5 +- examples/e2e/app-router/wrangler.jsonc | 7 + .../cloudflare/src/api/cloudflare-context.ts | 2 + .../src/api/r2-incremental-cache.ts | 171 ++++++++++++++++++ .../src/cli/build/utils/populate-cache.ts | 61 ++++++- 6 files changed, 243 insertions(+), 8 deletions(-) create mode 100644 .changeset/weak-houses-divide.md create mode 100644 packages/cloudflare/src/api/r2-incremental-cache.ts diff --git a/.changeset/weak-houses-divide.md b/.changeset/weak-houses-divide.md new file mode 100644 index 000000000..ec7f9db41 --- /dev/null +++ b/.changeset/weak-houses-divide.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/cloudflare": patch +--- + +feat: r2 adapter for the incremental cache diff --git a/examples/e2e/app-router/open-next.config.ts b/examples/e2e/app-router/open-next.config.ts index 00db54284..e01a5d58c 100644 --- a/examples/e2e/app-router/open-next.config.ts +++ b/examples/e2e/app-router/open-next.config.ts @@ -1,10 +1,11 @@ import { defineCloudflareConfig } from "@opennextjs/cloudflare"; import d1TagCache from "@opennextjs/cloudflare/d1-tag-cache"; -import kvIncrementalCache from "@opennextjs/cloudflare/kv-cache"; +// import kvIncrementalCache from "@opennextjs/cloudflare/kv-cache"; +import r2IncrementalCache from "@opennextjs/cloudflare/r2-incremental-cache"; import memoryQueue from "@opennextjs/cloudflare/memory-queue"; export default defineCloudflareConfig({ - incrementalCache: kvIncrementalCache, + incrementalCache: r2IncrementalCache, tagCache: d1TagCache, queue: memoryQueue, }); diff --git a/examples/e2e/app-router/wrangler.jsonc b/examples/e2e/app-router/wrangler.jsonc index 25be2dde5..f7de1ca39 100644 --- a/examples/e2e/app-router/wrangler.jsonc +++ b/examples/e2e/app-router/wrangler.jsonc @@ -26,5 +26,12 @@ "binding": "NEXT_CACHE_REVALIDATION_WORKER", "service": "app-router" } + ], + "r2_buckets": [ + { + "binding": "NEXT_CACHE_R2_BUCKET", + "bucket_name": "NEXT_CACHE_R2_BUCKET", + "preview_bucket_name": "NEXT_CACHE_R2_BUCKET" + } ] } diff --git a/packages/cloudflare/src/api/cloudflare-context.ts b/packages/cloudflare/src/api/cloudflare-context.ts index f13a6a5b6..6e866b52c 100644 --- a/packages/cloudflare/src/api/cloudflare-context.ts +++ b/packages/cloudflare/src/api/cloudflare-context.ts @@ -7,6 +7,8 @@ declare global { NEXT_CACHE_D1_TAGS_TABLE?: string; NEXT_CACHE_D1_REVALIDATIONS_TABLE?: string; NEXT_CACHE_REVALIDATION_WORKER?: Service; + NEXT_CACHE_R2_BUCKET?: R2Bucket; + NEXT_CACHE_R2_DIRECTORY?: string; ASSETS?: Fetcher; } } diff --git a/packages/cloudflare/src/api/r2-incremental-cache.ts b/packages/cloudflare/src/api/r2-incremental-cache.ts new file mode 100644 index 000000000..78bfb807d --- /dev/null +++ b/packages/cloudflare/src/api/r2-incremental-cache.ts @@ -0,0 +1,171 @@ +import { debug, error } from "@opennextjs/aws/adapters/logger.js"; +import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js"; +import { IgnorableError } from "@opennextjs/aws/utils/error.js"; + +import { getCloudflareContext } from "./cloudflare-context.js"; + +type Entry = { + value: CacheValue; + lastModified: number; +}; + +const ONE_YEAR_IN_SECONDS = 31536000; + +/** + * An instance of the Incremental Cache that uses an R2 bucket (`NEXT_CACHE_R2_BUCKET`) as it's + * underlying data store. + * + * The directory that the cache entries are stored in can be confused with the `NEXT_CACHE_R2_DIRECTORY` + * environment variable, and defaults to `incremental-cache`. + * + * The cache uses an instance of the Cache API (`incremental-cache`) to store a local version of the + * R2 cache entry to enable fast retrieval, with the cache being updated from R2 in the background. + */ +class R2IncrementalCache implements IncrementalCache { + readonly name = "r2-incremental-cache"; + + protected localCache: Cache | undefined; + + async get( + key: string, + isFetch?: IsFetch + ): Promise> | null> { + const r2 = getCloudflareContext().env.NEXT_CACHE_R2_BUCKET; + if (!r2) throw new IgnorableError("No R2 bucket"); + + debug(`Get ${key}`); + + try { + const r2Response = r2.get(this.getR2Key(key)); + + const localCacheKey = this.getLocalCacheKey(key, isFetch); + + // Check for a cached entry as this will be faster than R2. + const cachedResponse = await this.getFromLocalCache(localCacheKey); + if (cachedResponse) { + debug(` -> Cached response`); + // Update the local cache after the R2 fetch has completed. + getCloudflareContext().ctx.waitUntil( + Promise.resolve(r2Response).then(async (res) => { + if (res) { + const entry: Entry = await res.json(); + await this.putToLocalCache(localCacheKey, JSON.stringify(entry), entry.value.revalidate); + } + }) + ); + + return cachedResponse.json(); + } + + const r2Object = await r2Response; + if (!r2Object) return null; + const entry: Entry = await r2Object.json(); + + // Update the locale cache after retrieving from R2. + getCloudflareContext().ctx.waitUntil( + this.putToLocalCache(localCacheKey, JSON.stringify(entry), entry.value.revalidate) + ); + + return entry; + } catch (e) { + error(`Failed to get from cache`, e); + return null; + } + } + + async set( + key: string, + value: CacheValue, + isFetch?: IsFetch + ): Promise { + const r2 = getCloudflareContext().env.NEXT_CACHE_R2_BUCKET; + if (!r2) throw new IgnorableError("No R2 bucket"); + + debug(`Set ${key}`); + + try { + const entry: Entry = { + value, + // Note: `Date.now()` returns the time of the last IO rather than the actual time. + // See https://developers.cloudflare.com/workers/reference/security-model/ + lastModified: Date.now(), + }; + + await Promise.all([ + r2.put(this.getR2Key(key, isFetch), JSON.stringify(entry)), + // Update the locale cache for faster retrieval. + this.putToLocalCache( + this.getLocalCacheKey(key, isFetch), + JSON.stringify(entry), + entry.value.revalidate + ), + ]); + } catch (e) { + error(`Failed to set to cache`, e); + } + } + + async delete(key: string): Promise { + const r2 = getCloudflareContext().env.NEXT_CACHE_R2_BUCKET; + if (!r2) throw new IgnorableError("No R2 bucket"); + + debug(`Delete ${key}`); + + try { + await Promise.all([ + r2.delete(this.getR2Key(key)), + this.deleteFromLocalCache(this.getLocalCacheKey(key)), + ]); + } catch (e) { + error(`Failed to delete from cache`, e); + } + } + + protected getBaseCacheKey(key: string, isFetch?: boolean): string { + return `${process.env.NEXT_BUILD_ID ?? "no-build-id"}/${key}.${isFetch ? "fetch" : "cache"}`; + } + + protected getR2Key(key: string, isFetch?: boolean): string { + const directory = getCloudflareContext().env.NEXT_CACHE_R2_DIRECTORY ?? "incremental-cache"; + return `${directory}/${this.getBaseCacheKey(key, isFetch)}`; + } + + protected getLocalCacheKey(key: string, isFetch?: boolean) { + return new Request(new URL(this.getBaseCacheKey(key, isFetch), "http://cache.local")); + } + + protected async getLocalCacheInstance(): Promise { + if (this.localCache) return this.localCache; + + this.localCache = await caches.open("incremental-cache"); + return this.localCache; + } + + protected async getFromLocalCache(key: Request) { + const cache = await this.getLocalCacheInstance(); + return cache.match(key); + } + + protected async putToLocalCache( + key: Request, + entry: string, + revalidate: number | false | undefined + ): Promise { + const cache = await this.getLocalCacheInstance(); + await cache.put( + key, + new Response(entry, { + headers: new Headers({ + "cache-control": `max-age=${revalidate || ONE_YEAR_IN_SECONDS}`, + }), + }) + ); + } + + protected async deleteFromLocalCache(key: Request) { + const cache = await this.getLocalCacheInstance(); + await cache.delete(key); + } +} + +export default new R2IncrementalCache(); diff --git a/packages/cloudflare/src/cli/build/utils/populate-cache.ts b/packages/cloudflare/src/cli/build/utils/populate-cache.ts index ad38ede79..d8545cbdd 100644 --- a/packages/cloudflare/src/cli/build/utils/populate-cache.ts +++ b/packages/cloudflare/src/cli/build/utils/populate-cache.ts @@ -11,6 +11,7 @@ import type { OpenNextConfig, } from "@opennextjs/aws/types/open-next.js"; import type { IncrementalCache, TagCache } from "@opennextjs/aws/types/overrides.js"; +import { globSync } from "glob"; export type CacheBindingMode = "local" | "remote"; @@ -24,10 +25,20 @@ async function resolveCacheName( return typeof value === "function" ? (await value()).name : value; } -function runWrangler(opts: BuildOptions, mode: CacheBindingMode, args: string[]) { +function runWrangler( + opts: BuildOptions, + wranglerOpts: { mode: CacheBindingMode; excludeRemoteFlag?: boolean }, + args: string[] +) { const result = spawnSync( opts.packager, - ["exec", "wrangler", ...args, mode === "remote" && "--remote"].filter((v): v is string => !!v), + [ + "exec", + "wrangler", + ...args, + wranglerOpts.mode === "remote" && !wranglerOpts.excludeRemoteFlag && "--remote", + wranglerOpts.mode === "local" && "--local", + ].filter((v): v is string => !!v), { shell: true, stdio: ["ignore", "ignore", "inherit"], @@ -37,11 +48,24 @@ function runWrangler(opts: BuildOptions, mode: CacheBindingMode, args: string[]) if (result.status !== 0) { logger.error("Failed to populate cache"); process.exit(1); - } else { - logger.info("Successfully populated cache"); } } +function getCacheAssetPaths(opts: BuildOptions) { + return globSync(path.join(opts.outputDir, "cache/**/*"), { withFileTypes: true }) + .filter((f) => f.isFile()) + .map((f) => { + const relativePath = path.relative(path.join(opts.outputDir, "cache"), f.fullpathPosix()); + + return { + fsPath: f.fullpathPosix(), + destPath: relativePath.startsWith("__fetch") + ? `${relativePath.replace("__fetch/", "")}.fetch` + : relativePath, + }; + }); +} + export async function populateCache(opts: BuildOptions, config: OpenNextConfig, mode: CacheBindingMode) { const { incrementalCache, tagCache } = config.default.override ?? {}; @@ -51,7 +75,31 @@ export async function populateCache(opts: BuildOptions, config: OpenNextConfig, } if (!config.dangerous?.disableIncrementalCache && incrementalCache) { - logger.info("Incremental cache does not need populating"); + const name = await resolveCacheName(incrementalCache); + switch (name) { + case "r2-incremental-cache": { + logger.info("\nPopulating R2 incremental cache..."); + + const assets = getCacheAssetPaths(opts); + assets.forEach(({ fsPath, destPath }) => { + const fullDestPath = path.join( + "NEXT_CACHE_R2_BUCKET", + process.env.NEXT_CACHE_R2_DIRECTORY ?? "incremental-cache", + destPath + ); + + runWrangler(opts, { mode, excludeRemoteFlag: true }, [ + "r2 object put", + JSON.stringify(fullDestPath), + `--file ${JSON.stringify(fsPath)}`, + ]); + }); + logger.info(`Successfully populated cache with ${assets.length} assets`); + break; + } + default: + logger.info("INcremental cache does not need populating"); + } } if (!config.dangerous?.disableTagCache && !config.dangerous?.disableIncrementalCache && tagCache) { @@ -60,11 +108,12 @@ export async function populateCache(opts: BuildOptions, config: OpenNextConfig, case "d1-tag-cache": { logger.info("\nPopulating D1 tag cache..."); - runWrangler(opts, mode, [ + runWrangler(opts, { mode }, [ "d1 execute", "NEXT_CACHE_D1", `--file ${JSON.stringify(path.join(opts.outputDir, "cloudflare/cache-assets-manifest.sql"))}`, ]); + logger.info("Successfully populated cache"); break; } default: From ecc83aa94dbef6668ceeb64b3a6c4dcecf6fbbce Mon Sep 17 00:00:00 2001 From: James Date: Wed, 12 Mar 2025 23:43:31 +0000 Subject: [PATCH 07/19] directory -> prefix --- packages/cloudflare/src/api/cloudflare-context.ts | 2 +- packages/cloudflare/src/api/r2-incremental-cache.ts | 4 ++-- packages/cloudflare/src/cli/build/utils/populate-cache.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cloudflare/src/api/cloudflare-context.ts b/packages/cloudflare/src/api/cloudflare-context.ts index 6e866b52c..d7c039974 100644 --- a/packages/cloudflare/src/api/cloudflare-context.ts +++ b/packages/cloudflare/src/api/cloudflare-context.ts @@ -8,7 +8,7 @@ declare global { NEXT_CACHE_D1_REVALIDATIONS_TABLE?: string; NEXT_CACHE_REVALIDATION_WORKER?: Service; NEXT_CACHE_R2_BUCKET?: R2Bucket; - NEXT_CACHE_R2_DIRECTORY?: string; + NEXT_CACHE_R2_PREFIX?: string; ASSETS?: Fetcher; } } diff --git a/packages/cloudflare/src/api/r2-incremental-cache.ts b/packages/cloudflare/src/api/r2-incremental-cache.ts index 78bfb807d..9f41fcf97 100644 --- a/packages/cloudflare/src/api/r2-incremental-cache.ts +++ b/packages/cloudflare/src/api/r2-incremental-cache.ts @@ -15,7 +15,7 @@ const ONE_YEAR_IN_SECONDS = 31536000; * An instance of the Incremental Cache that uses an R2 bucket (`NEXT_CACHE_R2_BUCKET`) as it's * underlying data store. * - * The directory that the cache entries are stored in can be confused with the `NEXT_CACHE_R2_DIRECTORY` + * The directory that the cache entries are stored in can be confused with the `NEXT_CACHE_R2_PREFIX` * environment variable, and defaults to `incremental-cache`. * * The cache uses an instance of the Cache API (`incremental-cache`) to store a local version of the @@ -126,7 +126,7 @@ class R2IncrementalCache implements IncrementalCache { } protected getR2Key(key: string, isFetch?: boolean): string { - const directory = getCloudflareContext().env.NEXT_CACHE_R2_DIRECTORY ?? "incremental-cache"; + const directory = getCloudflareContext().env.NEXT_CACHE_R2_PREFIX ?? "incremental-cache"; return `${directory}/${this.getBaseCacheKey(key, isFetch)}`; } diff --git a/packages/cloudflare/src/cli/build/utils/populate-cache.ts b/packages/cloudflare/src/cli/build/utils/populate-cache.ts index d8545cbdd..f678ffcbf 100644 --- a/packages/cloudflare/src/cli/build/utils/populate-cache.ts +++ b/packages/cloudflare/src/cli/build/utils/populate-cache.ts @@ -84,7 +84,7 @@ export async function populateCache(opts: BuildOptions, config: OpenNextConfig, assets.forEach(({ fsPath, destPath }) => { const fullDestPath = path.join( "NEXT_CACHE_R2_BUCKET", - process.env.NEXT_CACHE_R2_DIRECTORY ?? "incremental-cache", + process.env.NEXT_CACHE_R2_PREFIX ?? "incremental-cache", destPath ); From c13ca95c8ba6656ea44804795fd1072faec5f310 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 13 Mar 2025 00:42:21 +0000 Subject: [PATCH 08/19] split r2 cache and regional cache into separate things --- examples/e2e/app-router/open-next.config.ts | 3 +- .../src/api/internal/incremental-cache.ts | 6 + .../src/api/r2-incremental-cache.ts | 106 ++------------ packages/cloudflare/src/api/regional-cache.ts | 133 ++++++++++++++++++ 4 files changed, 151 insertions(+), 97 deletions(-) create mode 100644 packages/cloudflare/src/api/internal/incremental-cache.ts create mode 100644 packages/cloudflare/src/api/regional-cache.ts diff --git a/examples/e2e/app-router/open-next.config.ts b/examples/e2e/app-router/open-next.config.ts index e01a5d58c..81590e11a 100644 --- a/examples/e2e/app-router/open-next.config.ts +++ b/examples/e2e/app-router/open-next.config.ts @@ -3,9 +3,10 @@ import d1TagCache from "@opennextjs/cloudflare/d1-tag-cache"; // import kvIncrementalCache from "@opennextjs/cloudflare/kv-cache"; import r2IncrementalCache from "@opennextjs/cloudflare/r2-incremental-cache"; import memoryQueue from "@opennextjs/cloudflare/memory-queue"; +import { withRegionalCache } from "@opennextjs/cloudflare/regional-cache"; export default defineCloudflareConfig({ - incrementalCache: r2IncrementalCache, + incrementalCache: withRegionalCache(r2IncrementalCache, { mode: "long-lived" }), tagCache: d1TagCache, queue: memoryQueue, }); diff --git a/packages/cloudflare/src/api/internal/incremental-cache.ts b/packages/cloudflare/src/api/internal/incremental-cache.ts new file mode 100644 index 000000000..2407fef8a --- /dev/null +++ b/packages/cloudflare/src/api/internal/incremental-cache.ts @@ -0,0 +1,6 @@ +import { CacheValue } from "@opennextjs/aws/types/overrides.js"; + +export type IncrementalCacheEntry = { + value: CacheValue; + lastModified: number; +}; diff --git a/packages/cloudflare/src/api/r2-incremental-cache.ts b/packages/cloudflare/src/api/r2-incremental-cache.ts index 9f41fcf97..f230a9eb3 100644 --- a/packages/cloudflare/src/api/r2-incremental-cache.ts +++ b/packages/cloudflare/src/api/r2-incremental-cache.ts @@ -3,13 +3,7 @@ import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs import { IgnorableError } from "@opennextjs/aws/utils/error.js"; import { getCloudflareContext } from "./cloudflare-context.js"; - -type Entry = { - value: CacheValue; - lastModified: number; -}; - -const ONE_YEAR_IN_SECONDS = 31536000; +import { IncrementalCacheEntry } from "./internal/incremental-cache.js"; /** * An instance of the Incremental Cache that uses an R2 bucket (`NEXT_CACHE_R2_BUCKET`) as it's @@ -24,8 +18,6 @@ const ONE_YEAR_IN_SECONDS = 31536000; class R2IncrementalCache implements IncrementalCache { readonly name = "r2-incremental-cache"; - protected localCache: Cache | undefined; - async get( key: string, isFetch?: IsFetch @@ -36,39 +28,12 @@ class R2IncrementalCache implements IncrementalCache { debug(`Get ${key}`); try { - const r2Response = r2.get(this.getR2Key(key)); - - const localCacheKey = this.getLocalCacheKey(key, isFetch); - - // Check for a cached entry as this will be faster than R2. - const cachedResponse = await this.getFromLocalCache(localCacheKey); - if (cachedResponse) { - debug(` -> Cached response`); - // Update the local cache after the R2 fetch has completed. - getCloudflareContext().ctx.waitUntil( - Promise.resolve(r2Response).then(async (res) => { - if (res) { - const entry: Entry = await res.json(); - await this.putToLocalCache(localCacheKey, JSON.stringify(entry), entry.value.revalidate); - } - }) - ); - - return cachedResponse.json(); - } - - const r2Object = await r2Response; + const r2Object = await r2.get(this.getR2Key(key, isFetch)); if (!r2Object) return null; - const entry: Entry = await r2Object.json(); - // Update the locale cache after retrieving from R2. - getCloudflareContext().ctx.waitUntil( - this.putToLocalCache(localCacheKey, JSON.stringify(entry), entry.value.revalidate) - ); - - return entry; + return r2Object.json(); } catch (e) { - error(`Failed to get from cache`, e); + error("Failed to get from cache", e); return null; } } @@ -84,24 +49,16 @@ class R2IncrementalCache implements IncrementalCache { debug(`Set ${key}`); try { - const entry: Entry = { + const entry: IncrementalCacheEntry = { value, // Note: `Date.now()` returns the time of the last IO rather than the actual time. // See https://developers.cloudflare.com/workers/reference/security-model/ lastModified: Date.now(), }; - await Promise.all([ - r2.put(this.getR2Key(key, isFetch), JSON.stringify(entry)), - // Update the locale cache for faster retrieval. - this.putToLocalCache( - this.getLocalCacheKey(key, isFetch), - JSON.stringify(entry), - entry.value.revalidate - ), - ]); + await r2.put(this.getR2Key(key, isFetch), JSON.stringify(entry)); } catch (e) { - error(`Failed to set to cache`, e); + error("Failed to set to cache", e); } } @@ -112,59 +69,16 @@ class R2IncrementalCache implements IncrementalCache { debug(`Delete ${key}`); try { - await Promise.all([ - r2.delete(this.getR2Key(key)), - this.deleteFromLocalCache(this.getLocalCacheKey(key)), - ]); + await r2.delete(this.getR2Key(key)); } catch (e) { - error(`Failed to delete from cache`, e); + error("Failed to delete from cache", e); } } - protected getBaseCacheKey(key: string, isFetch?: boolean): string { - return `${process.env.NEXT_BUILD_ID ?? "no-build-id"}/${key}.${isFetch ? "fetch" : "cache"}`; - } - protected getR2Key(key: string, isFetch?: boolean): string { const directory = getCloudflareContext().env.NEXT_CACHE_R2_PREFIX ?? "incremental-cache"; - return `${directory}/${this.getBaseCacheKey(key, isFetch)}`; - } - - protected getLocalCacheKey(key: string, isFetch?: boolean) { - return new Request(new URL(this.getBaseCacheKey(key, isFetch), "http://cache.local")); - } - - protected async getLocalCacheInstance(): Promise { - if (this.localCache) return this.localCache; - - this.localCache = await caches.open("incremental-cache"); - return this.localCache; - } - - protected async getFromLocalCache(key: Request) { - const cache = await this.getLocalCacheInstance(); - return cache.match(key); - } - - protected async putToLocalCache( - key: Request, - entry: string, - revalidate: number | false | undefined - ): Promise { - const cache = await this.getLocalCacheInstance(); - await cache.put( - key, - new Response(entry, { - headers: new Headers({ - "cache-control": `max-age=${revalidate || ONE_YEAR_IN_SECONDS}`, - }), - }) - ); - } - protected async deleteFromLocalCache(key: Request) { - const cache = await this.getLocalCacheInstance(); - await cache.delete(key); + return `${directory}/${process.env.NEXT_BUILD_ID ?? "no-build-id"}/${key}.${isFetch ? "fetch" : "cache"}`; } } diff --git a/packages/cloudflare/src/api/regional-cache.ts b/packages/cloudflare/src/api/regional-cache.ts new file mode 100644 index 000000000..552242218 --- /dev/null +++ b/packages/cloudflare/src/api/regional-cache.ts @@ -0,0 +1,133 @@ +import { debug, error } from "@opennextjs/aws/adapters/logger.js"; +import { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js"; + +import { getCloudflareContext } from "./cloudflare-context.js"; +import { IncrementalCacheEntry } from "./internal/incremental-cache.js"; + +const ONE_YEAR_IN_SECONDS = 31536000; +const ONE_MINUTE_IN_SECONDS = 60; + +type Options = { + mode: "short-lived" | "long-lived"; +}; + +class RegionalCache implements IncrementalCache { + public name: string; + + protected localCache: Cache | undefined; + + constructor( + private store: IncrementalCache, + private opts: Options + ) { + this.name = this.store.name; + } + + async get( + key: string, + isFetch?: IsFetch + ): Promise> | null> { + try { + const storeResponse = this.store.get(key, isFetch); + + const localCacheKey = this.getCacheKey(key, isFetch); + + // Check for a cached entry as this will be faster than the store response. + const cache = await this.getCacheInstance(); + const cachedResponse = await cache.match(localCacheKey); + if (cachedResponse) { + debug("Get - cached response"); + + // Update the local cache after the R2 fetch has completed. + getCloudflareContext().ctx.waitUntil( + Promise.resolve(storeResponse).then(async (rawEntry) => { + const { value, lastModified } = rawEntry ?? {}; + + if (value && typeof lastModified === "number") { + await this.putToCache(localCacheKey, { value, lastModified }); + } + }) + ); + + return cachedResponse.json(); + } + + const rawEntry = await storeResponse; + const { value, lastModified } = rawEntry ?? {}; + if (!value || typeof lastModified !== "number") return null; + + // Update the locale cache after retrieving from the store. + getCloudflareContext().ctx.waitUntil(this.putToCache(localCacheKey, { value, lastModified })); + + return { value, lastModified }; + } catch (e) { + error("Failed to get from regional cache", e); + return null; + } + } + + async set( + key: string, + value: CacheValue, + isFetch?: IsFetch + ): Promise { + try { + await Promise.all([ + this.store.set(key, value, isFetch), + this.putToCache(this.getCacheKey(key, isFetch), { + value, + // Note: `Date.now()` returns the time of the last IO rather than the actual time. + // See https://developers.cloudflare.com/workers/reference/security-model/ + lastModified: Date.now(), + }), + ]); + } catch (e) { + error(`Failed to get from regional cache`, e); + } + } + + async delete(key: string): Promise { + try { + const cache = await this.getCacheInstance(); + await Promise.all([this.store.delete(key), cache.delete(this.getCacheKey(key))]); + } catch (e) { + error("Failed to delete from regional cache", e); + } + } + + protected async getCacheInstance(): Promise { + if (this.localCache) return this.localCache; + + this.localCache = await caches.open("incremental-cache"); + return this.localCache; + } + + protected getCacheKey(key: string, isFetch?: boolean) { + return new Request( + new URL( + `${process.env.NEXT_BUILD_ID ?? "no-build-id"}/${key}.${isFetch ? "fetch" : "cache"}`, + "http://cache.local" + ) + ); + } + + protected async putToCache(key: Request, entry: IncrementalCacheEntry): Promise { + const cache = await this.getCacheInstance(); + + const age = + this.opts.mode === "short-lived" + ? ONE_MINUTE_IN_SECONDS + : entry.value.revalidate || ONE_YEAR_IN_SECONDS; + + await cache.put( + key, + new Response(JSON.stringify(entry), { + headers: new Headers({ "cache-control": `max-age=${age}` }), + }) + ); + } +} + +export function withRegionalCache(cache: IncrementalCache, opts: Options) { + return new RegionalCache(cache, opts); +} From 28c27a954b1f80b4d0a448f36cce74dc7d791d75 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 13 Mar 2025 00:42:41 +0000 Subject: [PATCH 09/19] add timeout in revalidate path test as it runs too fast locally From e018c711292ec346d2c715d377dde72ac023b603 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 13 Mar 2025 20:16:44 +0000 Subject: [PATCH 10/19] incorporate suggestions --- .../src/api/r2-incremental-cache.ts | 15 +++++-------- packages/cloudflare/src/api/regional-cache.ts | 21 ++++++++++--------- .../src/cli/build/utils/populate-cache.ts | 2 +- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/cloudflare/src/api/r2-incremental-cache.ts b/packages/cloudflare/src/api/r2-incremental-cache.ts index f230a9eb3..4ab451982 100644 --- a/packages/cloudflare/src/api/r2-incremental-cache.ts +++ b/packages/cloudflare/src/api/r2-incremental-cache.ts @@ -3,7 +3,6 @@ import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs import { IgnorableError } from "@opennextjs/aws/utils/error.js"; import { getCloudflareContext } from "./cloudflare-context.js"; -import { IncrementalCacheEntry } from "./internal/incremental-cache.js"; /** * An instance of the Incremental Cache that uses an R2 bucket (`NEXT_CACHE_R2_BUCKET`) as it's @@ -31,7 +30,10 @@ class R2IncrementalCache implements IncrementalCache { const r2Object = await r2.get(this.getR2Key(key, isFetch)); if (!r2Object) return null; - return r2Object.json(); + return { + value: await r2Object.json(), + lastModified: r2Object.uploaded.getTime(), + }; } catch (e) { error("Failed to get from cache", e); return null; @@ -49,14 +51,7 @@ class R2IncrementalCache implements IncrementalCache { debug(`Set ${key}`); try { - const entry: IncrementalCacheEntry = { - value, - // Note: `Date.now()` returns the time of the last IO rather than the actual time. - // See https://developers.cloudflare.com/workers/reference/security-model/ - lastModified: Date.now(), - }; - - await r2.put(this.getR2Key(key, isFetch), JSON.stringify(entry)); + await r2.put(this.getR2Key(key, isFetch), JSON.stringify(value)); } catch (e) { error("Failed to set to cache", e); } diff --git a/packages/cloudflare/src/api/regional-cache.ts b/packages/cloudflare/src/api/regional-cache.ts index 552242218..5877231e7 100644 --- a/packages/cloudflare/src/api/regional-cache.ts +++ b/packages/cloudflare/src/api/regional-cache.ts @@ -72,15 +72,14 @@ class RegionalCache implements IncrementalCache { isFetch?: IsFetch ): Promise { try { - await Promise.all([ - this.store.set(key, value, isFetch), - this.putToCache(this.getCacheKey(key, isFetch), { - value, - // Note: `Date.now()` returns the time of the last IO rather than the actual time. - // See https://developers.cloudflare.com/workers/reference/security-model/ - lastModified: Date.now(), - }), - ]); + await this.store.set(key, value, isFetch); + + await this.putToCache(this.getCacheKey(key, isFetch), { + value, + // Note: `Date.now()` returns the time of the last IO rather than the actual time. + // See https://developers.cloudflare.com/workers/reference/security-model/ + lastModified: Date.now(), + }); } catch (e) { error(`Failed to get from regional cache`, e); } @@ -88,8 +87,10 @@ class RegionalCache implements IncrementalCache { async delete(key: string): Promise { try { + await this.store.delete(key); + const cache = await this.getCacheInstance(); - await Promise.all([this.store.delete(key), cache.delete(this.getCacheKey(key))]); + await cache.delete(this.getCacheKey(key)); } catch (e) { error("Failed to delete from regional cache", e); } diff --git a/packages/cloudflare/src/cli/build/utils/populate-cache.ts b/packages/cloudflare/src/cli/build/utils/populate-cache.ts index f678ffcbf..d886d975e 100644 --- a/packages/cloudflare/src/cli/build/utils/populate-cache.ts +++ b/packages/cloudflare/src/cli/build/utils/populate-cache.ts @@ -98,7 +98,7 @@ export async function populateCache(opts: BuildOptions, config: OpenNextConfig, break; } default: - logger.info("INcremental cache does not need populating"); + logger.info("Incremental cache does not need populating"); } } From a2b26670c93cbb2f65030ed4b74f5a7d9372a65c Mon Sep 17 00:00:00 2001 From: James Date: Thu, 13 Mar 2025 20:33:26 +0000 Subject: [PATCH 11/19] lazily update the regional cache in the background via option --- examples/e2e/app-router/open-next.config.ts | 5 +++- packages/cloudflare/src/api/regional-cache.ts | 27 ++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/examples/e2e/app-router/open-next.config.ts b/examples/e2e/app-router/open-next.config.ts index 81590e11a..2f9054793 100644 --- a/examples/e2e/app-router/open-next.config.ts +++ b/examples/e2e/app-router/open-next.config.ts @@ -6,7 +6,10 @@ import memoryQueue from "@opennextjs/cloudflare/memory-queue"; import { withRegionalCache } from "@opennextjs/cloudflare/regional-cache"; export default defineCloudflareConfig({ - incrementalCache: withRegionalCache(r2IncrementalCache, { mode: "long-lived" }), + incrementalCache: withRegionalCache(r2IncrementalCache, { + mode: "long-lived", + shouldLazilyUpdateOnCacheHit: true, + }), tagCache: d1TagCache, queue: memoryQueue, }); diff --git a/packages/cloudflare/src/api/regional-cache.ts b/packages/cloudflare/src/api/regional-cache.ts index 5877231e7..ccc0deb76 100644 --- a/packages/cloudflare/src/api/regional-cache.ts +++ b/packages/cloudflare/src/api/regional-cache.ts @@ -9,6 +9,7 @@ const ONE_MINUTE_IN_SECONDS = 60; type Options = { mode: "short-lived" | "long-lived"; + shouldLazilyUpdateOnCacheHit?: boolean; }; class RegionalCache implements IncrementalCache { @@ -28,31 +29,31 @@ class RegionalCache implements IncrementalCache { isFetch?: IsFetch ): Promise> | null> { try { - const storeResponse = this.store.get(key, isFetch); - + const cache = await this.getCacheInstance(); const localCacheKey = this.getCacheKey(key, isFetch); // Check for a cached entry as this will be faster than the store response. - const cache = await this.getCacheInstance(); const cachedResponse = await cache.match(localCacheKey); if (cachedResponse) { debug("Get - cached response"); - // Update the local cache after the R2 fetch has completed. - getCloudflareContext().ctx.waitUntil( - Promise.resolve(storeResponse).then(async (rawEntry) => { - const { value, lastModified } = rawEntry ?? {}; + // Re-fetch from the store and update the regional cache in the background + if (this.opts.shouldLazilyUpdateOnCacheHit) { + getCloudflareContext().ctx.waitUntil( + this.store.get(key, isFetch).then(async (rawEntry) => { + const { value, lastModified } = rawEntry ?? {}; - if (value && typeof lastModified === "number") { - await this.putToCache(localCacheKey, { value, lastModified }); - } - }) - ); + if (value && typeof lastModified === "number") { + await this.putToCache(localCacheKey, { value, lastModified }); + } + }) + ); + } return cachedResponse.json(); } - const rawEntry = await storeResponse; + const rawEntry = await this.store.get(key, isFetch); const { value, lastModified } = rawEntry ?? {}; if (!value || typeof lastModified !== "number") return null; From 03ffc01138cf03e92f0b9cbd1e5f0d23bf447728 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 13 Mar 2025 20:46:21 +0000 Subject: [PATCH 12/19] add comments and make the lazy boolean non-optional --- packages/cloudflare/src/api/regional-cache.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/cloudflare/src/api/regional-cache.ts b/packages/cloudflare/src/api/regional-cache.ts index ccc0deb76..bd434b851 100644 --- a/packages/cloudflare/src/api/regional-cache.ts +++ b/packages/cloudflare/src/api/regional-cache.ts @@ -8,8 +8,18 @@ const ONE_YEAR_IN_SECONDS = 31536000; const ONE_MINUTE_IN_SECONDS = 60; type Options = { + /** + * The mode to use for the regional cache. + * + * - `short-lived`: Re-use a cache entry for up to a minute after it has been retrieved. + * - `long-lived`: Re-use a cache entry until it is revalidated. + */ mode: "short-lived" | "long-lived"; - shouldLazilyUpdateOnCacheHit?: boolean; + /** + * Whether the regional cache entry should be updated in the background or not when it experiences + * a cache hit. + */ + shouldLazilyUpdateOnCacheHit: boolean; }; class RegionalCache implements IncrementalCache { @@ -130,6 +140,19 @@ class RegionalCache implements IncrementalCache { } } +/** + * A regional cache will wrap an incremental cache and provide faster cache lookups for an entry + * when making requests within the region. + * + * The regional cache uses the Cache API. + * + * @param cache - Incremental cache instance. + * @param opts.mode - The mode to use for the regional cache. + * - `short-lived`: Re-use a cache entry for up to a minute after it has been retrieved. + * - `long-lived`: Re-use a cache entry until it is revalidated. + * @param opts.shouldLazilyUpdateOnCacheHit - Whether the regional cache entry should be updated in + * the background or not when it experiences a cache hit. + */ export function withRegionalCache(cache: IncrementalCache, opts: Options) { return new RegionalCache(cache, opts); } From 3c3b7f3105d4ef4b739dc7401bd5f5315e7141a9 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 13 Mar 2025 20:48:52 +0000 Subject: [PATCH 13/19] add warning comment --- packages/cloudflare/src/api/regional-cache.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cloudflare/src/api/regional-cache.ts b/packages/cloudflare/src/api/regional-cache.ts index bd434b851..b55ed22b6 100644 --- a/packages/cloudflare/src/api/regional-cache.ts +++ b/packages/cloudflare/src/api/regional-cache.ts @@ -146,6 +146,9 @@ class RegionalCache implements IncrementalCache { * * The regional cache uses the Cache API. * + * **WARNING:** If an entry is revalidated in one region, it will trigger an additional revalidation if + * a request is made to another region that has an entry stored in its regional cache. + * * @param cache - Incremental cache instance. * @param opts.mode - The mode to use for the regional cache. * - `short-lived`: Re-use a cache entry for up to a minute after it has been retrieved. From d676d1d5ded28cd57c34bd498ab6fcc3fb4d631b Mon Sep 17 00:00:00 2001 From: James Date: Thu, 13 Mar 2025 20:51:38 +0000 Subject: [PATCH 14/19] add comments to env vars --- packages/cloudflare/src/api/cloudflare-context.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cloudflare/src/api/cloudflare-context.ts b/packages/cloudflare/src/api/cloudflare-context.ts index d7c039974..3a0328ca2 100644 --- a/packages/cloudflare/src/api/cloudflare-context.ts +++ b/packages/cloudflare/src/api/cloudflare-context.ts @@ -7,7 +7,9 @@ declare global { NEXT_CACHE_D1_TAGS_TABLE?: string; NEXT_CACHE_D1_REVALIDATIONS_TABLE?: string; NEXT_CACHE_REVALIDATION_WORKER?: Service; + // R2 bucket used for the incremental cache NEXT_CACHE_R2_BUCKET?: R2Bucket; + // Prefix used for the R2 incremental cache bucket NEXT_CACHE_R2_PREFIX?: string; ASSETS?: Fetcher; } From 0c1887e2251a8b8cb13e87ae054c89d170f14601 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 14 Mar 2025 22:36:07 +0000 Subject: [PATCH 15/19] change lazy update defaults --- packages/cloudflare/src/api/regional-cache.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/cloudflare/src/api/regional-cache.ts b/packages/cloudflare/src/api/regional-cache.ts index b55ed22b6..bf8056b19 100644 --- a/packages/cloudflare/src/api/regional-cache.ts +++ b/packages/cloudflare/src/api/regional-cache.ts @@ -18,8 +18,10 @@ type Options = { /** * Whether the regional cache entry should be updated in the background or not when it experiences * a cache hit. + * + * Defaults to `false` for the `short-lived` mode, and `true` for the `long-lived` mode. */ - shouldLazilyUpdateOnCacheHit: boolean; + shouldLazilyUpdateOnCacheHit?: boolean; }; class RegionalCache implements IncrementalCache { @@ -32,6 +34,8 @@ class RegionalCache implements IncrementalCache { private opts: Options ) { this.name = this.store.name; + + this.opts.shouldLazilyUpdateOnCacheHit ??= this.opts.mode === "long-lived"; } async get( @@ -155,6 +159,8 @@ class RegionalCache implements IncrementalCache { * - `long-lived`: Re-use a cache entry until it is revalidated. * @param opts.shouldLazilyUpdateOnCacheHit - Whether the regional cache entry should be updated in * the background or not when it experiences a cache hit. + * + * Defaults to `false` for the `short-lived` mode, and `true` for the `long-lived` mode. */ export function withRegionalCache(cache: IncrementalCache, opts: Options) { return new RegionalCache(cache, opts); From 5ff320b0e5ea97aab133cc3a500b556f8c69f849 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 20 Mar 2025 21:51:37 +0000 Subject: [PATCH 16/19] change fallback ttl to 30 mins --- packages/cloudflare/src/api/regional-cache.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cloudflare/src/api/regional-cache.ts b/packages/cloudflare/src/api/regional-cache.ts index bf8056b19..fbe8c7b4e 100644 --- a/packages/cloudflare/src/api/regional-cache.ts +++ b/packages/cloudflare/src/api/regional-cache.ts @@ -4,15 +4,15 @@ import { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/ import { getCloudflareContext } from "./cloudflare-context.js"; import { IncrementalCacheEntry } from "./internal/incremental-cache.js"; -const ONE_YEAR_IN_SECONDS = 31536000; const ONE_MINUTE_IN_SECONDS = 60; +const THIRTY_MINUTES_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 30; type Options = { /** * The mode to use for the regional cache. * * - `short-lived`: Re-use a cache entry for up to a minute after it has been retrieved. - * - `long-lived`: Re-use a cache entry until it is revalidated. + * - `long-lived`: Re-use a fetch cache entry until it is revalidated, or an ISR/SSG entry for up to 30 minutes. */ mode: "short-lived" | "long-lived"; /** @@ -133,7 +133,7 @@ class RegionalCache implements IncrementalCache { const age = this.opts.mode === "short-lived" ? ONE_MINUTE_IN_SECONDS - : entry.value.revalidate || ONE_YEAR_IN_SECONDS; + : entry.value.revalidate || THIRTY_MINUTES_IN_SECONDS; await cache.put( key, @@ -156,7 +156,7 @@ class RegionalCache implements IncrementalCache { * @param cache - Incremental cache instance. * @param opts.mode - The mode to use for the regional cache. * - `short-lived`: Re-use a cache entry for up to a minute after it has been retrieved. - * - `long-lived`: Re-use a cache entry until it is revalidated. + * - `long-lived`: Re-use a fetch cache entry until it is revalidated, or an ISR/SSG entry for up to 30 minutes. * @param opts.shouldLazilyUpdateOnCacheHit - Whether the regional cache entry should be updated in * the background or not when it experiences a cache hit. * From 039dc1b62e751e5df07821803088dd2ad697dad8 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 20 Mar 2025 22:35:07 +0000 Subject: [PATCH 17/19] add separate app for e2e --- examples/common/apps.ts | 1 + examples/e2e/app-router/open-next.config.ts | 9 +-- examples/e2e/app-router/wrangler.jsonc | 7 -- .../overrides/r2-incremental-cache/.gitignore | 47 +++++++++++ .../r2-incremental-cache/app/favicon.ico | Bin 0 -> 25931 bytes .../r2-incremental-cache/app/globals.css | 14 ++++ .../r2-incremental-cache/app/layout.tsx | 25 ++++++ .../r2-incremental-cache/app/page.module.css | 17 ++++ .../r2-incremental-cache/app/page.tsx | 16 ++++ .../r2-incremental-cache/e2e/base.spec.ts | 36 +++++++++ .../e2e/playwright.config.ts | 8 ++ .../r2-incremental-cache/next.config.ts | 11 +++ .../r2-incremental-cache/open-next.config.ts | 14 ++++ .../r2-incremental-cache/package.json | 29 +++++++ .../r2-incremental-cache/tsconfig.json | 27 +++++++ .../r2-incremental-cache/wrangler.jsonc | 31 ++++++++ pnpm-lock.yaml | 73 ++++++++++++------ 17 files changed, 326 insertions(+), 39 deletions(-) create mode 100644 examples/overrides/r2-incremental-cache/.gitignore create mode 100644 examples/overrides/r2-incremental-cache/app/favicon.ico create mode 100644 examples/overrides/r2-incremental-cache/app/globals.css create mode 100644 examples/overrides/r2-incremental-cache/app/layout.tsx create mode 100644 examples/overrides/r2-incremental-cache/app/page.module.css create mode 100644 examples/overrides/r2-incremental-cache/app/page.tsx create mode 100644 examples/overrides/r2-incremental-cache/e2e/base.spec.ts create mode 100644 examples/overrides/r2-incremental-cache/e2e/playwright.config.ts create mode 100644 examples/overrides/r2-incremental-cache/next.config.ts create mode 100644 examples/overrides/r2-incremental-cache/open-next.config.ts create mode 100644 examples/overrides/r2-incremental-cache/package.json create mode 100644 examples/overrides/r2-incremental-cache/tsconfig.json create mode 100644 examples/overrides/r2-incremental-cache/wrangler.jsonc diff --git a/examples/common/apps.ts b/examples/common/apps.ts index 46325bc9b..939e395f7 100644 --- a/examples/common/apps.ts +++ b/examples/common/apps.ts @@ -15,6 +15,7 @@ const apps = [ // overrides "d1-tag-next", "memory-queue", + "r2-incremental-cache", // bugs "gh-119", "gh-219", diff --git a/examples/e2e/app-router/open-next.config.ts b/examples/e2e/app-router/open-next.config.ts index c7bc579f6..bab543836 100644 --- a/examples/e2e/app-router/open-next.config.ts +++ b/examples/e2e/app-router/open-next.config.ts @@ -1,15 +1,10 @@ import { defineCloudflareConfig } from "@opennextjs/cloudflare"; -// import kvIncrementalCache from "@opennextjs/cloudflare/kv-cache"; +import kvIncrementalCache from "@opennextjs/cloudflare/kv-cache"; import shardedTagCache from "@opennextjs/cloudflare/do-sharded-tag-cache"; import doQueue from "@opennextjs/cloudflare/durable-queue"; -import r2IncrementalCache from "@opennextjs/cloudflare/r2-incremental-cache"; -import { withRegionalCache } from "@opennextjs/cloudflare/regional-cache"; export default defineCloudflareConfig({ - incrementalCache: withRegionalCache(r2IncrementalCache, { - mode: "long-lived", - shouldLazilyUpdateOnCacheHit: true, - }), + incrementalCache: kvIncrementalCache, tagCache: shardedTagCache({ numberOfShards: 12, regionalCache: true }), queue: doQueue, }); diff --git a/examples/e2e/app-router/wrangler.jsonc b/examples/e2e/app-router/wrangler.jsonc index 8c50442fa..abb7ff277 100644 --- a/examples/e2e/app-router/wrangler.jsonc +++ b/examples/e2e/app-router/wrangler.jsonc @@ -44,12 +44,5 @@ "binding": "NEXT_CACHE_REVALIDATION_WORKER", "service": "app-router" } - ], - "r2_buckets": [ - { - "binding": "NEXT_CACHE_R2_BUCKET", - "bucket_name": "NEXT_CACHE_R2_BUCKET", - "preview_bucket_name": "NEXT_CACHE_R2_BUCKET" - } ] } diff --git a/examples/overrides/r2-incremental-cache/.gitignore b/examples/overrides/r2-incremental-cache/.gitignore new file mode 100644 index 000000000..3f753f293 --- /dev/null +++ b/examples/overrides/r2-incremental-cache/.gitignore @@ -0,0 +1,47 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/examples/overrides/r2-incremental-cache/app/favicon.ico b/examples/overrides/r2-incremental-cache/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/examples/overrides/r2-incremental-cache/app/globals.css b/examples/overrides/r2-incremental-cache/app/globals.css new file mode 100644 index 000000000..6e6f12f33 --- /dev/null +++ b/examples/overrides/r2-incremental-cache/app/globals.css @@ -0,0 +1,14 @@ +html, +body { + max-width: 100vw; + overflow-x: hidden; + height: 100vh; + display: flex; + flex-direction: column; +} + +footer { + padding: 1rem; + display: flex; + justify-content: end; +} diff --git a/examples/overrides/r2-incremental-cache/app/layout.tsx b/examples/overrides/r2-incremental-cache/app/layout.tsx new file mode 100644 index 000000000..e878f82a0 --- /dev/null +++ b/examples/overrides/r2-incremental-cache/app/layout.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +import { getCloudflareContext } from "@opennextjs/cloudflare"; + +export const metadata: Metadata = { + title: "SSG App", + description: "An app in which all the routes are SSG'd", +}; + +export default async function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const cloudflareContext = await getCloudflareContext({ + async: true, + }); + + return ( + + {children} + + ); +} diff --git a/examples/overrides/r2-incremental-cache/app/page.module.css b/examples/overrides/r2-incremental-cache/app/page.module.css new file mode 100644 index 000000000..1217984e8 --- /dev/null +++ b/examples/overrides/r2-incremental-cache/app/page.module.css @@ -0,0 +1,17 @@ +.page { + display: grid; + grid-template-rows: 20px 1fr 20px; + align-items: center; + justify-items: center; + flex: 1; + border: 3px solid gray; + margin: 1rem; + margin-block-end: 0; +} + +.main { + display: flex; + flex-direction: column; + gap: 32px; + grid-row-start: 2; +} diff --git a/examples/overrides/r2-incremental-cache/app/page.tsx b/examples/overrides/r2-incremental-cache/app/page.tsx new file mode 100644 index 000000000..de3385c2c --- /dev/null +++ b/examples/overrides/r2-incremental-cache/app/page.tsx @@ -0,0 +1,16 @@ +import styles from "./page.module.css"; + +export const revalidate = 5; + +export default async function Home() { + // We purposefully wait for 2 seconds to allow deduplication to occur + await new Promise((resolve) => setTimeout(resolve, 2000)); + return ( +
+
+

Hello from a Statically generated page

+

{Date.now()}

+
+
+ ); +} diff --git a/examples/overrides/r2-incremental-cache/e2e/base.spec.ts b/examples/overrides/r2-incremental-cache/e2e/base.spec.ts new file mode 100644 index 000000000..2f546e823 --- /dev/null +++ b/examples/overrides/r2-incremental-cache/e2e/base.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from "@playwright/test"; + +test.describe("r2-incremental-cache", () => { + test("the index page should work", async ({ page }) => { + await page.goto("/"); + await expect(page.getByText("Hello from a Statically generated page")).toBeVisible(); + }); + + test("the index page should revalidate", async ({ page, request }) => { + // We need to make sure the page is loaded and is a HIT + // If it is STALE, the next hit may have an updated date and thus fail the test + let cacheHeaders = ""; + do { + const req = await request.get("/"); + cacheHeaders = req.headers()["x-nextjs-cache"]; + await page.waitForTimeout(500); + } while (cacheHeaders !== "HIT"); + + await page.goto("/"); + const firstDate = await page.getByTestId("date-local").textContent(); + + await page.reload(); + let newDate = await page.getByTestId("date-local").textContent(); + expect(newDate).toBe(firstDate); + + await page.waitForTimeout(5000); + + do { + await page.reload(); + newDate = await page.getByTestId("date-local").textContent(); + await page.waitForTimeout(1000); + } while (newDate === firstDate); + + expect(newDate).not.toBe(firstDate); + }); +}); diff --git a/examples/overrides/r2-incremental-cache/e2e/playwright.config.ts b/examples/overrides/r2-incremental-cache/e2e/playwright.config.ts new file mode 100644 index 000000000..77b2b0216 --- /dev/null +++ b/examples/overrides/r2-incremental-cache/e2e/playwright.config.ts @@ -0,0 +1,8 @@ +import { configurePlaywright } from "../../../common/config-e2e"; + +// Here we don't want to run the tests in parallel +export default configurePlaywright("r2-incremental-cache", { + isCI: !!process.env.CI, + parallel: false, + multipleBrowsers: false, +}); diff --git a/examples/overrides/r2-incremental-cache/next.config.ts b/examples/overrides/r2-incremental-cache/next.config.ts new file mode 100644 index 000000000..4b075e442 --- /dev/null +++ b/examples/overrides/r2-incremental-cache/next.config.ts @@ -0,0 +1,11 @@ +import type { NextConfig } from "next"; +import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; + +initOpenNextCloudflareForDev(); + +const nextConfig: NextConfig = { + typescript: { ignoreBuildErrors: true }, + eslint: { ignoreDuringBuilds: true }, +}; + +export default nextConfig; diff --git a/examples/overrides/r2-incremental-cache/open-next.config.ts b/examples/overrides/r2-incremental-cache/open-next.config.ts new file mode 100644 index 000000000..2d73a2dd7 --- /dev/null +++ b/examples/overrides/r2-incremental-cache/open-next.config.ts @@ -0,0 +1,14 @@ +import { defineCloudflareConfig } from "@opennextjs/cloudflare"; +import d1TagCache from "@opennextjs/cloudflare/d1-tag-cache"; +import memoryQueue from "@opennextjs/cloudflare/memory-queue"; +import r2IncrementalCache from "@opennextjs/cloudflare/r2-incremental-cache"; +import { withRegionalCache } from "@opennextjs/cloudflare/regional-cache"; + +export default defineCloudflareConfig({ + incrementalCache: withRegionalCache(r2IncrementalCache, { + mode: "long-lived", + shouldLazilyUpdateOnCacheHit: true, + }), + tagCache: d1TagCache, + queue: memoryQueue, +}); diff --git a/examples/overrides/r2-incremental-cache/package.json b/examples/overrides/r2-incremental-cache/package.json new file mode 100644 index 000000000..8eda7f825 --- /dev/null +++ b/examples/overrides/r2-incremental-cache/package.json @@ -0,0 +1,29 @@ +{ + "name": "r2-incremental-cache", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "d1:clean": "wrangler d1 execute NEXT_CACHE_D1 --command \"DROP TABLE IF EXISTS tags; DROP TABLE IF EXISTS revalidations\"", + "build:worker": "pnpm d1:clean && pnpm opennextjs-cloudflare --populateCache=local", + "preview": "pnpm build:worker && pnpm wrangler dev", + "e2e": "playwright test -c e2e/playwright.config.ts" + }, + "dependencies": { + "react": "catalog:e2e", + "react-dom": "catalog:e2e", + "next": "catalog:e2e" + }, + "devDependencies": { + "@opennextjs/cloudflare": "workspace:*", + "@playwright/test": "catalog:", + "@types/node": "catalog:", + "@types/react": "catalog:e2e", + "@types/react-dom": "catalog:e2e", + "typescript": "catalog:", + "wrangler": "catalog:" + } +} diff --git a/examples/overrides/r2-incremental-cache/tsconfig.json b/examples/overrides/r2-incremental-cache/tsconfig.json new file mode 100644 index 000000000..d8b93235f --- /dev/null +++ b/examples/overrides/r2-incremental-cache/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/overrides/r2-incremental-cache/wrangler.jsonc b/examples/overrides/r2-incremental-cache/wrangler.jsonc new file mode 100644 index 000000000..c0e4413e9 --- /dev/null +++ b/examples/overrides/r2-incremental-cache/wrangler.jsonc @@ -0,0 +1,31 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "main": ".open-next/worker.js", + "name": "r2-incremental-cache", + "compatibility_date": "2025-02-04", + "compatibility_flags": ["nodejs_compat"], + "assets": { + "directory": ".open-next/assets", + "binding": "ASSETS" + }, + "d1_databases": [ + { + "binding": "NEXT_CACHE_D1", + "database_id": "NEXT_CACHE_D1", + "database_name": "NEXT_CACHE_D1" + } + ], + "services": [ + { + "binding": "NEXT_CACHE_REVALIDATION_WORKER", + "service": "r2-incremental-cache" + } + ], + "r2_buckets": [ + { + "binding": "NEXT_CACHE_R2_BUCKET", + "bucket_name": "NEXT_CACHE_R2_BUCKET", + "preview_bucket_name": "NEXT_CACHE_R2_BUCKET" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b14f9279..72d00f868 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -751,6 +751,40 @@ importers: specifier: 'catalog:' version: 3.114.1(@cloudflare/workers-types@4.20250224.0) + examples/overrides/r2-incremental-cache: + dependencies: + next: + specifier: catalog:e2e + version: 15.2.2(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: + specifier: catalog:e2e + version: 19.0.0 + react-dom: + specifier: catalog:e2e + version: 19.0.0(react@19.0.0) + devDependencies: + '@opennextjs/cloudflare': + specifier: workspace:* + version: link:../../../packages/cloudflare + '@playwright/test': + specifier: 'catalog:' + version: 1.51.1 + '@types/node': + specifier: 'catalog:' + version: 22.2.0 + '@types/react': + specifier: catalog:e2e + version: 19.0.0 + '@types/react-dom': + specifier: catalog:e2e + version: 19.0.0 + typescript: + specifier: 'catalog:' + version: 5.7.3 + wrangler: + specifier: 'catalog:' + version: 3.114.1(@cloudflare/workers-types@4.20250224.0) + examples/playground14: dependencies: next: @@ -16214,7 +16248,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.0 is-bun-module: 1.2.1 @@ -16233,7 +16267,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.0 is-bun-module: 1.2.1 @@ -16252,7 +16286,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 9.11.1(jiti@1.21.6) - eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.11.1(jiti@1.21.6)))(eslint@9.11.1(jiti@1.21.6)) + eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.11.1(jiti@1.21.6)) fast-glob: 3.3.2 get-tsconfig: 4.8.0 is-bun-module: 1.2.1 @@ -16271,7 +16305,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 9.19.0(jiti@1.21.6) - eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@9.19.0(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@9.19.0(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.19.0(jiti@1.21.6)))(eslint@9.19.0(jiti@1.21.6)) + eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@9.19.0(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.19.0(jiti@1.21.6)) fast-glob: 3.3.2 get-tsconfig: 4.8.0 is-bun-module: 1.2.1 @@ -16284,18 +16318,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.7.0(eslint@8.57.1)(typescript@5.7.3) - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -16306,7 +16329,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.11.0(@typescript-eslint/parser@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.11.1(jiti@1.21.6)))(eslint@9.11.1(jiti@1.21.6)): + eslint-module-utils@2.11.0(@typescript-eslint/parser@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.11.1(jiti@1.21.6)): dependencies: debug: 3.2.7 optionalDependencies: @@ -16317,7 +16340,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.11.0(@typescript-eslint/parser@8.7.0(eslint@9.19.0(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@9.19.0(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.19.0(jiti@1.21.6)))(eslint@9.19.0(jiti@1.21.6)): + eslint-module-utils@2.11.0(@typescript-eslint/parser@8.7.0(eslint@9.19.0(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.19.0(jiti@1.21.6)): dependencies: debug: 3.2.7 optionalDependencies: @@ -16328,7 +16351,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -16339,7 +16362,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.11.1(jiti@1.21.6)))(eslint@9.11.1(jiti@1.21.6)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.11.1(jiti@1.21.6)): dependencies: debug: 3.2.7 optionalDependencies: @@ -16350,7 +16373,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.7.0(eslint@9.19.0(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@9.19.0(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.19.0(jiti@1.21.6)))(eslint@9.19.0(jiti@1.21.6)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.7.0(eslint@9.19.0(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.19.0(jiti@1.21.6)): dependencies: debug: 3.2.7 optionalDependencies: @@ -16372,7 +16395,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -16400,7 +16423,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -16429,7 +16452,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.11.1(jiti@1.21.6) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.11.1(jiti@1.21.6)))(eslint@9.11.1(jiti@1.21.6)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.11.1(jiti@1.21.6)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -16458,7 +16481,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.19.0(jiti@1.21.6) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.7.0(eslint@9.19.0(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@9.19.0(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.19.0(jiti@1.21.6)))(eslint@9.19.0(jiti@1.21.6)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.7.0(eslint@9.19.0(jiti@1.21.6))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.19.0(jiti@1.21.6)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 From 8566368b8e1cd9f9aba7ab265a7be37e35b30b4c Mon Sep 17 00:00:00 2001 From: James Date: Fri, 21 Mar 2025 10:45:40 +0000 Subject: [PATCH 18/19] comments --- packages/cloudflare/src/api/r2-incremental-cache.ts | 12 ++++++++---- packages/cloudflare/src/api/regional-cache.ts | 4 ++-- .../cloudflare/src/cli/build/utils/populate-cache.ts | 5 ++++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/cloudflare/src/api/r2-incremental-cache.ts b/packages/cloudflare/src/api/r2-incremental-cache.ts index 4ab451982..5d78bffc5 100644 --- a/packages/cloudflare/src/api/r2-incremental-cache.ts +++ b/packages/cloudflare/src/api/r2-incremental-cache.ts @@ -8,7 +8,7 @@ import { getCloudflareContext } from "./cloudflare-context.js"; * An instance of the Incremental Cache that uses an R2 bucket (`NEXT_CACHE_R2_BUCKET`) as it's * underlying data store. * - * The directory that the cache entries are stored in can be confused with the `NEXT_CACHE_R2_PREFIX` + * The directory that the cache entries are stored in can be configured with the `NEXT_CACHE_R2_PREFIX` * environment variable, and defaults to `incremental-cache`. * * The cache uses an instance of the Cache API (`incremental-cache`) to store a local version of the @@ -17,6 +17,12 @@ import { getCloudflareContext } from "./cloudflare-context.js"; class R2IncrementalCache implements IncrementalCache { readonly name = "r2-incremental-cache"; + protected directory: string; + + constructor() { + this.directory = getCloudflareContext().env.NEXT_CACHE_R2_PREFIX ?? "incremental-cache"; + } + async get( key: string, isFetch?: IsFetch @@ -71,9 +77,7 @@ class R2IncrementalCache implements IncrementalCache { } protected getR2Key(key: string, isFetch?: boolean): string { - const directory = getCloudflareContext().env.NEXT_CACHE_R2_PREFIX ?? "incremental-cache"; - - return `${directory}/${process.env.NEXT_BUILD_ID ?? "no-build-id"}/${key}.${isFetch ? "fetch" : "cache"}`; + return `${this.directory}/${process.env.NEXT_BUILD_ID ?? "no-build-id"}/${key}.${isFetch ? "fetch" : "cache"}`; } } diff --git a/packages/cloudflare/src/api/regional-cache.ts b/packages/cloudflare/src/api/regional-cache.ts index fbe8c7b4e..f526aa4ed 100644 --- a/packages/cloudflare/src/api/regional-cache.ts +++ b/packages/cloudflare/src/api/regional-cache.ts @@ -12,7 +12,7 @@ type Options = { * The mode to use for the regional cache. * * - `short-lived`: Re-use a cache entry for up to a minute after it has been retrieved. - * - `long-lived`: Re-use a fetch cache entry until it is revalidated, or an ISR/SSG entry for up to 30 minutes. + * - `long-lived`: Re-use a fetch cache entry until it is revalidated (per-region), or an ISR/SSG entry for up to 30 minutes. */ mode: "short-lived" | "long-lived"; /** @@ -156,7 +156,7 @@ class RegionalCache implements IncrementalCache { * @param cache - Incremental cache instance. * @param opts.mode - The mode to use for the regional cache. * - `short-lived`: Re-use a cache entry for up to a minute after it has been retrieved. - * - `long-lived`: Re-use a fetch cache entry until it is revalidated, or an ISR/SSG entry for up to 30 minutes. + * - `long-lived`: Re-use a fetch cache entry until it is revalidated (per-region), or an ISR/SSG entry for up to 30 minutes. * @param opts.shouldLazilyUpdateOnCacheHit - Whether the regional cache entry should be updated in * the background or not when it experiences a cache hit. * diff --git a/packages/cloudflare/src/cli/build/utils/populate-cache.ts b/packages/cloudflare/src/cli/build/utils/populate-cache.ts index d886d975e..cc6ea02e5 100644 --- a/packages/cloudflare/src/cli/build/utils/populate-cache.ts +++ b/packages/cloudflare/src/cli/build/utils/populate-cache.ts @@ -52,7 +52,10 @@ function runWrangler( } function getCacheAssetPaths(opts: BuildOptions) { - return globSync(path.join(opts.outputDir, "cache/**/*"), { withFileTypes: true }) + return globSync(path.join(opts.outputDir, "cache/**/*"), { + withFileTypes: true, + windowsPathsNoEscape: true, + }) .filter((f) => f.isFile()) .map((f) => { const relativePath = path.relative(path.join(opts.outputDir, "cache"), f.fullpathPosix()); From 33c238b37c8cee0f82d4982dfc4a16f2b6c73cd4 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 21 Mar 2025 10:55:14 +0000 Subject: [PATCH 19/19] undo directory change as it breaks build --- packages/cloudflare/src/api/r2-incremental-cache.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/cloudflare/src/api/r2-incremental-cache.ts b/packages/cloudflare/src/api/r2-incremental-cache.ts index 5d78bffc5..de216d70c 100644 --- a/packages/cloudflare/src/api/r2-incremental-cache.ts +++ b/packages/cloudflare/src/api/r2-incremental-cache.ts @@ -17,12 +17,6 @@ import { getCloudflareContext } from "./cloudflare-context.js"; class R2IncrementalCache implements IncrementalCache { readonly name = "r2-incremental-cache"; - protected directory: string; - - constructor() { - this.directory = getCloudflareContext().env.NEXT_CACHE_R2_PREFIX ?? "incremental-cache"; - } - async get( key: string, isFetch?: IsFetch @@ -77,7 +71,9 @@ class R2IncrementalCache implements IncrementalCache { } protected getR2Key(key: string, isFetch?: boolean): string { - return `${this.directory}/${process.env.NEXT_BUILD_ID ?? "no-build-id"}/${key}.${isFetch ? "fetch" : "cache"}`; + const directory = getCloudflareContext().env.NEXT_CACHE_R2_PREFIX ?? "incremental-cache"; + + return `${directory}/${process.env.NEXT_BUILD_ID ?? "no-build-id"}/${key}.${isFetch ? "fetch" : "cache"}`; } }