跳到內容

ch03-execution-model

取得並執行

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

npx degit mrsuner/cloudflare-serverless-platform-manuals/examples/ch03-execution-model
cd ch03-execution-model
npm install

可用指令

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

說明

Companion example for docs/03-execution-model.md.

Terminal window
npm install
npm run cf-typegen
npm run dev
Terminal window
$ curl -s localhost:8787/ctx
{"own":["tracing","access","cache","props","exports"],
"prototype":["waitUntil","passThroughOnException","constructor"]}

Seven members. The official Context docs page documents four — tracing, access and cache are undocumented there.

2. Background work without waitUntil is discarded

Section titled “2. Background work without waitUntil is discarded”
Terminal window
$ curl -s localhost:8787/reset
$ curl -s "localhost:8787/fire?mode=none"
{"mode":"none","started":true}
$ sleep 4 && curl -s localhost:8787/events
{"events":["none:started"]} # none:completed never appears
$ curl -s "localhost:8787/fire?mode=waituntil"
{"mode":"waituntil","started":true}
$ sleep 4 && curl -s localhost:8787/events
{"events":["none:started","waituntil:started","waituntil:completed"]}

The only difference between the two paths is the ctx.waitUntil(task) call. Reproducible locally.

3. request.cf is a convincing fake in local dev

Section titled “3. request.cf is a convincing fake in local dev”
Terminal window
$ curl -s localhost:8787/cf
{"cf":{"asn":395747,"colo":"DFW","city":"Austin","region":"Texas",
"postalCode":"78701","country":"US","timezone":"America/Chicago",
"latitude":"30.27130","longitude":"-97.74260","tlsVersion":"TLSv1.3",...}}

Nobody here is in Austin, Texas. Wrangler tries to fetch a real cf object and falls back to a placeholder, logging only:

[wrangler:warn] Unable to fetch the `Request.cf` object! Falling back to a default placeholder...

Every field is well-formed and internally consistent, so geo logic “works” locally against fabricated data. Verify after deploying.

Terminal window
$ curl -o /dev/null -w "%{http_code}\n" "localhost:8787/boom" # 500
$ curl -o /dev/null -w "%{http_code}\n" "localhost:8787/boom?pass=1" # 500

Both 500 locally and on workers.dev — there is no origin to pass through to. The difference only shows on a route in front of a real origin.

The Context docs say ctx.exports “requires enable_ctx_exports compatibility flag”. Adding it makes the Worker fail to start:

✘ [ERROR] The Workers runtime failed to start.
Runtime stderr: The compatibility flag enable_ctx_exports became the default
as of 2025-11-17 so does not need to be specified anymore.

Omit the flag. ctx.exports is already there.

2026-07-28 · wrangler@4.114.0 · workerd@1.20260722.1 · compatibility_date: 2026-07-24

原始碼

wrangler.jsonc

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "ch03-execution-model",
  "main": "src/index.ts",
  "compatibility_date": "2026-07-24",
  "observability": { "enabled": true }
}

package.json

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

src/index.ts

// ---------------------------------------------------------------------------
// Chapter 03 — The invocation lifecycle.
//
// Module scope: shared by every request this isolate serves (chapter 01).
// Used here only as an observation log, never for per-request state.
// ---------------------------------------------------------------------------
const events: string[] = [];
const record = (e: string) => void events.push(e);

export default {
  async fetch(
    request: Request,
    env: CloudflareBindings,
    ctx: ExecutionContext,
  ): Promise<Response> {
    const url = new URL(request.url);

    switch (url.pathname) {
      // --- What is actually on `ctx`? --------------------------------------
      // The docs page lists four members. The runtime has seven.
      case "/ctx":
        return Response.json({
          own: Object.getOwnPropertyNames(ctx),
          prototype: Object.getOwnPropertyNames(Object.getPrototypeOf(ctx)),
        });

      // --- Geo / TLS metadata ----------------------------------------------
      // In `wrangler dev` this is a realistic-looking PLACEHOLDER, not your
      // real location. See the README.
      case "/cf":
        return Response.json({ cf: request.cf ?? null });

      // --- Does background work survive the response? -----------------------
      // /fire?mode=none      -> fire and forget: killed, never completes
      // /fire?mode=waituntil -> handed to ctx.waitUntil(): completes
      case "/fire": {
        const mode = url.searchParams.get("mode") ?? "none";
        const task = (async () => {
          await scheduler.wait(2000);
          record(`${mode}:completed`);
        })();

        if (mode === "waituntil") ctx.waitUntil(task);

        record(`${mode}:started`);
        return Response.json({ mode, started: true });
      }

      // --- Fail open instead of returning 500 -------------------------------
      // With ?pass=1 the request is passed through to the origin instead of
      // becoming an error. On a workers.dev route there is no origin, so the
      // observable difference is in the logs, not the response.
      case "/boom":
        if (url.searchParams.get("pass") === "1") ctx.passThroughOnException();
        throw new Error("deliberate failure");

      case "/events":
        return Response.json({ events });

      case "/reset":
        events.length = 0;
        return Response.json({ ok: true });

      default:
        return new Response(
          [
            "Chapter 03 — invocation lifecycle",
            "",
            "  GET /ctx                    real shape of the ExecutionContext",
            "  GET /cf                     request.cf (placeholder in local dev)",
            "  GET /fire?mode=none         background work WITHOUT waitUntil",
            "  GET /fire?mode=waituntil    background work WITH waitUntil",
            "  GET /events                 what actually completed",
            "  GET /reset                  clear the log",
            "  GET /boom[?pass=1]          exception, with/without passthrough",
            "",
          ].join("\n"),
          { status: 404, headers: { "content-type": "text/plain; charset=utf-8" } },
        );
    }
  },
} 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"]
}