From c0e9e08356b45243b752af937f463105a58f9a0e Mon Sep 17 00:00:00 2001 From: WillTaylorDev Date: Tue, 24 Feb 2026 18:20:19 -0500 Subject: [PATCH] [wrangler] Add worker cache configuration support (#12625) --- .changeset/seven-squids-dream.md | 20 ++ packages/workers-utils/src/config/config.ts | 1 + .../workers-utils/src/config/environment.ts | 13 ++ packages/workers-utils/src/config/index.ts | 1 + .../workers-utils/src/config/validation.ts | 41 ++++ packages/workers-utils/src/worker.ts | 3 +- .../normalize-and-validate-config.test.ts | 184 ++++++++++++++++++ .../RemoteRuntimeController.test.ts | 1 + .../create-worker-upload-form/helpers.ts | 2 + .../metadata.test.ts | 6 + .../pages/create-worker-bundle-contents.ts | 1 + packages/wrangler/src/deploy/deploy.ts | 1 + .../create-worker-upload-form.ts | 2 + packages/wrangler/src/dev/remote.ts | 1 + packages/wrangler/src/secret/index.ts | 1 + .../wrangler/src/versions/secrets/index.ts | 2 + packages/wrangler/src/versions/upload.ts | 3 +- 17 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 .changeset/seven-squids-dream.md diff --git a/.changeset/seven-squids-dream.md b/.changeset/seven-squids-dream.md new file mode 100644 index 000000000000..ae7dd26470f9 --- /dev/null +++ b/.changeset/seven-squids-dream.md @@ -0,0 +1,20 @@ +--- +"wrangler": minor +"@cloudflare/workers-utils": minor +--- + +Add `cache` configuration option for enabling worker cache (experimental) + +You can now enable cache before worker execution using the new `cache` configuration: + +```jsonc +{ + "cache": { + "enabled": true, + }, +} +``` + +This setting is environment-inheritable and opt-in. When enabled, cache behavior is applied before your worker runs. + +Note: This feature is experimental. The runtime API is not yet generally available. diff --git a/packages/workers-utils/src/config/config.ts b/packages/workers-utils/src/config/config.ts index 5289f8715b68..4ecd04a38cf3 100644 --- a/packages/workers-utils/src/config/config.ts +++ b/packages/workers-utils/src/config/config.ts @@ -388,6 +388,7 @@ export const defaultWranglerConfig: Config = { upload_source_maps: undefined, assets: undefined, observability: { enabled: true }, + cache: undefined, /** The default here is undefined so that we can delegate to the CLOUDFLARE_COMPLIANCE_REGION environment variable. */ compliance_region: undefined, python_modules: { exclude: ["**/*.pyc"] }, diff --git a/packages/workers-utils/src/config/environment.ts b/packages/workers-utils/src/config/environment.ts index d55932c32878..6ae7ce719626 100644 --- a/packages/workers-utils/src/config/environment.ts +++ b/packages/workers-utils/src/config/environment.ts @@ -620,6 +620,14 @@ interface EnvironmentInheritable { */ observability: Observability | undefined; + /** + * Specify the cache behavior of the Worker. + * + * @inheritable + * @hidden + */ + cache: CacheOptions | undefined; + /** * Specify the compliance region mode of the Worker. * @@ -1428,6 +1436,11 @@ export interface Observability { }; } +export interface CacheOptions { + /** If cache is enabled for this Worker */ + enabled: boolean; +} + export type DockerConfiguration = { /** Socket used by miniflare to communicate with Docker */ socketPath: string; diff --git a/packages/workers-utils/src/config/index.ts b/packages/workers-utils/src/config/index.ts index e7607ab75062..5939ab0815c9 100644 --- a/packages/workers-utils/src/config/index.ts +++ b/packages/workers-utils/src/config/index.ts @@ -13,6 +13,7 @@ export type { RawDevConfig, } from "./config"; export type { + CacheOptions, ConfigModuleRuleType, Environment, RawEnvironment, diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index bb27f4370138..747187f96c0b 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -40,6 +40,7 @@ import type { Binding } from "../types"; import type { Config, DevConfig, RawConfig, RawDevConfig } from "./config"; import type { Assets, + CacheOptions, DispatchNamespaceOutbound, Environment, Observability, @@ -1936,6 +1937,14 @@ function normalizeAndValidateEnvironment( validateObservability, undefined ), + cache: inheritable( + diagnostics, + topLevelEnv, + rawEnv, + "cache", + validateCache, + undefined + ), compliance_region: inheritable( diagnostics, topLevelEnv, @@ -4915,6 +4924,38 @@ const validateObservability: ValidatorFn = (diagnostics, field, value) => { return isValid; }; +const validateCache: ValidatorFn = (diagnostics, field, value) => { + if (value === undefined) { + return true; + } + + if (typeof value !== "object" || value === null) { + diagnostics.errors.push( + `"${field}" should be an object but got ${JSON.stringify(value)}.` + ); + return false; + } + + const val = value as CacheOptions; + let isValid = true; + + isValid = + validateRequiredProperty( + diagnostics, + field, + "enabled", + val.enabled, + "boolean" + ) && isValid; + + isValid = + validateAdditionalProperties(diagnostics, field, Object.keys(val), [ + "enabled", + ]) && isValid; + + return isValid; +}; + function warnIfDurableObjectsHaveNoMigrations( diagnostics: Diagnostics, durableObjects: Config["durable_objects"], diff --git a/packages/workers-utils/src/worker.ts b/packages/workers-utils/src/worker.ts index 7b756e8cef4e..f0f6cf5caae9 100644 --- a/packages/workers-utils/src/worker.ts +++ b/packages/workers-utils/src/worker.ts @@ -1,4 +1,4 @@ -import type { Observability, Route } from "./config/environment"; +import type { CacheOptions, Observability, Route } from "./config/environment"; import type { INHERIT_SYMBOL } from "./constants"; import type { Json, WorkerMetadata } from "./types"; import type { AssetConfig, RouterConfig } from "@cloudflare/workers-shared"; @@ -428,6 +428,7 @@ export interface CfWorkerInit { } | undefined; observability: Observability | undefined; + cache: CacheOptions | undefined; } export interface CfWorkerContext { diff --git a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts index 68d6b5ebbf04..3d332710a5c4 100644 --- a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts +++ b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts @@ -125,6 +125,7 @@ describe("normalizeAndValidateConfig()", () => { keep_names: undefined, assets: undefined, observability: undefined, + cache: undefined, compliance_region: undefined, images: undefined, media: undefined, @@ -7484,6 +7485,189 @@ describe("normalizeAndValidateConfig()", () => { }); }); + describe("[cache]", () => { + it("should error when cache is not an object", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + cache: "enabled", + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "cache" should be an object but got "enabled"." + `); + }); + + it("should error when cache is null", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + cache: null, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "cache" should be an object but got null." + `); + }); + + it("should error when cache.enabled is missing", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + cache: {}, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "cache.enabled" is a required field." + `); + }); + + it("should error when cache.enabled is not a boolean", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + cache: { + enabled: "true", + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - Expected "cache.enabled" to be of type boolean but got "true"." + `); + }); + + it("should not error on valid cache config with enabled true", ({ + expect, + }) => { + const { config, diagnostics } = normalizeAndValidateConfig( + { + cache: { + enabled: true, + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(false); + expect(config.cache).toEqual({ enabled: true }); + }); + + it("should not error on valid cache config with enabled false", ({ + expect, + }) => { + const { config, diagnostics } = normalizeAndValidateConfig( + { + cache: { + enabled: false, + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(false); + expect(config.cache).toEqual({ enabled: false }); + }); + + it("should warn on unexpected fields in cache config", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + cache: { + enabled: true, + invalid_key: "hello", + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - Unexpected fields found in cache field: "invalid_key"" + `); + }); + + it("should inherit cache from top-level config to environment", ({ + expect, + }) => { + const { config, diagnostics } = normalizeAndValidateConfig( + { + cache: { + enabled: true, + }, + env: { + production: {}, + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: "production" } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(false); + expect(config.cache).toEqual({ enabled: true }); + }); + + it("should allow environment to override top-level cache", ({ + expect, + }) => { + const { config, diagnostics } = normalizeAndValidateConfig( + { + cache: { + enabled: true, + }, + env: { + staging: { + cache: { + enabled: false, + }, + }, + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: "staging" } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(false); + expect(config.cache).toEqual({ enabled: false }); + }); + }); + describe("route & routes fields", () => { it("should error if both route and routes are specified in the same environment", ({ expect, diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/RemoteRuntimeController.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/RemoteRuntimeController.test.ts index b5a18b83818c..16033619e4b1 100644 --- a/packages/wrangler/src/__tests__/api/startDevWorker/RemoteRuntimeController.test.ts +++ b/packages/wrangler/src/__tests__/api/startDevWorker/RemoteRuntimeController.test.ts @@ -156,6 +156,7 @@ describe("RemoteRuntimeController", () => { limits: undefined, observability: undefined, containers: undefined, + cache: undefined, }); vi.mocked(createWorkerPreview).mockResolvedValue({ diff --git a/packages/wrangler/src/__tests__/create-worker-upload-form/helpers.ts b/packages/wrangler/src/__tests__/create-worker-upload-form/helpers.ts index e3f4135ff4f3..4bd36fab5426 100644 --- a/packages/wrangler/src/__tests__/create-worker-upload-form/helpers.ts +++ b/packages/wrangler/src/__tests__/create-worker-upload-form/helpers.ts @@ -44,6 +44,7 @@ export function createEsmWorker( tail_consumers: undefined, limits: undefined, observability: undefined, + cache: undefined, ...overrides, }; } @@ -77,6 +78,7 @@ export function createCjsWorker( tail_consumers: undefined, limits: undefined, observability: undefined, + cache: undefined, ...overrides, }; } diff --git a/packages/wrangler/src/__tests__/create-worker-upload-form/metadata.test.ts b/packages/wrangler/src/__tests__/create-worker-upload-form/metadata.test.ts index 15ddd84e90d0..2f50c606be60 100644 --- a/packages/wrangler/src/__tests__/create-worker-upload-form/metadata.test.ts +++ b/packages/wrangler/src/__tests__/create-worker-upload-form/metadata.test.ts @@ -182,6 +182,12 @@ describe("createWorkerUploadForm — optional metadata fields", () => { key: "observability", expected: { enabled: true }, }, + { + label: "cache", + overrides: { cache: { enabled: true } }, + key: "cache_options", + expected: { enabled: true }, + }, { label: "annotations", overrides: { diff --git a/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts b/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts index cc5bcc292152..0972a2a69465 100644 --- a/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts +++ b/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts @@ -74,6 +74,7 @@ function createWorkerBundleFormData( assets: undefined, containers: undefined, // containers are not supported in Pages observability: undefined, + cache: undefined, // cache is not supported in Pages }, getBindings(config, { pages: true }) ); diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index 9d4822e52d95..bef59c9d7fc5 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -879,6 +879,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m } : undefined, observability: config.observability, + cache: config.cache, }; sourceMapSize = worker.sourceMaps?.reduce( diff --git a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts index ce3e054f9f1d..d13cbe949df4 100644 --- a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts +++ b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts @@ -81,6 +81,7 @@ export function createWorkerUploadForm( keep_assets, assets, observability, + cache, } = worker; const assetConfig: AssetConfigMetadata = { @@ -726,6 +727,7 @@ export function createWorkerUploadForm( }, }), ...(observability && { observability }), + ...(cache && { cache_options: cache }), }; if (options?.unsafe?.metadata !== undefined) { diff --git a/packages/wrangler/src/dev/remote.ts b/packages/wrangler/src/dev/remote.ts index 8d37600d67d9..602d0adc36b5 100644 --- a/packages/wrangler/src/dev/remote.ts +++ b/packages/wrangler/src/dev/remote.ts @@ -204,6 +204,7 @@ export async function createRemoteWorkerInit(props: { streaming_tail_consumers: undefined, limits: undefined, // no limits in preview - not supported yet but can be added observability: undefined, // no observability in dev, + cache: undefined, // no cache in dev }; return init; diff --git a/packages/wrangler/src/secret/index.ts b/packages/wrangler/src/secret/index.ts index 7bd454bb55d1..ac62395cb78d 100644 --- a/packages/wrangler/src/secret/index.ts +++ b/packages/wrangler/src/secret/index.ts @@ -91,6 +91,7 @@ async function createDraftWorker({ assets: undefined, containers: undefined, observability: undefined, + cache: undefined, }, {} ), diff --git a/packages/wrangler/src/versions/secrets/index.ts b/packages/wrangler/src/versions/secrets/index.ts index eedf993f1a53..331ee5592338 100644 --- a/packages/wrangler/src/versions/secrets/index.ts +++ b/packages/wrangler/src/versions/secrets/index.ts @@ -70,6 +70,7 @@ export interface VersionDetails { limits: CfUserLimits; }; }; + cache_options?: { enabled: boolean }; } interface ScriptSettings { @@ -191,6 +192,7 @@ export async function copyWorkerVersionWithNewSecrets({ keep_assets: true, assets: undefined, observability: scriptSettings.observability, + cache: versionInfo.cache_options, }; const body = createWorkerUploadForm(worker, bindings, { diff --git a/packages/wrangler/src/versions/upload.ts b/packages/wrangler/src/versions/upload.ts index 52bd8d036d7d..78e617e285e6 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -728,8 +728,9 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m run_worker_first: props.assetsOptions.run_worker_first, } : undefined, - logpush: undefined, // both logpush and observability are not supported in versions upload + logpush: undefined, // logpush and observability are non-versioned settings observability: undefined, + cache: config.cache, // cache is a versioned setting }; if (config.containers && config.containers.length > 0) {