diff --git a/.changeset/evict-durable-objects.md b/.changeset/evict-durable-objects.md new file mode 100644 index 0000000000..d7cdcd801d --- /dev/null +++ b/.changeset/evict-durable-objects.md @@ -0,0 +1,22 @@ +--- +"@cloudflare/vitest-pool-workers": patch +--- + +Add `evictDurableObject` and `evictAllDurableObjects` test helpers to `cloudflare:test` + +These helpers let you exercise how a Durable Object behaves across evictions in your tests. Eviction is graceful: durable storage is preserved, in-memory state is reset by tearing down the instance, hibernatable WebSockets are hibernated rather than closed, and eviction waits for in-flight requests to drain. + +```ts +import { evictDurableObject, evictAllDurableObjects } from "cloudflare:test"; +import { env } from "cloudflare:workers"; + +const id = env.COUNTER.idFromName("my-counter"); +const stub = env.COUNTER.get(id); + +// Evict the Durable Object instance pointed to by a specific stub +await evictDurableObject(stub); +await evictDurableObject(stub, { webSockets: "close" }); + +// Evict all currently-running Durable Objects in evictable namespaces +await evictAllDurableObjects(); +``` diff --git a/fixtures/vitest-pool-workers-examples/README.md b/fixtures/vitest-pool-workers-examples/README.md index 53fccef2f4..d883b88bad 100644 --- a/fixtures/vitest-pool-workers-examples/README.md +++ b/fixtures/vitest-pool-workers-examples/README.md @@ -2,25 +2,25 @@ This directory contains example projects tested with `@cloudflare/vitest-pool-workers`. It aims to provide the building blocks for you to write tests for your own Workers. -| Directory | Overview | -| --------------------------------------------------------------------------------- | ------------------------------------------------------------------ | -| [✅ basics-unit-integration-self](basics-unit-integration-self) | Basic unit tests and integration tests using `exports.default` | -| [⚠️ basics-integration-auxiliary](basics-integration-auxiliary) | Basic integration tests using an auxiliary worker[^1] | -| [⚡️ pages-functions-unit-integration-self](pages-functions-unit-integration-self) | Functions unit tests and integration tests using `exports.default` | -| [📦 kv-r2-caches](kv-r2-caches) | Tests using KV, R2 and the Cache API | -| [📚 d1](d1) | Tests using D1 with migrations | -| [📌 durable-objects](durable-objects) | Tests using Durable Objects with direct access | -| [🔁 workflows](workflows) | Tests using Workflows | -| [🚥 queues](queues) | Tests using Queue producers and consumers | -| [🚰 pipelines](pipelines) | Tests using Pipelines | -| [🚀 hyperdrive](hyperdrive) | Tests using Hyperdrive with a Vitest managed TCP server | -| [🤹 request-mocking](request-mocking) | Tests using declarative (MSW) / imperative outbound request mocks | -| [🔌 multiple-workers](multiple-workers) | Tests using multiple auxiliary workers and request mocks | -| [⚙️ web-assembly](web-assembly) | Tests importing WebAssembly modules | -| [🤯 rpc](rpc) | Tests using named entrypoints, Durable Objects and RPC | -| [🧠 ai-vectorize](ai-vectorize) | Tests using Workers AI and Vectorize | -| [🔄 context-exports](context-exports) | Tests using context exports | -| [📥 dynamic-import](dynamic-import) | Tests using dynamic imports | -| [🖼️ images](images) | Tests using the Images binding | +| Directory | Overview | +| --------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| [✅ basics-unit-integration-self](basics-unit-integration-self) | Basic unit tests and integration tests using `exports.default` | +| [⚠️ basics-integration-auxiliary](basics-integration-auxiliary) | Basic integration tests using an auxiliary worker[^1] | +| [⚡️ pages-functions-unit-integration-self](pages-functions-unit-integration-self) | Functions unit tests and integration tests using `exports.default` | +| [📦 kv-r2-caches](kv-r2-caches) | Tests using KV, R2 and the Cache API | +| [📚 d1](d1) | Tests using D1 with migrations | +| [📌 durable-objects](durable-objects) | Tests using Durable Objects with direct access, alarms and eviction | +| [🔁 workflows](workflows) | Tests using Workflows | +| [🚥 queues](queues) | Tests using Queue producers and consumers | +| [🚰 pipelines](pipelines) | Tests using Pipelines | +| [🚀 hyperdrive](hyperdrive) | Tests using Hyperdrive with a Vitest managed TCP server | +| [🤹 request-mocking](request-mocking) | Tests using declarative (MSW) / imperative outbound request mocks | +| [🔌 multiple-workers](multiple-workers) | Tests using multiple auxiliary workers and request mocks | +| [⚙️ web-assembly](web-assembly) | Tests importing WebAssembly modules | +| [🤯 rpc](rpc) | Tests using named entrypoints, Durable Objects and RPC | +| [🧠 ai-vectorize](ai-vectorize) | Tests using Workers AI and Vectorize | +| [🔄 context-exports](context-exports) | Tests using context exports | +| [📥 dynamic-import](dynamic-import) | Tests using dynamic imports | +| [🖼️ images](images) | Tests using the Images binding | [^1]: When using `exports.default` for integration tests, your worker code runs in the same context as the test runner. This means you can use global mocks to control your worker, but also means your worker uses the same subtly different module resolution behaviour provided by Vite. Usually this isn't a problem, but if you'd like to run your worker in a fresh environment that's as close to production as possible, using an auxiliary worker may be a good idea. Note this prevents global mocks from controlling your worker, and requires you to build your worker ahead-of-time. This means your tests won't re-run automatically if you change your worker's source code, but could be useful if you have a complicated build process (e.g. full-stack framework). diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/README.md b/fixtures/vitest-pool-workers-examples/durable-objects/README.md index 424a3671a0..018855f449 100644 --- a/fixtures/vitest-pool-workers-examples/durable-objects/README.md +++ b/fixtures/vitest-pool-workers-examples/durable-objects/README.md @@ -2,7 +2,8 @@ This Worker implements a counter with Durable Objects. Each object holds a single count. -| Test | Overview | -| --------------------------------------------------- | --------------------------------------------------------------------- | -| [direct-access.test.ts](test/direct-access.test.ts) | Tests for endpoints that also access object instance members directly | -| [alarm.test.ts](test/alarm.test.ts) | Tests that immediately execute object alarms | +| Test | Overview | +| --------------------------------------------------- | ---------------------------------------------------------------------- | +| [direct-access.test.ts](test/direct-access.test.ts) | Tests for endpoints that also access object instance members directly | +| [alarm.test.ts](test/alarm.test.ts) | Tests that immediately execute object alarms | +| [eviction.test.ts](test/eviction.test.ts) | Tests eviction, storage preservation and WebSocket hibernation/closing | diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/test/eviction.test.ts b/fixtures/vitest-pool-workers-examples/durable-objects/test/eviction.test.ts new file mode 100644 index 0000000000..5e1b1e08a3 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/test/eviction.test.ts @@ -0,0 +1,121 @@ +import { + evictAllDurableObjects, + evictDurableObject, + runInDurableObject, +} from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import { it } from "vitest"; +import { Counter } from "../src/"; + +function getResponseWebSocket(response: Response) { + const socket = response.webSocket; + if (socket === null || socket === undefined) { + throw new TypeError("Expected WebSocket response"); + } + return socket; +} + +function waitForMessage(socket: WebSocket) { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Timed out waiting for WebSocket message")); + }, 10_000); + socket.addEventListener("message", (event) => { + clearTimeout(timeout); + resolve( + typeof event.data === "string" + ? event.data + : new TextDecoder().decode(event.data as ArrayBuffer) + ); + }); + socket.addEventListener("error", () => { + clearTimeout(timeout); + reject(new Error("WebSocket error while waiting for message")); + }); + }); +} + +function waitForClose(socket: WebSocket) { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Timed out waiting for WebSocket close")); + }, 10_000); + socket.addEventListener("close", (event) => { + clearTimeout(timeout); + resolve(event); + }); + socket.addEventListener("error", () => { + clearTimeout(timeout); + reject(new Error("WebSocket error while waiting for close")); + }); + }); +} + +it("resets in-memory state but preserves storage on targeted eviction", async ({ + expect, +}) => { + const id = env.COUNTER.idFromName(`evict-${crypto.randomUUID()}`); + const stub = env.COUNTER.get(id); + + // Persist `count = 2` through the `fetch()` handler + expect(await (await stub.fetch("https://example.com/")).text()).toBe("1"); + expect(await (await stub.fetch("https://example.com/")).text()).toBe("2"); + + // Corrupt in-memory state without persisting it to storage + await runInDurableObject(stub, (instance: Counter) => { + instance.count = 999; + }); + + await evictDurableObject(stub, { webSockets: "hibernate" }); + + // After eviction the instance is torn down: the in-memory `999` is discarded + // and `count` is reloaded from storage (`2`), so the next increment yields `3` + expect(await (await stub.fetch("https://example.com/")).text()).toBe("3"); +}); + +it("resets all running instances with bulk eviction", async ({ expect }) => { + const id = env.COUNTER.idFromName(`evict-all-${crypto.randomUUID()}`); + const stub = env.COUNTER.get(id); + + expect(await (await stub.fetch("https://example.com/")).text()).toBe("1"); + await runInDurableObject(stub, (instance: Counter) => { + instance.count = 999; + }); + + await evictAllDurableObjects(); + + expect(await (await stub.fetch("https://example.com/")).text()).toBe("2"); +}); + +it("hibernates WebSockets across eviction", async ({ expect }) => { + const id = env.COUNTER.idFromName(`evict-ws-${crypto.randomUUID()}`); + const stub = env.COUNTER.get(id); + const response = await stub.fetch("https://example.com/websocket-order", { + headers: { Upgrade: "websocket" }, + }); + const socket = getResponseWebSocket(response); + socket.accept(); + + await evictDurableObject(stub); + + // The WebSocket should be hibernated rather than closed, so messages still + // round-trip after eviction (waking the Durable Object back up) + const messagePromise = waitForMessage(socket); + socket.send("after-eviction"); + expect(await messagePromise).toBe("after-eviction"); + socket.close(1000, "done"); +}); + +it("closes WebSockets when requested during eviction", async ({ expect }) => { + const id = env.COUNTER.idFromName(`evict-ws-close-${crypto.randomUUID()}`); + const stub = env.COUNTER.get(id); + const response = await stub.fetch("https://example.com/websocket-order", { + headers: { Upgrade: "websocket" }, + }); + const socket = getResponseWebSocket(response); + socket.accept(); + + const closePromise = waitForClose(socket); + await evictDurableObject(stub, { webSockets: "close" }); + expect(await closePromise).toBeDefined(); +}); diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index 2d7f2a7c26..85f811006d 100644 --- a/packages/miniflare/package.json +++ b/packages/miniflare/package.json @@ -52,7 +52,7 @@ "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "catalog:default", - "workerd": "1.20260623.1", + "workerd": "1.20260625.1", "ws": "catalog:default", "youch": "4.1.0-beta.10" }, diff --git a/packages/vitest-pool-workers/src/worker/durable-objects.ts b/packages/vitest-pool-workers/src/worker/durable-objects.ts index 22f0db21ce..1f6c0d3998 100644 --- a/packages/vitest-pool-workers/src/worker/durable-objects.ts +++ b/packages/vitest-pool-workers/src/worker/durable-objects.ts @@ -1,7 +1,13 @@ import assert from "node:assert"; import { env, exports } from "cloudflare:workers"; +import workerdUnsafe from "workerd:unsafe"; import { getSerializedOptions } from "./env"; import type { __VITEST_POOL_WORKERS_RUNNER_DURABLE_OBJECT__ } from "./index"; +import type { DurableObjectEvictionOptions } from "workerd:unsafe"; + +const DEFAULT_EVICTION_OPTIONS: DurableObjectEvictionOptions = { + webSockets: "hibernate", +}; const CF_KEY_ACTION = "vitestPoolWorkersDurableObjectAction"; @@ -163,6 +169,20 @@ export async function runDurableObjectAlarm( return await runInDurableObject(stub, runAlarm); } +// See public facing `cloudflare:test` types for docs +// (`async` so it throws asynchronously/rejects) +export async function evictDurableObject( + stub: DurableObjectStub, + options: DurableObjectEvictionOptions = DEFAULT_EVICTION_OPTIONS +): Promise { + if (!isDurableObjectStub(stub)) { + throw new TypeError( + "Failed to execute 'evictDurableObject': parameter 1 is not of type 'DurableObjectStub'." + ); + } + await workerdUnsafe.evict(stub, options); +} + /** * Internal method for running `callback` inside the I/O context of the * Runner Durable Object. diff --git a/packages/vitest-pool-workers/src/worker/lib/cloudflare/test.ts b/packages/vitest-pool-workers/src/worker/lib/cloudflare/test.ts index 5ced49a253..84c269f639 100644 --- a/packages/vitest-pool-workers/src/worker/lib/cloudflare/test.ts +++ b/packages/vitest-pool-workers/src/worker/lib/cloudflare/test.ts @@ -10,6 +10,8 @@ export { runInDurableObject, runDurableObjectAlarm, listDurableObjectIds, + evictDurableObject, + evictAllDurableObjects, createExecutionContext, waitOnExecutionContext, createScheduledController, diff --git a/packages/vitest-pool-workers/src/worker/reset.ts b/packages/vitest-pool-workers/src/worker/reset.ts index 3b494e1fed..035b458846 100644 --- a/packages/vitest-pool-workers/src/worker/reset.ts +++ b/packages/vitest-pool-workers/src/worker/reset.ts @@ -1,4 +1,9 @@ import workerdUnsafe from "workerd:unsafe"; +import type { DurableObjectEvictionOptions } from "workerd:unsafe"; + +const DEFAULT_EVICTION_OPTIONS: DurableObjectEvictionOptions = { + webSockets: "hibernate", +}; export async function reset(): Promise { await workerdUnsafe.deleteAllDurableObjects(); @@ -7,3 +12,10 @@ export async function reset(): Promise { export async function abortAllDurableObjects(): Promise { await workerdUnsafe.abortAllDurableObjects(); } + +// See public facing `cloudflare:test` types for docs +export async function evictAllDurableObjects( + options: DurableObjectEvictionOptions = DEFAULT_EVICTION_OPTIONS +): Promise { + await workerdUnsafe.evictAllDurableObjects(options); +} diff --git a/packages/vitest-pool-workers/src/worker/types-ambient.d.ts b/packages/vitest-pool-workers/src/worker/types-ambient.d.ts index 5a311d9276..521204e5fe 100644 --- a/packages/vitest-pool-workers/src/worker/types-ambient.d.ts +++ b/packages/vitest-pool-workers/src/worker/types-ambient.d.ts @@ -65,8 +65,24 @@ declare module "cloudflare:mock-agent" { } declare module "workerd:unsafe" { + export interface DurableObjectEvictionOptions { + webSockets?: "close" | "hibernate"; + } + function abortAllDurableObjects(): Promise; function deleteAllDurableObjects(): Promise; + function evict( + stub: DurableObjectStub, + options?: DurableObjectEvictionOptions + ): Promise; + function evictAllDurableObjects( + options?: DurableObjectEvictionOptions + ): Promise; - export default { abortAllDurableObjects, deleteAllDurableObjects }; + export default { + abortAllDurableObjects, + deleteAllDurableObjects, + evict, + evictAllDurableObjects, + }; } diff --git a/packages/vitest-pool-workers/types/cloudflare-test.d.ts b/packages/vitest-pool-workers/types/cloudflare-test.d.ts index 47c2204975..2d29f9d6ea 100644 --- a/packages/vitest-pool-workers/types/cloudflare-test.d.ts +++ b/packages/vitest-pool-workers/types/cloudflare-test.d.ts @@ -37,6 +37,39 @@ declare module "cloudflare:test" { export function runDurableObjectAlarm( stub: DurableObjectStub ): Promise; + export interface DurableObjectEvictionOptions { + /** + * Controls what happens to hibernatable WebSockets when evicting a Durable + * Object. Defaults to `"hibernate"`. + */ + webSockets?: "close" | "hibernate"; + } + /** + * Evicts the currently-running Durable Object pointed-to by `stub`, tearing + * down its instance to reset in-memory state while preserving durable + * storage. By default, hibernatable WebSockets are hibernated rather than closed, and + * eviction waits for in-flight requests to drain (with a timeout). + * + * Useful for testing how a Durable Object behaves across evictions, e.g. + * recovering state from storage or resuming hibernated WebSockets. + * + * Rejects if `stub` is not a Durable Object stub, if the target Durable + * Object is not currently running, or if its namespace has eviction + * prevented. Note this can only be used with `stub`s pointing to Durable + * Objects defined in the `main` worker. + * + * @example + * ```ts + * import { evictDurableObject } from "cloudflare:test"; + * + * await evictDurableObject(stub); + * await evictDurableObject(stub, { webSockets: "close" }); + * ``` + */ + export function evictDurableObject( + stub: DurableObjectStub, + options?: DurableObjectEvictionOptions + ): Promise; /** * Gets the IDs of all objects that have been created in the `namespace`. */ @@ -76,6 +109,30 @@ declare module "cloudflare:test" { */ export function abortAllDurableObjects(): Promise; + /** + * Evicts all currently-running Durable Objects in evictable namespaces. + * Unlike `abortAllDurableObjects()`, eviction is graceful: durable storage is + * preserved, hibernatable WebSockets are hibernated rather than closed by default, and + * eviction waits for in-flight requests to drain (with a timeout). In-memory + * state is reset by tearing down each instance. + * + * Non-running/idle actors are skipped, running facets are recursively + * evicted, and namespaces with eviction prevented are respected. + * + * @example + * ```ts + * import { evictAllDurableObjects } from "cloudflare:test"; + * import { afterEach } from "vitest"; + * + * afterEach(async () => { + * await evictAllDurableObjects(); + * }); + * ``` + */ + export function evictAllDurableObjects( + options?: DurableObjectEvictionOptions + ): Promise; + /** * Creates an instance of `ExecutionContext` for use as the 3rd argument to * modules-format exported handlers. diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 8e314037b9..dd14a92268 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -84,7 +84,7 @@ "miniflare": "workspace:*", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260623.1" + "workerd": "1.20260625.1" }, "devDependencies": { "@aws-sdk/client-s3": "^3.721.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 715b76738a..866e187204 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2249,8 +2249,8 @@ importers: specifier: catalog:default version: 7.28.0 workerd: - specifier: 1.20260623.1 - version: 1.20260623.1 + specifier: 1.20260625.1 + version: 1.20260625.1 ws: specifier: catalog:default version: 8.21.0 @@ -4280,8 +4280,8 @@ importers: specifier: 2.0.0-rc.24 version: 2.0.0-rc.24 workerd: - specifier: 1.20260623.1 - version: 1.20260623.1 + specifier: 1.20260625.1 + version: 1.20260625.1 devDependencies: '@aws-sdk/client-s3': specifier: ^3.721.0 @@ -5493,8 +5493,8 @@ packages: cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260623.1': - resolution: {integrity: sha512-MvDoIsRTsUJRzAl1/4hDXL839piyyjCeYatBHWgMc12Go7nHxkgbRih+1GJImEiKACSentu410bOupcutqFbpg==} + '@cloudflare/workerd-darwin-64@1.20260625.1': + resolution: {integrity: sha512-naCfBv0WnnTQIQPTniqMoUlklOIFjrAcSn1X+IAOhY8aFLF/xGYtFjs1eEE8sFib3ZuChGGpU23FFORVczqr0A==} engines: {node: '>=16'} cpu: [x64] os: [darwin] @@ -5511,8 +5511,8 @@ packages: cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260623.1': - resolution: {integrity: sha512-sNqHQvHPMOj5/BJOadtEZekRPSG5qQ0/ulC30ZRHRLnmx6tj5O4Wb3Nf0oznnI0pmjXhbv6b7+TOpDkaFMjbBg==} + '@cloudflare/workerd-darwin-arm64@1.20260625.1': + resolution: {integrity: sha512-jmH6zjp6Wrux46+qtFwDwrj+vd7s5bdwEqeGvdnwE0a4IEeAhKs0L42HQOyID+g5lkrHq9m55+AbhtmRAm63Pw==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] @@ -5529,8 +5529,8 @@ packages: cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-64@1.20260623.1': - resolution: {integrity: sha512-XYTWqTlZlCXqG+Po6awjXtlxw73hb3C39B/PP0sb4H9NI3V0eynq8Q7rXNe7DHJs2pWRfDJihQzpayQvpwf5wQ==} + '@cloudflare/workerd-linux-64@1.20260625.1': + resolution: {integrity: sha512-MiQkpA/dX8d83Zp64pzHUKfd6ca4cvwxnNobSP6CnXvfESvnNI9pfa+nfwnParla36sPmnYntNkjR7NjRuDeKQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] @@ -5547,8 +5547,8 @@ packages: cpu: [arm64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260623.1': - resolution: {integrity: sha512-PC5UDKA8oQB3Gek/Y+ysovdHNjp55CihOQZd7F9xPwpkv9qTBB0mhyHnfoG2YHtW1bb9CNhuwiThaNxegpE4mg==} + '@cloudflare/workerd-linux-arm64@1.20260625.1': + resolution: {integrity: sha512-LxxW7Qv60Xvv37+w6gUSDpYZziyqMy+cZWd9IvSA5ehVgKAxmzEaYPMiSZlxk32nbIWL9u/tfjXYCOKJ4Lo+XQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] @@ -5565,8 +5565,8 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20260623.1': - resolution: {integrity: sha512-OTHLCVYyN0pEfrajpjjnrGg5zA1GDnpNYmMz3x2ESFtH/oXRODsUQBllP7oJpJvMURF3rXSYwAhMojaftGry8w==} + '@cloudflare/workerd-windows-64@1.20260625.1': + resolution: {integrity: sha512-LH6iIX1HHaTwVKV5VokDxxUErXJzQoNZFRwVm7Vx/3fB/ApcTcRCUaMqcxI4as94jEUqg+pmX5czOndiveohow==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -15455,8 +15455,8 @@ packages: engines: {node: '>=16'} hasBin: true - workerd@1.20260623.1: - resolution: {integrity: sha512-9SJsdTSsehhqc26TUJIzyi1XgyYeqFym4hinZnWoAP1BkhEoMQ5Ygz7Xw9T+2ecU+y409JBEScBgWTdZ06mBrg==} + workerd@1.20260625.1: + resolution: {integrity: sha512-GApQvFX52SDM6L4u0+RRnUDB1wJOnEwoXjinkmOPtIyofWBxrlZckdegJSYc1leg++lLZ3+DQ4zMVmBqYVtzfA==} engines: {node: '>=16'} hasBin: true @@ -17136,7 +17136,7 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20260423.1': optional: true - '@cloudflare/workerd-darwin-64@1.20260623.1': + '@cloudflare/workerd-darwin-64@1.20260625.1': optional: true '@cloudflare/workerd-darwin-arm64@1.20260317.1': @@ -17145,7 +17145,7 @@ snapshots: '@cloudflare/workerd-darwin-arm64@1.20260423.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260623.1': + '@cloudflare/workerd-darwin-arm64@1.20260625.1': optional: true '@cloudflare/workerd-linux-64@1.20260317.1': @@ -17154,7 +17154,7 @@ snapshots: '@cloudflare/workerd-linux-64@1.20260423.1': optional: true - '@cloudflare/workerd-linux-64@1.20260623.1': + '@cloudflare/workerd-linux-64@1.20260625.1': optional: true '@cloudflare/workerd-linux-arm64@1.20260317.1': @@ -17163,7 +17163,7 @@ snapshots: '@cloudflare/workerd-linux-arm64@1.20260423.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260623.1': + '@cloudflare/workerd-linux-arm64@1.20260625.1': optional: true '@cloudflare/workerd-windows-64@1.20260317.1': @@ -17172,7 +17172,7 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260423.1': optional: true - '@cloudflare/workerd-windows-64@1.20260623.1': + '@cloudflare/workerd-windows-64@1.20260625.1': optional: true '@cloudflare/workers-editor-shared@0.1.1(@cloudflare/style-const@6.1.3(react@19.2.4))(@cloudflare/style-container@7.12.2(@cloudflare/style-const@6.1.3(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -27263,13 +27263,13 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260423.1 '@cloudflare/workerd-windows-64': 1.20260423.1 - workerd@1.20260623.1: + workerd@1.20260625.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260623.1 - '@cloudflare/workerd-darwin-arm64': 1.20260623.1 - '@cloudflare/workerd-linux-64': 1.20260623.1 - '@cloudflare/workerd-linux-arm64': 1.20260623.1 - '@cloudflare/workerd-windows-64': 1.20260623.1 + '@cloudflare/workerd-darwin-64': 1.20260625.1 + '@cloudflare/workerd-darwin-arm64': 1.20260625.1 + '@cloudflare/workerd-linux-64': 1.20260625.1 + '@cloudflare/workerd-linux-arm64': 1.20260625.1 + '@cloudflare/workerd-windows-64': 1.20260625.1 wrangler@4.76.0(@cloudflare/workers-types@4.20260623.1): dependencies: