205 lines
9.1 KiB
JavaScript
205 lines
9.1 KiB
JavaScript
const REFRESH_DEFAULTS = Object.freeze({
|
|
live_enabled: true, overview_seconds: 3, traffic_seconds: 5,
|
|
cache_seconds: 5, system_info_seconds: 60
|
|
});
|
|
const REFRESH_CHOICES = Object.freeze({
|
|
overview_seconds: [1, 2, 3, 5, 10], traffic_seconds: [0, 3, 5, 10, 30],
|
|
cache_seconds: [0, 3, 5, 10, 30], system_info_seconds: [0, 15, 30, 60, 300]
|
|
});
|
|
function normalizeRefreshPreferences(input = {}) {
|
|
const value = { ...REFRESH_DEFAULTS };
|
|
if (typeof input.live_enabled === "boolean") value.live_enabled = input.live_enabled;
|
|
for (const [key, allowed] of Object.entries(REFRESH_CHOICES)) {
|
|
if (allowed.includes(input[key])) value[key] = input[key];
|
|
}
|
|
return value;
|
|
}
|
|
|
|
const rustProxyRefresh = (() => {
|
|
const tasks = new Map();
|
|
let ready = false, options = { ...REFRESH_DEFAULTS }, panel = currentPanel;
|
|
let settingsTab = "listeners", watchingVisibility = false;
|
|
let sessionReady = true, visibleResumeHandled = false;
|
|
const visible = () => !document.hidden;
|
|
function allowed(task) {
|
|
return ready && authenticated && sessionReady && visible() && options.live_enabled &&
|
|
task.interval() > 0 && (!task.active || task.active());
|
|
}
|
|
function clear(task, abort = false) {
|
|
if (task.timer !== null) clearTimeout(task.timer);
|
|
task.timer = null;
|
|
if (abort) {
|
|
++task.generation;
|
|
task.controller?.abort();
|
|
}
|
|
}
|
|
function schedule(task, delay) {
|
|
if (task.timer !== null || task.inFlight || !allowed(task)) return;
|
|
task.timer = setTimeout(() => { task.timer = null; void execute(task, false); }, delay);
|
|
}
|
|
function execute(task, manual) {
|
|
if (!authenticated || !sessionReady || (!manual && !allowed(task))) return Promise.resolve();
|
|
if (task.inFlight) {
|
|
if (manual) task.queued = true;
|
|
return task.inFlight;
|
|
}
|
|
clear(task);
|
|
const controller = new AbortController(), generation = task.generation, epoch = authEpoch;
|
|
const panelAtStart = panel, tabAtStart = settingsTab;
|
|
task.controller = controller;
|
|
const isCurrent = () => authenticated && epoch === authEpoch && !controller.signal.aborted &&
|
|
generation === task.generation && (manual ? panel === panelAtStart && settingsTab === tabAtStart : allowed(task));
|
|
const promise = Promise.resolve().then(() => task.run({ signal: controller.signal, isCurrent }));
|
|
task.inFlight = promise.catch(error => {
|
|
if (error?.name !== "AbortError" && isCurrent()) {
|
|
if (task.onError) task.onError(error);
|
|
else console.warn(`Refresh ${task.id} failed:`, error);
|
|
}
|
|
}).finally(() => {
|
|
task.inFlight = null;
|
|
if (task.controller === controller) task.controller = null;
|
|
if (task.queued && authenticated) {
|
|
task.queued = false;
|
|
void execute(task, true);
|
|
} else {
|
|
task.queued = false;
|
|
schedule(task, task.interval() * 1000);
|
|
}
|
|
});
|
|
return task.inFlight;
|
|
}
|
|
function register(id, spec) {
|
|
if (tasks.has(id)) throw new Error(`Duplicate refresh task: ${id}`);
|
|
if (typeof spec.run !== "function" || typeof spec.interval !== "function") throw new Error(`Invalid refresh task: ${id}`);
|
|
const task = { ...spec, id, timer: null, inFlight: null, controller: null, generation: 0, queued: false, enabled: false, seconds: null };
|
|
tasks.set(id, task);
|
|
sync();
|
|
return () => { clear(task, true); tasks.delete(id); };
|
|
}
|
|
function sync() {
|
|
for (const task of tasks.values()) {
|
|
const shouldRun = allowed(task), seconds = task.interval();
|
|
if (!shouldRun) {
|
|
if (task.enabled) clear(task, true);
|
|
task.enabled = false;
|
|
} else if (!task.enabled || task.seconds !== seconds) {
|
|
clear(task);
|
|
task.enabled = true;
|
|
schedule(task, 0);
|
|
}
|
|
task.seconds = seconds;
|
|
}
|
|
if (ready && authenticated && sessionReady && visible() && options.live_enabled) startLive();
|
|
else stopLive();
|
|
}
|
|
function suspendForSessionValidation() {
|
|
sessionReady = false;
|
|
for (const task of tasks.values()) {
|
|
clear(task, true); task.queued = false; task.enabled = false;
|
|
}
|
|
sync();
|
|
}
|
|
function completeSessionValidation(success) {
|
|
sessionReady = !!success;
|
|
sync();
|
|
}
|
|
function requestSessionResume() {
|
|
if (!ready || !authenticated || !visible() || visibleResumeHandled) return;
|
|
visibleResumeHandled = true;
|
|
void revalidateSession();
|
|
}
|
|
function onVisibility() {
|
|
if (visible()) requestSessionResume();
|
|
else { visibleResumeHandled = false; sync(); }
|
|
}
|
|
function onPageHide() {
|
|
visibleResumeHandled = false;
|
|
suspendForSessionValidation();
|
|
}
|
|
function onPageShow(event) { if (event.persisted) requestSessionResume(); }
|
|
function start() {
|
|
ready = true; sessionReady = true; visibleResumeHandled = false; panel = currentPanel;
|
|
settingsTab = window.rustProxySettings?.current?.() || "listeners";
|
|
if (!watchingVisibility) {
|
|
document.addEventListener("visibilitychange", onVisibility);
|
|
window.addEventListener("pagehide", onPageHide);
|
|
window.addEventListener("pageshow", onPageShow);
|
|
watchingVisibility = true;
|
|
}
|
|
sync();
|
|
}
|
|
function stop() {
|
|
ready = false; sessionReady = false; visibleResumeHandled = false;
|
|
for (const task of tasks.values()) {
|
|
clear(task, true); task.queued = false; task.enabled = false;
|
|
}
|
|
if (watchingVisibility) {
|
|
document.removeEventListener("visibilitychange", onVisibility);
|
|
window.removeEventListener("pagehide", onPageHide);
|
|
window.removeEventListener("pageshow", onPageShow);
|
|
watchingVisibility = false;
|
|
}
|
|
stopLive();
|
|
}
|
|
function update(value) { options = normalizeRefreshPreferences(value); sync(); }
|
|
function setPanel(value) {
|
|
panel = value; settingsTab = window.rustProxySettings?.current?.() || settingsTab;
|
|
for (const task of tasks.values()) if (task.active && !task.active()) task.queued = false;
|
|
sync();
|
|
}
|
|
function setSettingsTab(value) {
|
|
settingsTab = value;
|
|
for (const task of tasks.values()) if (task.active && !task.active()) task.queued = false;
|
|
sync();
|
|
}
|
|
function manual(id) {
|
|
const task = tasks.get(id);
|
|
if (!task) return Promise.reject(new Error(`Unknown refresh task: ${id}`));
|
|
return execute(task, true);
|
|
}
|
|
register("status-full", {
|
|
interval: () => 0, run: async ({ signal, isCurrent }) => {
|
|
const value = await api("/api/status", { signal });
|
|
if (isCurrent()) renderStatus(value, true);
|
|
}
|
|
});
|
|
register("status-fallback", {
|
|
interval: () => options.overview_seconds, active: () => !liveConnected && (liveFallback || typeof EventSource === "undefined"), run: async ({ signal, isCurrent }) => {
|
|
const value = await api("/api/status?view=live", { signal });
|
|
if (isCurrent()) renderLiveStatus(value);
|
|
}, onError: error => { if (error?.status !== 401) $("connection-label").textContent = t("disconnected"); }
|
|
});
|
|
register("logs-fallback", {
|
|
interval: () => options.overview_seconds, active: () => !liveConnected && (liveFallback || typeof EventSource === "undefined"), run: async ({ signal, isCurrent }) => {
|
|
const value = await api("/api/logs", { signal });
|
|
if (isCurrent()) renderLogs(value);
|
|
}
|
|
});
|
|
register("system-info", {
|
|
interval: () => options.system_info_seconds,
|
|
active: () => panel === "overview" || (panel === "settings" && ["system-info", "cache"].includes(settingsTab)),
|
|
run: async ({ signal, isCurrent }) => {
|
|
const value = await api("/api/system-info", { signal });
|
|
if (!isCurrent()) return;
|
|
latestStatus = { ...(latestStatus || {}), ...value };
|
|
if (latestStatus.metrics) renderDiagnostics(latestStatus, false, true);
|
|
window.rustProxySettings?.render();
|
|
}
|
|
});
|
|
register("tunnels", {
|
|
interval: () => 2, active: () => !!$("tunnels-dialog")?.open,
|
|
run: async context => { await loadTunnels(context); }
|
|
});
|
|
return {
|
|
register, manual, start, stop, sync, update, setPanel, setSettingsTab,
|
|
suspendForSessionValidation, completeSessionValidation,
|
|
current: () => ({ ...options }), diagnostics: () => [...tasks.values()].map(t => ({ id: t.id, enabled: t.enabled, inFlight: !!t.inFlight, scheduled: t.timer !== null }))
|
|
};
|
|
})();
|
|
window.rustProxyRefresh = rustProxyRefresh;
|
|
|
|
$("overview-refresh")?.addEventListener("click", () => {
|
|
void Promise.all([rustProxyRefresh.manual("status-full"), rustProxyRefresh.manual("logs-fallback"), rustProxyRefresh.manual("system-info")]);
|
|
});
|
|
$("system-info-refresh")?.addEventListener("click", () => void rustProxyRefresh.manual("system-info"));
|