跳到內容

ch20-cron

取得並執行

這個範例可以獨立 clone 執行,不依賴其他章節。

npx degit mrsuner/cloudflare-serverless-platform-manuals/examples/ch20-cron
cd ch20-cron
npm install

可用指令

npm run dev	# wrangler dev
npm run typecheck	# tsc --noEmit
npm run cf-typegen	# wrangler types --env-interface CloudflareBindings

說明

Companion example for docs/20-cron-triggers.md.

Terminal window
npm install && npm run dev
Terminal window
B=localhost:8787
curl -s "$B/clear"
curl -s "$B/cdn-cgi/handler/scheduled?format=json"
curl -s "$B/cdn-cgi/handler/scheduled?cron=0+15+1+*+*&format=json"
curl -s "$B/cdn-cgi/handler/scheduled?cron=*%2F3+*+*+*+*&time=1745856238000&format=json"
curl -s "$B/runs"

2026-08-01 · wrangler@4.114.0 · workerd@1.20260722.1

GET /cdn-cgi/handler/scheduled -> ok
GET /cdn-cgi/handler/scheduled?format=json -> {"outcome":"ok","noRetry":false}
GET /__scheduled -> 404

wrangler dev --test-scheduled is likewise absent from current docs.

Without ?cron=, controller.cron is an empty string

Section titled “Without ?cron=, controller.cron is an empty string”
cron='' scheduledTime=1785560195010 -> branch: other
cron='0 15 1 * *' scheduledTime=1785560195018 -> branch: monthly
cron='*/3 * * * *' scheduledTime=1745856238000 -> branch: frequent

Not the first configured cron — empty. Any switch (controller.cron) falls to default during local testing unless you pass ?cron=. ?time= sets scheduledTime (the third row is the timestamp I passed in).

{"ownProps":["cron","scheduledTime"],
"proto":["noRetry","constructor"],
"type":null}
interface ScheduledController {
readonly scheduledTime: number;
readonly cron: string;
noRetry(): void;
}

controller.type is documented but does not exist — absent from both the generated types and the runtime. noRetry() is the reverse: present in types and runtime, missing from the Scheduled Handler API reference page.

ctx in a scheduled handler has the same shape as in fetch, and waitUntil does work here (unlike inside a Durable Object, chapter 14).

after controller.noRetry() -> {"outcome":"ok","noRetry":true}
handler throws -> {"outcome":"exception","noRetry":false}

This is what the dashboard’s Cron Events table records, so error handling can be validated locally.

原始碼

wrangler.jsonc

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "ch20-cron",
  "main": "src/index.ts",
  "compatibility_date": "2026-07-24",
  "observability": { "enabled": true },
  "triggers": {
    "crons": [
      "*/3 * * * *",      // every 3 minutes
      "0 15 1 * *",       // 15:00 UTC on the 1st
      "59 23 LW * *",     // last weekday of the month
      "0 18 * * friL"     // last Friday of the month
    ]
  }
}

package.json

{ "name": "ch20-cron", "private": true, "type": "module",
  "scripts": { "dev": "wrangler dev", "typecheck": "tsc --noEmit",
    "cf-typegen": "wrangler types --env-interface CloudflareBindings" },
  "devDependencies": { "typescript": "^5.9.0", "wrangler": "^4.114.0" } }

src/index.ts

// ---------------------------------------------------------------------------
// Chapter 20 — Cron Triggers.
// ---------------------------------------------------------------------------
const runs: Array<Record<string, unknown>> = [];
let noRetryNext = false;
let throwNext = false;

export default {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname === "/runs") return Response.json({ count: runs.length, runs });
    if (url.pathname === "/clear") { runs.length = 0; return Response.json({ ok: true }); }
    if (url.pathname === "/noretry") { noRetryNext = url.searchParams.get("on") === "1"; return Response.json({ noRetryNext }); }
    if (url.pathname === "/throw") { throwNext = true; return Response.json({ throwNext }); }
    return new Response("try /runs /clear, and POST /cdn-cgi/handler/scheduled\n", { status: 404 });
  },

  async scheduled(
    controller: ScheduledController,
    env: CloudflareBindings,
    ctx: ExecutionContext,
  ): Promise<void> {
    const c = controller as ScheduledController & { noRetry?: () => void; type?: string };

    runs.push({
      cron: controller.cron,
      scheduledTime: controller.scheduledTime,
      scheduledTimeIso: new Date(controller.scheduledTime).toISOString(),
      // `type` and `noRetry` are barely documented — probe them.
      type: c.type ?? null,
      ownProps: Object.getOwnPropertyNames(controller),
      proto: Object.getOwnPropertyNames(Object.getPrototypeOf(controller)),
      hasNoRetry: typeof c.noRetry === "function",
      // ctx in a scheduled handler
      ctxOwn: Object.getOwnPropertyNames(ctx),
      ctxProto: Object.getOwnPropertyNames(Object.getPrototypeOf(ctx)),
    });

    // Demonstrate branching on which schedule fired.
    switch (controller.cron) {
      case "*/3 * * * *":
        runs.push({ branch: "frequent" });
        break;
      case "0 15 1 * *":
        runs.push({ branch: "monthly" });
        break;
      default:
        runs.push({ branch: "other", cron: controller.cron });
    }

    if (noRetryNext) { c.noRetry?.(); runs.push({ calledNoRetry: true }); }
    if (throwNext) { throwNext = false; throw new Error("deliberate scheduled failure"); }
  },
} satisfies ExportedHandler<CloudflareBindings>;

tsconfig.json

{ "compilerOptions": { "target": "esnext", "lib": ["esnext"], "module": "esnext",
  "moduleResolution": "bundler", "types": ["./worker-configuration.d.ts"],
  "strict": true, "skipLibCheck": true, "noEmit": true, "isolatedModules": true },
  "include": ["src/**/*.ts", "worker-configuration.d.ts"] }