/** 与 /database/api/* 的约定一致;401 时清空会话(回到登录视图)。 */ import { me } from "./stores.js"; export class ApiError extends Error { constructor( readonly status: number, readonly code: string, message: string, readonly details?: Record, ) { super(message); this.name = "ApiError"; } } export class UnauthenticatedError extends Error { constructor() { super("unauthenticated"); this.name = "UnauthenticatedError"; } } interface RequestOpts { readonly method?: string; readonly body?: unknown; } export async function api(path: string, opts: RequestOpts = {}): Promise { const res = await fetch(path, { credentials: "same-origin", method: opts.method ?? "GET", ...(opts.body !== undefined ? { headers: { "Content-Type": "application/json" }, body: JSON.stringify(opts.body) } : {}), }); if (res.status === 401) { me.set(null); throw new UnauthenticatedError(); } if (res.status === 204) return null as T; const text = await res.text(); const data = text === "" ? null : (JSON.parse(text) as unknown); if (!res.ok) { const err = (data as { error?: { code?: string; message?: string } } | null)?.error ?? {}; throw new ApiError(res.status, err.code ?? "unknown", err.message ?? res.statusText, err as Record); } return data as T; }