export const DEFAULT_API = "http://127.0.0.1:8000"; export const API_STORAGE_KEY = "stateHubApiBase"; const API_QUERY_PARAMS = ["api_base", "apiBase"]; function cleanApiBase(value) { if (typeof value !== "string") return null; const cleaned = value.trim().replace(/\/+$/, ""); return cleaned || null; } function getStorageApiBase(storage) { if (!storage?.getItem) return null; try { return cleanApiBase(storage.getItem(API_STORAGE_KEY)); } catch { return null; } } function urlFromLocation(location) { if (!location) return null; try { return new URL(location.href ?? String(location)); } catch { return null; } } function getQueryApiBase(url) { if (!url) return null; for (const name of API_QUERY_PARAMS) { const value = cleanApiBase(url.searchParams.get(name)); if (value) return value; } return null; } function inferApiBase(url) { if (!url || !["http:", "https:"].includes(url.protocol)) return DEFAULT_API; if (url.hostname === "::1" || url.hostname === "[::1]") return DEFAULT_API; const apiUrl = new URL(url.href); apiUrl.port = globalThis.STATE_HUB_API_PORT || "8000"; apiUrl.pathname = ""; apiUrl.search = ""; apiUrl.hash = ""; return apiUrl.origin; } export function resolveApiBase({ location = globalThis.location, storage = globalThis.localStorage, } = {}) { const url = urlFromLocation(location); return ( getQueryApiBase(url) || cleanApiBase(globalThis.STATE_HUB_API_BASE) || getStorageApiBase(storage) || inferApiBase(url) ); } export const API = resolveApiBase(); export const POLL = 15_000; export const POLL_HEAVY = 60_000; export const FETCH_TIMEOUT = 12_000; export function pollDelay({ok = true, base = POLL, failures = 0} = {}) { return ok ? base : Math.min(base * 2 ** Math.min(failures, 4), 300_000); } export function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Waits `ms` if the tab is visible; pauses until the tab becomes visible if hidden, // then returns immediately so the next poll fires as soon as the user returns. export async function waitForVisible(ms) { if (typeof document === "undefined") return sleep(ms); if (document.visibilityState === "visible") return sleep(ms); return new Promise(resolve => { const handler = () => { document.removeEventListener("visibilitychange", handler); resolve(); }; document.addEventListener("visibilitychange", handler); }); } export async function apiFetch(path, options = {}) { const url = path.startsWith("http") ? path : `${API}${path}`; const timeout = options.timeout ?? FETCH_TIMEOUT; const {timeout: _timeout, ...fetchOptions} = options; const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), timeout); try { return await fetch(url, {cache: "no-store", ...fetchOptions, signal: ctrl.signal}); } finally { clearTimeout(timer); } }