import { ORIGIN } from "./config"; export class Unauthorized extends Error { constructor() { super("Sign in required"); } } export class HttpError extends Error { constructor(readonly status: number) { super(status === 503 ? "Server busy" : `Request failed (${status})`); } } type Transport = { token: () => string | null; onUnauthorized: (token: string) => void }; let transport: Transport = { token: () => null, onUnauthorized: () => {} }; export function setTransport(next: Transport) { transport = next; } export type Conditional = { etag: string | null; value: T }; /** A decoded JSON response body, before its contract parser checks it. */ export type JsonBody = Awaited>; /** Turns a JSON response body into a validated domain value (see agents-shared/contract). */ export type Parser = (body: JsonBody) => T; async function send(path: string, init: RequestInit & { timeout?: number } = {}) { const token = transport.token(); if (!token) throw new Unauthorized(); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), init.timeout ?? 20000); init.signal?.addEventListener("abort", () => controller.abort()); try { const response = await fetch(ORIGIN + path, { ...init, signal: controller.signal, headers: { Accept: "application/json", ...init.headers, Authorization: `Bearer ${token}` }, }); if (response.status === 401) { transport.onUnauthorized(token); throw new Unauthorized(); } return response; } finally { clearTimeout(timer); } } export async function getJSON(path: string, parse: Parser, signal?: AbortSignal): Promise { const response = await send(path, { signal }); if (!response.ok) throw new HttpError(response.status); return parse(await response.json()); } /** GET with If-None-Match; a 304 returns the previous value unchanged. */ export async function getConditional( path: string, parse: Parser, previous: Conditional | undefined, signal?: AbortSignal, ): Promise> { const response = await send(path, { signal, headers: previous?.etag ? { "If-None-Match": previous.etag } : {}, }); if (response.status === 304 && previous) return previous; if (!response.ok) throw new HttpError(response.status); return { etag: response.headers.get("etag"), value: parse(await response.json()) }; }