Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/kind-geese-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"miniflare": patch
---

Enable local scheduled handler dispatch for Workers + Assets (#9882)

It is now possible to trigger a scheduled handler on a Worker that has assets.
7 changes: 7 additions & 0 deletions .changeset/preserve-all-deploy-flags-in-autoconfig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": patch
---

Preserve all deployment-affecting CLI flags in the interactive deploy config flow

When running `wrangler deploy` without a config file and going through the interactive setup flow, CLI flags beyond `--compatibility-flags` (such as `--routes`/`--route`, `--domains`/`--domain`, `--triggers`, `--var`, `--define`, `--alias`, `--jsx-factory`, `--jsx-fragment`, `--tsconfig`, `--minify`, `--upload-source-maps`, `--no-bundle`, `--logpush`, `--keep-vars`, `--legacy-env`, and `--dispatch-namespace`) were silently dropped. These flags are now persisted to the generated `wrangler.jsonc` config file (where a config field equivalent exists) and included in the suggested CLI command when the user declines config file generation.
1 change: 1 addition & 0 deletions packages/miniflare/src/plugins/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ export const ASSETS_PLUGIN: Plugin<typeof AssetsOptionsSchema> = {
name: `${RPC_PROXY_SERVICE_NAME}:${id}`,
worker: {
compatibilityDate: "2024-08-01",
compatibilityFlags: ["service_binding_extra_handlers"],
modules: [
{
name: "assets-proxy-worker.mjs",
Expand Down
22 changes: 22 additions & 0 deletions packages/miniflare/src/workers/assets/rpc-proxy.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@ export default class RPCProxyWorker extends WorkerEntrypoint<Env> {
return this.env.ROUTER_WORKER.fetch(request);
}

// Forward scheduled events to the User Worker. The proxy itself doesn't run
// any scheduled logic; it just dispatches a real scheduled event to the user
// worker via the Fetcher built-in, then propagates the user worker's noRetry
// decision back onto this controller so the outcome surfaces correctly to
// the caller (e.g. the entry worker's `/cdn-cgi/handler/scheduled` handler).
async scheduled(controller: ScheduledController) {
const result = await this.env.USER_WORKER.scheduled?.({
cron: controller.cron,
scheduledTime: new Date(controller.scheduledTime),
});
if (result?.noRetry) {
controller.noRetry();
}
if (result?.outcome !== "ok") {
// Re-throw so workerd surfaces `outcome: "exception"` to the caller
// rather than swallowing the user worker's failure.
throw new Error(
`User Worker scheduled handler failed with outcome: ${result?.outcome}`
);
}
}

tail(events: TraceItem[]) {
// Temporary workaround: the tail events is not serializable over capnproto yet
// But they are effectively JSON, so we are serializing them to JSON and parsing it back to make it transferable.
Expand Down
83 changes: 83 additions & 0 deletions packages/miniflare/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,89 @@ test("Miniflare: manually triggered scheduled events", async ({ expect }) => {
expect(await res.text()).toBe("true");
});

test("Miniflare: manually triggered scheduled events with assets", async ({
expect,
}) => {
const log = new TestLog();
const tmp = await useTmp();
await fs.writeFile(
path.join(tmp, "foo.html"),
"<!doctype html><p>asset</p>",
"utf8"
);
await fs.writeFile(path.join(tmp, "foo.md"), "asset", "utf8");
const mf = new Miniflare({
log,
modules: true,
script: `
let scheduledRun = false;
let cron;
let scheduledTime;
export default {
fetch() {
return Response.json({ scheduledRun, cron, scheduledTime });
},
scheduled(controller, env, ctx) {
scheduledRun = true;
cron = controller.cron;
scheduledTime = Number(controller.scheduledTime);
controller.noRetry();
}
}`,
assets: {
directory: tmp,
routerConfig: {
has_user_worker: true,
},
},
unsafeTriggerHandlers: true,
});
useDispose(mf);

type ScheduledResult = {
scheduledRun: boolean;
cron?: string;
scheduledTime?: number;
};

let res = await mf.dispatchFetch("http://localhost");
let json = (await res.json()) as ScheduledResult;
expect(json.scheduledRun).toBe(false);
expect(json.cron).toBe(undefined);
expect(json.scheduledTime).toBe(undefined);

res = await mf.dispatchFetch("http://localhost/foo");
expect(res.headers.get("content-type")).toBe("text/html; charset=utf-8");
expect(await res.text()).toBe("<!doctype html><p>asset</p>");

res = await mf.dispatchFetch("http://localhost/foo.md");
expect(res.headers.get("content-type")).toBe("text/markdown; charset=utf-8");
expect(await res.text()).toBe("asset");

res = await mf.dispatchFetch("http://localhost/cdn-cgi/handler/scheduled");
expect(await res.text()).toBe("ok");

res = await mf.dispatchFetch("http://localhost");
json = (await res.json()) as ScheduledResult;
expect(json.scheduledRun).toBe(true);
expect(json.cron).toBe("");
expect(json.scheduledTime).toBeDefined();

res = await mf.dispatchFetch(
"http://localhost/cdn-cgi/handler/scheduled?format=json&cron=0+0+0+0+0&time=1234567890987"
);
expect(await res.json()).toEqual({
outcome: "ok",
noRetry: true,
});

res = await mf.dispatchFetch("http://localhost");
json = (await res.json()) as ScheduledResult;
expect(json.scheduledRun).toBe(true);
expect(json.cron).toBe("0 0 0 0 0");
expect(json.scheduledTime).toBe(1234567890987);
});

test("Miniflare: manually triggered email handler - valid email", async ({
expect,
}) => {
Expand Down
Loading
Loading