TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import useSWR, { type SWRConfiguration, mutate } from "swr";34export class ClientApiError extends Error {5 constructor(6 public status: number,7 public code: string,8 message: string,9 public details?: unknown,10 public requestId?: string,11 ) {12 super(message);13 }14}1516export async function api<T = unknown>(input: string, init: RequestInit & { json?: unknown } = {}): Promise<T> {17 const { json, headers, ...rest } = init;18 const res = await fetch(input, {19 credentials: "same-origin",20 ...rest,21 headers: { ...(json !== undefined ? { "Content-Type": "application/json" } : {}), ...(headers ?? {}) },22 body: json !== undefined ? JSON.stringify(json) : rest.body,23 });24 const ct = res.headers.get("content-type") ?? "";25 if (!res.ok) {26 let body: { error?: { code?: string; message?: string; detail?: string; details?: unknown; requestId?: string } } = {};27 if (ct.includes("json")) body = await res.json().catch(() => ({}));28 const err = body.error;29 throw new ClientApiError(res.status, err?.code ?? "HTTP_ERROR", err?.message ?? `Request failed (${res.status})`, err?.details ?? err?.detail, err?.requestId);30 }31 if (ct.includes("json")) return (await res.json()) as T;32 return (await res.text()) as unknown as T;33}3435export const fetcher = <T,>(url: string) => api<T>(url);3637export function useApi<T>(key: string | null, config?: SWRConfiguration<T>) {38 return useSWR<T>(key, fetcher<T>, { revalidateOnFocus: false, ...config });39}4041export function revalidate(keyPrefix: string) {42 return mutate((key) => typeof key === "string" && key.startsWith(keyPrefix), undefined, { revalidate: true });43}4445/**46 * Consume a `text/event-stream` POST response as parsed JSON events.47 * Returns an abort function; resolves when the stream ends.48 */49export async function streamEvents(url: string, body: unknown, onEvent: (ev: Record<string, unknown> & { type: string }) => void, signal?: AbortSignal): Promise<void> {50 const res = await fetch(url, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", Accept: "text/event-stream" }, body: JSON.stringify(body), signal });51 if (!res.ok || !res.body) {52 let err: { error?: { code?: string; message?: string; details?: unknown; requestId?: string } } = {};53 try {54 err = await res.json();55 } catch {56 /* ignore */57 }58 throw new ClientApiError(res.status, err.error?.code ?? "HTTP_ERROR", err.error?.message ?? `Request failed (${res.status})`, err.error?.details, err.error?.requestId);59 }60 const reader = res.body.getReader();61 const decoder = new TextDecoder();62 let buffer = "";63 while (true) {64 const { value, done } = await reader.read();65 if (done) break;66 buffer += decoder.decode(value, { stream: true });67 let idx: number;68 while ((idx = buffer.indexOf("\n\n")) >= 0) {69 const frame = buffer.slice(0, idx);70 buffer = buffer.slice(idx + 2);71 for (const line of frame.split("\n")) {72 if (!line.startsWith("data:")) continue;73 const payload = line.slice(5).trim();74 if (!payload) continue;75 try {76 onEvent(JSON.parse(payload));77 } catch {78 /* malformed frame */79 }80 }81 }82 }83}84