Something went wrong. Try again.
Where attention appears to go across agent threads: collector, web explorer, and native iOS client
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172import { 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<T> = { etag: string | null; value: T };
/** A decoded JSON response body, before its contract parser checks it. */export type JsonBody = Awaited<ReturnType<Response["json"]>>;/** Turns a JSON response body into a validated domain value (see agents-shared/contract). */export type Parser<T> = (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<T>(path: string, parse: Parser<T>, signal?: AbortSignal): Promise<T> { 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<T>( path: string, parse: Parser<T>, previous: Conditional<T> | undefined, signal?: AbortSignal,): Promise<Conditional<T>> { 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()) };}