Skip to content

Commit e63539d

Browse files
NuroDevjamesopstaddevin-ai-integration[bot]
authored
feat(wrangler): Support disabling state persistence in unstable_startWorker() (cloudflare#12815)
Co-authored-by: James Opstad <13586373+jamesopstad@users.noreply.github.com> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 556bce0 commit e63539d

8 files changed

Lines changed: 191 additions & 8 deletions

File tree

.changeset/kind-socks-beam.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Support disabling persistence in `unstable_startWorker()` and `unstable_dev()`
6+
7+
You can now disable persistence entirely by setting `persist: false` in the `dev` options:
8+
9+
```typescript
10+
const worker = await unstable_dev("./src/worker.ts", {
11+
persist: false,
12+
});
13+
```
14+
15+
Or when using `unstable_startWorker()`:
16+
17+
```typescript
18+
const worker = await unstable_startWorker({
19+
entrypoint: "./src/worker.ts",
20+
dev: {
21+
persist: false,
22+
},
23+
});
24+
```
25+
26+
This is useful for testing scenarios where you want to ensure a clean state on each run without any persisted data from previous runs.

packages/wrangler/src/__tests__/api/startDevWorker/LocalRuntimeController.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,74 @@ describe("LocalRuntimeController", () => {
733733
res = await fetch(urlFromParts(event.proxyData.userWorkerUrl));
734734
expect(await res.text()).toBe("miss");
735735
});
736+
it("should not persist data when persist is false", async () => {
737+
const bus = new FakeBus();
738+
const controller = new LocalRuntimeController(bus);
739+
teardown(() => controller.teardown());
740+
741+
const config = {
742+
dev: {
743+
persist: false,
744+
},
745+
entrypoint: "NOT_REAL",
746+
name: "worker",
747+
} satisfies Partial<StartDevWorkerOptions>;
748+
749+
const bundle = makeEsbuildBundle(dedent/*javascript*/ `
750+
export default {
751+
async fetch(request, env, ctx) {
752+
const key = "http://localhost/";
753+
if (request.method === "POST") {
754+
const response = new Response("cached", {
755+
headers: {
756+
"Cache-Control": "max-age=3600"
757+
}
758+
});
759+
await caches.default.put(key, response);
760+
}
761+
return (await caches.default.match(key)) ?? new Response("miss");
762+
}
763+
}`);
764+
765+
controller.onBundleStart({
766+
config: configDefaults(config),
767+
type: "bundleStart",
768+
});
769+
770+
controller.onBundleComplete({
771+
bundle,
772+
config: configDefaults(config),
773+
type: "bundleComplete",
774+
});
775+
776+
let event = await bus.waitFor("reloadComplete");
777+
let res = await fetch(urlFromParts(event.proxyData.userWorkerUrl), {
778+
method: "POST",
779+
});
780+
expect(await res.text()).toBe("cached");
781+
782+
// Check that data is cached within the same session
783+
res = await fetch(urlFromParts(event.proxyData.userWorkerUrl));
784+
expect(await res.text()).toBe("cached");
785+
786+
// Restart the worker - data should NOT be persisted since persist is false
787+
await controller.teardown();
788+
controller.onBundleStart({
789+
config: configDefaults(config),
790+
type: "bundleStart",
791+
});
792+
controller.onBundleComplete({
793+
bundle,
794+
config: configDefaults(config),
795+
type: "bundleComplete",
796+
});
797+
798+
event = await bus.waitFor("reloadComplete");
799+
res = await fetch(urlFromParts(event.proxyData.userWorkerUrl));
800+
801+
// Data should be gone since persistence was disabled
802+
expect(await res.text()).toBe("miss");
803+
});
736804
it("should expose KV namespace bindings", async () => {
737805
const bus = new FakeBus();
738806
const controller = new LocalRuntimeController(bus);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import path from "node:path";
2+
import { describe, it } from "vitest";
3+
import { getLocalPersistencePath } from "../../dev/get-local-persistence-path";
4+
import { runInTempDir } from "../helpers/run-in-tmp";
5+
import type { Config } from "@cloudflare/workers-utils";
6+
7+
function makeConfig(userConfigPath?: string): Config {
8+
return {
9+
userConfigPath,
10+
} as Config;
11+
}
12+
13+
describe("getLocalPersistencePath", () => {
14+
runInTempDir();
15+
16+
describe("when persistence is disabled", () => {
17+
it("should return `false` when `persistTo` is `false`", ({ expect }) => {
18+
const result = getLocalPersistencePath(false, makeConfig());
19+
expect(result).toBe(false);
20+
});
21+
});
22+
23+
describe("when persistence is enabled with default path", () => {
24+
it("should return `.wrangler/state` relative to cwd when no config path", ({
25+
expect,
26+
}) => {
27+
const result = getLocalPersistencePath(undefined, makeConfig());
28+
expect(result).toBe(path.resolve(process.cwd(), ".wrangler/state"));
29+
});
30+
31+
it("should return `.wrangler/state` relative to config file directory", ({
32+
expect,
33+
}) => {
34+
const configPath = "/some/project/wrangler.json";
35+
const result = getLocalPersistencePath(undefined, makeConfig(configPath));
36+
expect(result).toBe(path.resolve("/some/project", ".wrangler/state"));
37+
});
38+
});
39+
40+
describe("when persistence path is explicitly specified", () => {
41+
it("should resolve relative path from cwd", ({ expect }) => {
42+
const result = getLocalPersistencePath("./custom-persist", makeConfig());
43+
expect(result).toBe(path.resolve(process.cwd(), "./custom-persist"));
44+
});
45+
46+
it("should resolve absolute path from cwd", ({ expect }) => {
47+
const absolutePath = "/absolute/persist/path";
48+
const result = getLocalPersistencePath(absolutePath, makeConfig());
49+
expect(result).toBe(path.resolve(process.cwd(), absolutePath));
50+
});
51+
52+
it("should resolve relative path from cwd even when config path is set", ({
53+
expect,
54+
}) => {
55+
// When a custom persistTo is specified, it's always relative to cwd, not config path
56+
const configPath = "/some/project/wrangler.json";
57+
const result = getLocalPersistencePath(
58+
"./custom-persist",
59+
makeConfig(configPath)
60+
);
61+
expect(result).toBe(path.resolve(process.cwd(), "./custom-persist"));
62+
});
63+
});
64+
});

packages/wrangler/src/api/startDevWorker/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ export interface StartDevWorkerInput {
145145
remote?: boolean | "minimal";
146146
/** Cloudflare Account credentials. Can be provided upfront or as a function which will be called only when required. */
147147
auth?: AsyncHook<CfAccount, [Pick<Config, "account_id">]>; // provide config.account_id as a hook param
148-
/** Whether local storage (KV, Durable Objects, R2, D1, etc) is persisted. You can also specify the directory to persist data to. */
149-
persist?: string;
148+
/** Whether local storage (KV, Durable Objects, R2, D1, etc) is persisted. You can also specify the directory to persist data to. Set to `false` to disable persistence. */
149+
persist?: string | false;
150150
/** Controls which logs are logged 🤙. */
151151
logLevel?: LogLevel;
152152
/** Whether the worker server restarts upon source/config file changes. */
@@ -202,7 +202,7 @@ export interface StartDevWorkerInput {
202202

203203
export type StartDevWorkerOptions = Omit<
204204
StartDevWorkerInput,
205-
"assets" | "containers"
205+
"assets" | "containers" | "dev"
206206
> & {
207207
/** A worker's directory. Usually where the Wrangler configuration file is located */
208208
projectRoot: string;
@@ -221,7 +221,7 @@ export type StartDevWorkerOptions = Omit<
221221
site?: Config["site"];
222222
};
223223
dev: StartDevWorkerInput["dev"] & {
224-
persist: string;
224+
persist: string | false;
225225
auth?: AsyncHook<CfAccount>; // redefine without config.account_id hook param (can only be provided by ConfigController with access to the Wrangler configuration file, not by other controllers eg RemoteRuntimeContoller)
226226
};
227227
entrypoint: string;

packages/wrangler/src/dev.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,8 @@ export type StartDevOptions = DevArguments &
356356
enableIpc?: boolean;
357357
dockerPath?: string;
358358
containerEngine?: string;
359+
/** Set to `false` to disable persistence. When `true` or `undefined`, uses default persistence path. */
360+
persist?: boolean;
359361
};
360362

361363
export async function getHostAndRoutes(

packages/wrangler/src/dev/get-local-persistence-path.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,34 @@ import type { Config } from "@cloudflare/workers-utils";
66
*
77
* We use the `userConfigPath` rather than the potentially redirected `configPath`
88
* to decide the path to this directory.
9+
*
10+
* @param persistTo - The path to persist to, or `false` to disable persistence, or `undefined` to use the default path.
11+
*
12+
* @returns The path to persist to, or `false` if persistence is disabled.
913
*/
14+
export function getLocalPersistencePath(
15+
persistTo: false,
16+
config: Config
17+
): false;
18+
1019
export function getLocalPersistencePath(
1120
persistTo: string | undefined,
21+
config: Config
22+
): string;
23+
24+
export function getLocalPersistencePath(
25+
persistTo: string | false | undefined,
26+
config: Config
27+
): string | false;
28+
29+
export function getLocalPersistencePath(
30+
persistTo: string | false | undefined,
1231
{ userConfigPath }: Config
13-
) {
32+
): string | false {
33+
if (persistTo === false) {
34+
return false;
35+
}
36+
1437
return persistTo
1538
? // If path specified, always treat it as relative to cwd()
1639
path.resolve(process.cwd(), persistTo)

packages/wrangler/src/dev/miniflare/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export interface ConfigBundle {
8080
rules: Config["rules"];
8181
inspectorPort: number | undefined;
8282
inspectorHost: string | undefined;
83-
localPersistencePath: string | null;
83+
localPersistencePath: string | false;
8484
liveReload: boolean;
8585
crons: Config["triggers"]["crons"];
8686
queueConsumers: Config["queues"]["consumers"];
@@ -883,7 +883,7 @@ export function buildMiniflareBindingOptions(
883883
export function getDefaultPersistRoot(
884884
localPersistencePath: ConfigBundle["localPersistencePath"]
885885
): string | undefined {
886-
if (localPersistencePath !== null) {
886+
if (localPersistencePath !== false) {
887887
const v3Path = path.join(localPersistencePath, "v3");
888888
return v3Path;
889889
}

packages/wrangler/src/dev/start-dev.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ async function setupDevEnv(
257257
? undefined
258258
: args.upstreamProtocol === "https",
259259
},
260-
persist: args.persistTo,
260+
persist: args.persist === false ? false : args.persistTo,
261261
liveReload: args.liveReload,
262262
testScheduled: args.testScheduled,
263263
logLevel: args.logLevel,

0 commit comments

Comments
 (0)