g1t/apps/status/src/probe.ts

94 lines3,318 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1/**
2 * Running a part's check: each request it makes, all at once, with a short
3 * timeout. The answer's body is never read.
4 */
5import type { Check, Step } from "./components.ts";
6
7/** A check's raw outcome. */
8export type ProbeResult = {
9 ok: boolean;
10 /** How long it took, in milliseconds: the slowest of its requests. */
11 ms: number;
12 /** Why it failed, in a few words: "timed out", "HTTP 502". */
13 error?: string;
14};
15
16/** No request waits longer than this. */
17export const TIMEOUT_MS = 5000;
18
19export const USER_AGENT = "g1t-status (+https://status.g1t.sh)";
20
21type Fetch = (url: string, init: RequestInit) => Promise<Response>;
22
23class Timeout extends Error {}
24
25/** Runs `work`, timing it, and failing it after `timeoutMs`. */
26export async function timed(
27 work: (signal: AbortSignal) => Promise<true | string>,
28 timeoutMs = TIMEOUT_MS,
29 now: () => number = Date.now,
30): Promise<ProbeResult> {
31 const started = now();
32 const controller = new AbortController();
33 let timer: ReturnType<typeof setTimeout> | undefined;
34 const deadline = new Promise<never>((_, reject) => {
35 timer = setTimeout(() => {
36 controller.abort();
37 reject(new Timeout());
38 }, timeoutMs);
39 });
40 try {
41 const outcome = await Promise.race([work(controller.signal), deadline]);
42 const ms = now() - started;
43 return outcome === true ? { ok: true, ms } : { ok: false, ms, error: outcome };
44 } catch (error) {
45 const ms = now() - started;
46 return { ok: false, ms, error: error instanceof Timeout ? "timed out" : "could not connect" };
47 } finally {
48 clearTimeout(timer);
49 }
50}
51
52/** One request, answered with the status that means it works. */
53export function step(fetcher: Fetch, { url, headers = {}, expect }: Step, timeoutMs = TIMEOUT_MS): Promise<ProbeResult> {
54 return timed(async (signal) => {
55 const response = await fetcher(url, {
56 signal,
57 redirect: "manual",
58 headers: { "user-agent": USER_AGENT, "cache-control": "no-cache", ...headers },
59 });
60 await response.body?.cancel().catch(() => undefined);
61 const good = expect == null ? response.ok : response.status === expect;
62 return good || `HTTP ${response.status}`;
63 }, timeoutMs);
64}
65
66/** A check's requests together: it works when every one does, and takes as long as the slowest. */
67export function combine(results: ProbeResult[]): ProbeResult {
68 const ms = Math.max(0, ...results.map((r) => r.ms));
69 const failed = results.find((r) => !r.ok);
70 return failed ? { ok: false, ms, error: failed.error ?? "no answer" } : { ok: true, ms };
71}
72
73/** What runs a check. `billing` is null when there is no binding to it. */
74export type Probers = {
75 fetch: Fetch;
76 billing: (() => Promise<unknown>) | null;
77 timeoutMs?: number;
78};
79
80/** Runs one part's check. Null for a part with no check. */
81export async function runCheck(check: Check, probers: Probers): Promise<ProbeResult | null> {
82 const timeoutMs = probers.timeoutMs ?? TIMEOUT_MS;
83 switch (check.kind) {
84 case "http":
85 return combine(await Promise.all(check.steps.map((s) => step(probers.fetch, s, timeoutMs))));
86 case "billing": {
87 const billing = probers.billing;
88 if (!billing) return null;
89 return timed(async () => ((await billing()) ? true : "no price book"), timeoutMs);
90 }
91 case "none":
92 return null;
93 }
94}