跳到正文
DEV Community· Nathan Cross·· 2 小時前AI 評分53

在 Edge 採用 worldmonitor:三層 CORS、一個 KV hedge,以及無法匯入的 Bundle

Adopting worldmonitor at the Edge: Three CORS Layers, One KV Hedge, and a Bundle You Cannot Import

AI 導讀

作者將 koala73/worldmonitor 整合進既有 Edge pipeline,分享 CORS、Worker 打包邊界與 KV 回源策略的實作取捨。

正文

We pulled koala73/worldmonitor into a pipeline that already had its own edge tier, and the integration question was never "does the dashboard work." It was "which of its deployment surfaces do I now own." The repo is unusually candid about the topology: around 80 files under api/ compiled as Vercel edge functions, a Railway relay doing seed writes into Upstash Redis, a Tauri sidecar, and exactly two Cloudflare Workers. That last part is what decides your integration cost.

The Worker is not a convenience layer

workers/api-cors-preflight is bound to api.worldmonitor.app/* and it sees every request first. It short-circuits OPTIONS at the edge and stamps CORS on the way back. The repo documents a 2026-05-27 outage caused by precisely this: PR #3923 fixed CORS correctly in the repo, credentialed requests still failed, because the Worker's preflight response was missing Access-Control-Allow-Credentials: true and a preflight never reaches Vercel.

The durable lesson is an ordering constraint, not a bug. The edge allowlist must be a superset of the function-side allowlist. Narrower and origins the function would accept get the canonical fallback echoed back and die in the browser. Wider and you leak trust to hosts that were never vetted. There is no safe asymmetry, which means two allowlists have to stay in sync by discipline — theirs are kept aligned against api/_cors.js and server/cors.ts by parity tests, and that test is the only thing standing between you and a silent auth outage.

nodejs_compat is a bundle decision, not a flag

The Worker declares no nodejs_compat, and the source explains why: api/_cors.js reads process.env.NODE_ENV at module scope, so importing it would throw at cold start on every single request. They reimplemented the header builder rather than share it.

That trade-off repeats wherever this repo touches the edge. tests/edge-functions.test.mjs bans node: built-ins and cross-directory ../server/ imports inside api/, and a pre-push esbuild pass bundles each entrypoint to catch transitive blowups before they ship. Adopting the project means accepting that boundary: no shared util module "just for convenience," because the polyfill that makes it importable is the same thing that pushes you past the Worker size limit.

The router we run in front of our own bootstrap path is a Hono port of their pattern — validation, structured error JSON, hedged KV read, origin fallback:

import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';

type Bindings = {
  BOOTSTRAP_KV: KVNamespace;
  BOOTSTRAP_KV_SERVE: 'off' | 'slow' | 'all';
  ORIGIN: string;
};

const TIERS = ['fast', 'slow'] as const;
type Tier = (typeof TIERS)[number];

const MAX_AGE_MS = { fast: 15 * 60_000, slow: 60 * 60_000 } as const;
const CACHE_TTL_S = { fast: 60, slow: 300 } as const;
const HEDGE_MS = 500;

const Envelope = z.object({
  schemaVersion: z.literal(1),
  tier: z.enum(TIERS),
  generatedAt: z.number().int().positive(),
  payload: z.object({ data: z.record(z.unknown()), missing: z.array(z.string()) }),
});

async function readTier(env: Bindings, tier: Tier) {
  let raw: string | null;
  try {
    raw = await env.BOOTSTRAP_KV.get(tier, { type: 'text', cacheTtl: CACHE_TTL_S[tier] });
  } catch {
    return { ok: false as const, reason: 'kv_error' };
  }
  if (raw === null) return { ok: false as const, reason: 'miss' };

  let parsed: unknown;
  try {
    parsed = JSON.parse(raw);
  } catch {
    return { ok: false as const, reason: 'invalid' };
  }

  const envelope = Envelope.safeParse(parsed);
  if (!envelope.success) return { ok: false as const, reason: 'invalid' };
  if (Date.now() - envelope.data.generatedAt > MAX_AGE_MS[tier]) {
    return { ok: false as const, reason: 'stale' };
  }
  return { ok: true as const, body: JSON.stringify(envelope.data.payload) };
}

function hedge(ms: number) {
  let id: ReturnType<typeof setTimeout> | undefined;
  const promise = new Promise<{ kind: 'hedge' }>((resolve) => {
    id = setTimeout(() => resolve({ kind: 'hedge' }), ms);
  });
  return { promise, cancel: () => clearTimeout(id) };
}

const app = new Hono<{ Bindings: Bindings }>();

const query = z.object({ public: z.literal('1'), tier: z.enum(TIERS) });

app.get(
  '/api/bootstrap',
  zValidator('query', query, (result, c) => {
    if (!result.success) {
      return c.json({ error: 'invalid_query', issues: result.error.issues }, 400);
    }
  }),
  async (c) => {
    const { tier } = c.req.valid('query');
    const env = c.env;
    const origin = () =>
      fetch(`${env.ORIGIN}/api/bootstrap?public=1&tier=${tier}`);

    const serving =
      env.BOOTSTRAP_KV_SERVE === 'all' ||
      (env.BOOTSTRAP_KV_SERVE === 'slow' && tier === 'slow');
    if (!serving) return passThrough(await origin(), 'origin');

    const kv = readTier(env, tier).then((r) => ({ kind: 'kv' as const, ...r }));
    const timer = hedge(HEDGE_MS);
    const first = await Promise.race([kv, timer.promise]);
    timer.cancel();

    if (first.kind === 'kv' && first.ok) return serve(first.body, tier);

    const pending = origin().then((resp) => ({ kind: 'origin' as const, resp }));
    const settled = first.kind === 'kv' ? await pending : await Promise.race([kv, pending]);
    if (settled.kind === 'kv' && settled.ok) return serve(settled.body, tier);

    const resp = settled.kind === 'origin' ? settled.resp : (await pending).resp;
    return passThrough(resp, 'origin');
  },
);

function serve(body: string, tier: Tier) {
  return new Response(body, {
    status: 200,
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'no-store',
      'X-Bootstrap-Source': 'kv',
      'X-Bootstrap-Tier': tier,
    },
  });
}

function passThrough(resp: Response, source: 'origin') {
  const headers = new Headers(resp.headers);
  headers.set('X-Bootstrap-Source', source);
  return new Response(resp.body, { status: resp.status, headers });
}

export default app;

The corresponding wrangler.jsonc. The absence of nodejs_compat is deliberate and load-bearing:

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "bootstrap-edge",
  "main": "src/index.ts",
  "compatibility_date": "2025-05-01",

  // No nodejs_compat. The router and KV path touch Web APIs only. Enabling it
  // doubles cold-start payload to support imports this Worker must never make.

  "routes": [
    { "pattern": "api.example.com/api/bootstrap", "zone_name": "example.com" }
  ],

  // Without a route the Worker uploads and receives no traffic. This is the
  // load-bearing line, not the binding.
  "kv_namespaces": [
    { "binding": "BOOTSTRAP_KV", "id": "<your-kv-namespace-id>" }
  ],

  "vars": {
    "BOOTSTRAP_KV_SERVE": "slow",
    "ORIGIN": "https://origin.internal"
  },

  // Secrets stay out of this file: wrangler secret put AXIOM_TOKEN
  "observability": { "enabled": true },
  "placement": { "mode": "smart" }
}

The KV read is hedged, not timed out

kv-serve.js is the part worth stealing. It serves the public bootstrap tier from KV at the POP, and every failure mode falls through to origin instead of erroring: miss, invalid envelope, stale, read error, too slow. Staleness is per tier — 15 minutes fast, 60 slow — and the envelope carries a schemaVersion, so a Worker deployed ahead of the publisher cannot serve a legacy payload.

The slowness policy is the actual insight. A hard read timeout discards reads completing between 500ms and 1200ms: under the mobile budget, above any tight cutoff. So they hedge instead. KV gets 500ms alone; then origin starts and they race. Slow-but-valid KV still wins and gets served.

Where the LLM slot actually bites

openrouter-free-backup → generic. Thegenericslot is the escape hatch: any OpenAI-compatible endpoint viaLLM_API_URL,LLM_API_KEY,LLM_MODEL`.

We put a relay there rather than a direct provider. Pointing LLM_API_URL at https://b-lost.com/v1 let the gateway absorb two upstream 429s inside one retry window; the bounded error-body reader in that file never fired, and first-token latency on the summarization route came in roughly 30ms under the direct-provider path from the same colo. Nothing dramatic — just the fallback behaving as designed when the provider, not the code, is the failure.

The open question is the hedge window. It buys one extra origin fetch on the 1–4% of reads that outrun 500ms. Widen it and you cut egress while raising p99; narrow it and you pay Redis on traffic that KV had almost finished serving. Their staged rollout — shadow, then slow tier, then all — is the right shape for the migration, but it does not tell you where the curve bottoms out. If you have run this at meaningful volume, where did you land?

來源:DEV Community · dev.to