generated from coulomb/repo-seed
Adds preferred workplan REST/event surfaces, legacy-meter telemetry and weekly review summaries, documentation/dashboard terminology updates, dashboard API loading fixes, and close-out sync for STATE-WP-0052 and STATE-WP-0054.
101 lines
2.9 KiB
JavaScript
101 lines
2.9 KiB
JavaScript
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);
|
|
}
|
|
}
|