diff --git a/.changeset/volatile-cache-local-dev.md b/.changeset/volatile-cache-local-dev.md new file mode 100644 index 00000000000..f5324a560a4 --- /dev/null +++ b/.changeset/volatile-cache-local-dev.md @@ -0,0 +1,8 @@ +--- +"miniflare": minor +"wrangler": minor +--- + +Support experimental Volatile Cache bindings in local development + +`wrangler dev` now maps `volatile_cache` entries in `unsafe.bindings` to workerd's in-memory `MemoryCache` implementation, including the configured cache ID and size limits. Production deployment behavior is unchanged. diff --git a/packages/miniflare/src/plugins/core/index.ts b/packages/miniflare/src/plugins/core/index.ts index 6928db95c81..0bc5dd6d8b9 100644 --- a/packages/miniflare/src/plugins/core/index.ts +++ b/packages/miniflare/src/plugins/core/index.ts @@ -200,6 +200,20 @@ const CoreOptionsSchemaInput = z.intersection( unsafeOverrideFetchWorker: z.string().optional(), unsafeEvalBinding: z.string().optional(), + unsafeMemoryCaches: z + .record( + z.object({ + id: z.string().optional(), + maxKeys: z.number().int().nonnegative().max(0xffffffff), + maxValueSize: z.number().int().nonnegative().max(0xffffffff), + maxTotalValueSize: z + .number() + .int() + .nonnegative() + .max(Number.MAX_SAFE_INTEGER), + }) + ) + .optional(), unsafeUseModuleFallbackService: z.boolean().optional(), /** Used to set the vitest pool worker SELF binding to point to the Router Worker if there are assets. @@ -726,6 +740,26 @@ export const CORE_PLUGIN: Plugin< unsafeEval: kVoid, }); } + if (options.unsafeMemoryCaches !== undefined) { + bindings.push( + ...Object.entries(options.unsafeMemoryCaches).map( + ([ + name, + { id, maxKeys, maxValueSize, maxTotalValueSize }, + ]): Worker_Binding => ({ + name, + memoryCache: { + id, + limits: { + maxKeys, + maxValueSize, + maxTotalValueSize: BigInt(maxTotalValueSize), + }, + }, + }) + ) + ); + } return Promise.all(bindings); }, diff --git a/packages/miniflare/src/runtime/config/index.ts b/packages/miniflare/src/runtime/config/index.ts index 0afd9773bbd..fb84d2522bb 100644 --- a/packages/miniflare/src/runtime/config/index.ts +++ b/packages/miniflare/src/runtime/config/index.ts @@ -52,7 +52,14 @@ function encodeCapnpStruct(obj: any, struct: Struct) { export function serializeConfig(config: Config): Buffer { const debugPath = process.env.MINIFLARE_WORKERD_CONFIG_DEBUG; if (debugPath) { - writeFileSync(debugPath, JSON.stringify(config, null, 2)); + writeFileSync( + debugPath, + JSON.stringify( + config, + (_key, value) => (typeof value === "bigint" ? value.toString() : value), + 2 + ) + ); } const message = new Message(); const struct = message.initRoot(CapnpConfig); diff --git a/packages/miniflare/src/runtime/config/workerd.ts b/packages/miniflare/src/runtime/config/workerd.ts index 4ee0396b224..46d5c899280 100644 --- a/packages/miniflare/src/runtime/config/workerd.ts +++ b/packages/miniflare/src/runtime/config/workerd.ts @@ -116,6 +116,7 @@ export type Worker_Binding = { | { analyticsEngine?: ServiceDesignator } | { hyperdrive?: Worker_Binding_Hyperdrive } | { unsafeEval?: Void } + | { memoryCache?: Worker_Binding_MemoryCache } | { workerLoader?: Worker_Binding_WorkerLoader } | { workerdDebugPort?: Void } ); @@ -188,7 +189,7 @@ export interface Worker_Binding_MemoryCache { export interface Worker_Binding_MemoryCacheLimits { maxKeys?: number; maxValueSize?: number; - maxTotalValueSize?: number; + maxTotalValueSize?: bigint; } export type Worker_DurableObjectNamespace = { diff --git a/packages/miniflare/test/index.spec.ts b/packages/miniflare/test/index.spec.ts index db3378243b2..22bde120d38 100644 --- a/packages/miniflare/test/index.spec.ts +++ b/packages/miniflare/test/index.spec.ts @@ -3205,6 +3205,41 @@ test("Miniflare: supports unsafe eval bindings", async ({ expect }) => { expect(await response.text()).toBe("the computed value is 3"); }); +test("Miniflare: supports memory cache bindings", async ({ expect }) => { + const mf = new Miniflare({ + modules: true, + script: `export default { + async fetch(req, env) { + const first = await env.CACHE.read("key", async () => ({ + value: "first", + expiration: Date.now() + 60_000, + })); + const second = await env.CACHE.read("key", async () => ({ + value: "second", + expiration: Date.now() + 60_000, + })); + return Response.json({ first, second }); + } + }`, + unsafeMemoryCaches: { + CACHE: { + id: "test-cache", + maxKeys: 10, + maxValueSize: 1024, + maxTotalValueSize: 1024, + }, + }, + }); + useDispose(mf); + + const response = await mf.dispatchFetch("http://localhost"); + expect(response.ok).toBe(true); + expect(await response.json()).toEqual({ + first: "first", + second: "first", + }); +}); + test("Miniflare: supports wrapped bindings", async ({ expect }) => { const store = new Map(); const mf = new Miniflare({ diff --git a/packages/workers-utils/src/types.ts b/packages/workers-utils/src/types.ts index b8ecf7eee6e..41e2f63f911 100644 --- a/packages/workers-utils/src/types.ts +++ b/packages/workers-utils/src/types.ts @@ -484,6 +484,13 @@ export type Binding = | ({ type: "vpc_service" } & BindingOmit) | ({ type: "vpc_network" } & BindingOmit) | ({ type: "media" } & BindingOmit) + | { + type: "unsafe_volatile_cache"; + cache_id: string; + max_keys: number; + max_value_size: number; + max_total_value_size: number; + } | ({ type: `unsafe_${string}` } & Omit) | { type: "assets" } | { type: "inherit" }; diff --git a/packages/wrangler/src/__tests__/unstable-get-miniflare-worker-options.test.ts b/packages/wrangler/src/__tests__/unstable-get-miniflare-worker-options.test.ts index c66fc2f11a6..965159b8142 100644 --- a/packages/wrangler/src/__tests__/unstable-get-miniflare-worker-options.test.ts +++ b/packages/wrangler/src/__tests__/unstable-get-miniflare-worker-options.test.ts @@ -231,4 +231,41 @@ describe("unstable_getMiniflareWorkerOptions", () => { ).toBeUndefined(); }); }); + + it("configures unsafe volatile cache bindings for local development", ({ + expect, + }) => { + writeWranglerConfig( + { + name: "test-worker", + main: "./index.js", + compatibility_date: "2024-10-04", + unsafe: { + bindings: [ + { + name: "AIG_VOLATILE_CACHE", + type: "volatile_cache", + cache_id: "ai-gateway-worker-staging", + max_keys: 10_000, + max_value_size: 16_384, + max_total_value_size: 33_554_432, + }, + ], + }, + }, + "./wrangler.json" + ); + + const { workerOptions } = + unstable_getMiniflareWorkerOptions("./wrangler.json"); + + expect(workerOptions.unsafeMemoryCaches).toEqual({ + AIG_VOLATILE_CACHE: { + id: "ai-gateway-worker-staging", + maxKeys: 10_000, + maxValueSize: 16_384, + maxTotalValueSize: 33_554_432, + }, + }); + }); }); diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index 58f205d09db..35fcf745cae 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -484,6 +484,7 @@ type WorkerOptionsBindings = Pick< | "flagship" | "artifacts" | "workerLoaders" + | "unsafeMemoryCaches" | "unsafeBindings" | "additionalUnboundDurableObjects" | "media" @@ -561,6 +562,10 @@ export function buildMiniflareBindingOptions( const flagshipBindings = extractBindingsOfType("flagship", bindings); const artifactsBindings = extractBindingsOfType("artifacts", bindings); const workerLoaders = extractBindingsOfType("worker_loader", bindings); + const volatileCaches = extractBindingsOfType( + "unsafe_volatile_cache", + bindings + ); const sendEmailBindings = extractBindingsOfType("send_email", bindings); // Extract both regular and unsafe ratelimit bindings // Unsafe bindings have type "unsafe_ratelimit" (prefixed with "unsafe_") @@ -811,6 +816,25 @@ export function buildMiniflareBindingOptions( dataBlobBindings, wasmBindings, unsafeBindings, + unsafeMemoryCaches: Object.fromEntries( + volatileCaches.map( + ({ + binding, + cache_id, + max_keys, + max_value_size, + max_total_value_size, + }) => [ + binding, + { + id: cache_id, + maxKeys: max_keys, + maxValueSize: max_value_size, + maxTotalValueSize: max_total_value_size, + }, + ] + ) + ), ai: aiBindings.length > 0