SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
3.2 KB · 84 lines typescript
Raw Blame History
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