"use client"; import useSWR, { type SWRConfiguration, mutate } from "swr"; export class ClientApiError extends Error { constructor( public status: number, public code: string, message: string, public details?: unknown, public requestId?: string, ) { super(message); } } export async function api(input: string, init: RequestInit & { json?: unknown } = {}): Promise { const { json, headers, ...rest } = init; const res = await fetch(input, { credentials: "same-origin", ...rest, headers: { ...(json !== undefined ? { "Content-Type": "application/json" } : {}), ...(headers ?? {}) }, body: json !== undefined ? JSON.stringify(json) : rest.body, }); const ct = res.headers.get("content-type") ?? ""; if (!res.ok) { let body: { error?: { code?: string; message?: string; detail?: string; details?: unknown; requestId?: string } } = {}; if (ct.includes("json")) body = await res.json().catch(() => ({})); const err = body.error; throw new ClientApiError(res.status, err?.code ?? "HTTP_ERROR", err?.message ?? `Request failed (${res.status})`, err?.details ?? err?.detail, err?.requestId); } if (ct.includes("json")) return (await res.json()) as T; return (await res.text()) as unknown as T; } export const fetcher = (url: string) => api(url); export function useApi(key: string | null, config?: SWRConfiguration) { return useSWR(key, fetcher, { revalidateOnFocus: false, ...config }); } export function revalidate(keyPrefix: string) { return mutate((key) => typeof key === "string" && key.startsWith(keyPrefix), undefined, { revalidate: true }); } /** * Consume a `text/event-stream` POST response as parsed JSON events. * Returns an abort function; resolves when the stream ends. */ export async function streamEvents(url: string, body: unknown, onEvent: (ev: Record & { type: string }) => void, signal?: AbortSignal): Promise { const res = await fetch(url, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", Accept: "text/event-stream" }, body: JSON.stringify(body), signal }); if (!res.ok || !res.body) { let err: { error?: { code?: string; message?: string; details?: unknown; requestId?: string } } = {}; try { err = await res.json(); } catch { /* ignore */ } throw new ClientApiError(res.status, err.error?.code ?? "HTTP_ERROR", err.error?.message ?? `Request failed (${res.status})`, err.error?.details, err.error?.requestId); } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); let idx: number; while ((idx = buffer.indexOf("\n\n")) >= 0) { const frame = buffer.slice(0, idx); buffer = buffer.slice(idx + 2); for (const line of frame.split("\n")) { if (!line.startsWith("data:")) continue; const payload = line.slice(5).trim(); if (!payload) continue; try { onEvent(JSON.parse(payload)); } catch { /* malformed frame */ } } } } }