Files
byre/web/dist/api.js
T

132 lines
4.2 KiB
JavaScript
Raw Normal View History

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 };
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) {
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, onReady) {
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.addEventListener("ready", () => {
heard = Date.now();
onReady();
});
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