export class ApiError extends Error { status; constructor(status, message) { super(message); this.status = status; } } function target(path, opts) { const pathParams = (opts.path ?? {}); let url = path.replace(/\{(\w+)\}/g, (_, key) => { const value = pathParams[key]; if (value === undefined) { throw new Error(`missing path parameter ${key} for ${path}`); } return encodeURIComponent(String(value)); }); const query = new URLSearchParams(); for (const [key, value] of Object.entries((opts.query ?? {}))) { if (value !== undefined) { query.set(key, String(value)); } } if (query.size > 0) { url += `?${query}`; } return url; } export async function request(method, path, opts = {}) { const url = target(path, opts); const init = { method: method.toUpperCase(), keepalive: opts.keepalive ?? false }; if (opts.body instanceof Blob) { init.headers = { "Content-Type": opts.body.type || "application/octet-stream" }; init.body = opts.body; } else if (opts.body !== undefined) { init.headers = { "Content-Type": "application/json" }; init.body = JSON.stringify(opts.body); } const res = await fetch(url, init); if (res.status === 204) { return undefined; } if (!res.headers.get("Content-Type")?.startsWith("application/json")) { const text = (await res.text()).trim(); throw new ApiError(res.status, `${res.status} ${res.statusText}${text ? `: ${text}` : ""}`); } const data = await res.json(); if (!res.ok) { throw new ApiError(res.status, data.message); } return data; } export const get = (path, opts) => request("get", path, opts); export const post = (path, opts) => request("post", path, opts); export const put = (path, opts) => request("put", path, opts); export const del = (path, opts) => request("delete", path, opts); export function socket(path, opts) { const ws = new WebSocket(`${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}${target(path, opts)}`); ws.binaryType = "arraybuffer"; return ws; } const staleAfterMs = 40_000; const retryAfterMs = 2_000; export function subscribe(after, filter, onEvent, onState) { let last = after; let heard = Date.now(); let source; let retry = 0; const connect = () => { clearTimeout(retry); source?.close(); heard = Date.now(); const query = new URLSearchParams({ after: String(last) }); if (filter.task_id) { query.set("task_id", filter.task_id); } if (filter.lead) { query.set("lead", "true"); } const s = new EventSource(`/api/events?${query}`); source = s; s.onopen = () => { heard = Date.now(); onState(true); }; s.onerror = () => { onState(false); if (s.readyState === EventSource.CLOSED) { retry = setTimeout(connect, retryAfterMs); } }; s.addEventListener("ping", () => { heard = Date.now(); }); s.onmessage = (msg) => { heard = Date.now(); const ev = JSON.parse(msg.data); if (ev.seq > last) { last = ev.seq; } onEvent(ev); }; }; const check = () => { if (Date.now() - heard < staleAfterMs) { return; } onState(false); connect(); }; const onVisible = () => { if (document.visibilityState === "visible") { check(); } }; const interval = setInterval(check, 5_000); addEventListener("online", connect); document.addEventListener("visibilitychange", onVisible); connect(); return () => { clearInterval(interval); clearTimeout(retry); removeEventListener("online", connect); document.removeEventListener("visibilitychange", onVisible); source?.close(); }; } //# sourceMappingURL=api.js.map