// Fetch wrapper + live event stream. export const clientId = Math.random().toString(36).slice(2); export class ApiError extends Error { constructor(status, message) { super(message); this.status = status; } } async function request(method, url, body, opts = {}) { const headers = { 'X-Client-Id': clientId }; let payload = body; if (body !== undefined && !(body instanceof FormData) && typeof body !== 'string') { headers['Content-Type'] = 'application/json'; payload = JSON.stringify(body); } else if (typeof body === 'string') headers['Content-Type'] = opts.contentType || 'text/plain'; const res = await fetch(url, { method, headers, body: payload, credentials: 'same-origin' }); if (res.status === 401 && !url.startsWith('/api/auth')) { window.dispatchEvent(new CustomEvent('wb:signed-out')); } const ct = res.headers.get('content-type') || ''; const data = ct.includes('json') ? await res.json() : await res.text(); if (!res.ok) throw new ApiError(res.status, data?.error || res.statusText); return data; } export const api = { get: (u) => request('GET', u), post: (u, b = {}, o) => request('POST', u, b, o), patch: (u, b = {}) => request('PATCH', u, b), del: (u) => request('DELETE', u), }; let es = null; const listeners = new Set(); export function onEvent(fn) { listeners.add(fn); return () => listeners.delete(fn); } export function connectEvents() { if (es) return; es = new EventSource(`/api/events?client=${clientId}`); es.onmessage = (m) => { let ev; try { ev = JSON.parse(m.data); } catch { return; } for (const fn of listeners) fn(ev); }; es.onopen = () => { for (const fn of listeners) fn({ type: 'reconnected' }); }; } export function disconnectEvents() { es?.close(); es = null; }