2026-10-14 09:40:12 -07:00
|
|
|
export class ApiError extends Error {
|
|
|
|
|
status;
|
|
|
|
|
constructor(status, message) {
|
|
|
|
|
super(message);
|
|
|
|
|
this.status = status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export async function request(method, 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}`;
|
|
|
|
|
}
|
|
|
|
|
const init = { method: method.toUpperCase() };
|
2026-10-14 20:07:26 -07:00
|
|
|
if (opts.body instanceof Blob) {
|
|
|
|
|
init.headers = { "Content-Type": opts.body.type || "application/octet-stream" };
|
|
|
|
|
init.body = opts.body;
|
|
|
|
|
}
|
|
|
|
|
else if (opts.body !== undefined) {
|
2026-10-14 09:40:12 -07:00
|
|
|
init.headers = { "Content-Type": "application/json" };
|
|
|
|
|
init.body = JSON.stringify(opts.body);
|
|
|
|
|
}
|
|
|
|
|
const res = await fetch(url, init);
|
2026-10-14 21:25:01 -07:00
|
|
|
if (res.status === 204) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
2026-10-14 23:02:38 -07:00
|
|
|
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}` : ""}`);
|
|
|
|
|
}
|
2026-10-14 09:40:12 -07:00
|
|
|
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);
|
2026-10-14 21:25:01 -07:00
|
|
|
export const del = (path, opts) => request("delete", path, opts);
|
2026-10-14 22:28:25 -07:00
|
|
|
const staleAfterMs = 40_000;
|
|
|
|
|
const retryAfterMs = 2_000;
|
2026-10-14 23:53:17 -07:00
|
|
|
export function subscribe(after, taskId, onEvent, onState) {
|
2026-10-14 22:28:25 -07:00
|
|
|
let last = after;
|
|
|
|
|
let heard = Date.now();
|
|
|
|
|
let source;
|
|
|
|
|
let retry = 0;
|
|
|
|
|
const connect = () => {
|
|
|
|
|
clearTimeout(retry);
|
|
|
|
|
source?.close();
|
|
|
|
|
heard = Date.now();
|
2026-10-14 23:53:17 -07:00
|
|
|
const query = new URLSearchParams({ after: String(last) });
|
|
|
|
|
if (taskId) {
|
|
|
|
|
query.set("task_id", taskId);
|
|
|
|
|
}
|
|
|
|
|
const s = new EventSource(`/api/events?${query}`);
|
2026-10-14 22:28:25 -07:00
|
|
|
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();
|
|
|
|
|
};
|
2026-10-14 23:53:17 -07:00
|
|
|
const onVisible = () => {
|
2026-10-14 22:28:25 -07:00
|
|
|
if (document.visibilityState === "visible") {
|
|
|
|
|
check();
|
|
|
|
|
}
|
2026-10-14 23:53:17 -07:00
|
|
|
};
|
|
|
|
|
const interval = setInterval(check, 5_000);
|
|
|
|
|
addEventListener("online", connect);
|
|
|
|
|
document.addEventListener("visibilitychange", onVisible);
|
2026-10-14 22:28:25 -07:00
|
|
|
connect();
|
2026-10-14 23:53:17 -07:00
|
|
|
return () => {
|
|
|
|
|
clearInterval(interval);
|
|
|
|
|
clearTimeout(retry);
|
|
|
|
|
removeEventListener("online", connect);
|
|
|
|
|
document.removeEventListener("visibilitychange", onVisible);
|
|
|
|
|
source?.close();
|
|
|
|
|
};
|
2026-10-14 09:40:12 -07:00
|
|
|
}
|
|
|
|
|
//# sourceMappingURL=api.js.map
|