/** * `fx.fetch` body — lazy chunk so Store-only / edge graphs that never call * outbound HTTP do not pay for host parsing + dry-run stub strings. */ import type { EffectExternal } from "./effects.ts"; import { isDryRun, recordWouldHaveFired } from "./dry-run.ts"; import { requestSignal } from "./abort-scope.ts"; /** * Hostname for `fx.fetch` capability / ledger resource. * * @param href - Absolute URL string */ export function hostFromFetchUrl(href: string): string { try { const host = new URL(href).hostname; if (host) return host; } catch { /* fall through */ } throw new Error(`fx.fetch: invalid URL "${href}" — expected an absolute URL with a hostname`); } type Gated = ( kind: "fetch", resource: string, body: () => T | Promise, externalOf?: | EffectExternal | ((result: T | undefined, error: unknown) => EffectExternal | undefined), ) => Promise; /** * Capability-gated outbound HTTP (always `external.kind: "third-party"`). * * @param gated - Flow `gated` helper * @param url - Absolute URL * @param init - Fetch init */ export function runFxFetch(gated: Gated, url: string | URL, init?: RequestInit): Promise { const href = typeof url === "string" ? url : url.href; const host = hostFromFetchUrl(href); return gated( "fetch", host, async () => { if (isDryRun()) { recordWouldHaveFired("fetch", host); return new Response(null, { status: 204 }); } return globalThis.fetch(url, { ...init, signal: requestSignal(init?.signal), }); }, { host, kind: "third-party" }, ); }