BT  ·  2026-10-10

index.ts

 1import { isAuthorized, unauthorized } from "./auth";
 2import { errorPage, historyPage, homePage, html } from "./html";
 3import { MonitorDO, REGIONS, endpoints, type Check, type Region, type RegionSnapshot } from "./monitor";
 4
 5export { MonitorDO };
 6
 7const PAGE_SIZE = 50;
 8const RPC_TIMEOUT_MS = 5000;
 9
10export default {
11  async fetch(req, env): Promise<Response> {
12    if (!(await isAuthorized(req, env))) return unauthorized();
13    const url = new URL(req.url);
14    if (req.method !== "GET") return new Response("Method not allowed", { status: 405 });
15    if (url.pathname === "/") return home(env);
16    if (url.pathname === "/endpoint") return history(env, url.searchParams);
17    return errorPage(404, "Not found.");
18  },
19
20  // Watchdog: creates each regional monitor on first run and restarts any stopped alarm loop.
21  async scheduled(_controller, env): Promise<void> {
22    const results = await Promise.allSettled(REGIONS.map((r) => monitor(env, r).ensureAlarm(r)));
23    results.forEach((r, i) => {
24      if (r.status === "rejected") console.error("ensureAlarm failed", { region: REGIONS[i], error: String(r.reason) });
25    });
26  },
27} satisfies ExportedHandler<Env>;
28
29export function monitor(env: Env, region: Region): DurableObjectStub<MonitorDO> {
30  const ns = env.MONITOR as DurableObjectNamespace<MonitorDO>;
31  return ns.get(ns.idFromName(region), { locationHint: region });
32}
33
34/** Calls every regional monitor in parallel; a region that errors or is slow yields null. */
35async function fanOut<T>(env: Env, call: (stub: DurableObjectStub<MonitorDO>, region: Region) => Promise<T>): Promise<(T | null)[]> {
36  return Promise.all(
37    REGIONS.map(async (region) => {
38      try {
39        return await Promise.race([
40          call(monitor(env, region), region),
41          new Promise<never>((_, reject) => setTimeout(() => reject(new Error("timeout")), RPC_TIMEOUT_MS)),
42        ]);
43      } catch (e) {
44        console.error("region RPC failed", { region, error: String(e) });
45        return null;
46      }
47    }),
48  );
49}
50
51async function home(env: Env): Promise<Response> {
52  const snapshots = await fanOut<RegionSnapshot>(env, (stub, region) => stub.latest(region));
53  return html(homePage(env, endpoints(env), snapshots, Date.now()));
54}
55
56async function history(env: Env, params: URLSearchParams): Promise<Response> {
57  const endpoint = params.get("url") ?? "";
58  if (!endpoints(env).includes(endpoint)) return errorPage(404, "Unknown endpoint.");
59  const regionParam = params.get("region") ?? "all";
60  const region = (REGIONS as readonly string[]).includes(regionParam) ? (regionParam as Region) : "all";
61  const failuresOnly = params.get("failures") === "1";
62  const beforeParam = Number(params.get("before"));
63  const before = Number.isFinite(beforeParam) && beforeParam > 0 ? beforeParam : null;
64
65  const results = await fanOut<Check[]>(env, (stub, r) =>
66    region === "all" || region === r
67      ? stub.history(endpoint, before ?? Number.MAX_SAFE_INTEGER, PAGE_SIZE + 1, failuresOnly)
68      : Promise.resolve([]),
69  );
70  const { checks, nextBefore } = mergePage(results.map((r) => r ?? []), PAGE_SIZE);
71  const unreachable = REGIONS.filter((_, i) => results[i] === null);
72  return html(historyPage({ endpoint, region, failuresOnly, before, checks, nextBefore, unreachable }));
73}
74
75/**
76 * Merges per-region pages (each newest first, each fetched with limit > pageSize) into one page.
77 * Rows sharing the cut-off timestamp stay together so the `ts < before` cursor never skips any.
78 */
79export function mergePage(lists: Check[][], pageSize: number): { checks: Check[]; nextBefore: number | null } {
80  const all = lists.flat().sort((a, b) => b.ts - a.ts);
81  if (all.length <= pageSize) return { checks: all, nextBefore: null };
82  let end = pageSize;
83  while (end < all.length && all[end].ts === all[end - 1].ts) end++;
84  return { checks: all.slice(0, end), nextBefore: all[end - 1].ts };
85}