v0.31.0
This commit is contained in:
1 parent
f213d853e2
commit
76b8bb7ee2
78 files changed
+7750
-5204
No files matched your search
@@ -0,0 +1,161 @@
|
||||
// Single scheduler for all authenticated dashboard polling. SSE is controlled here
|
||||
// but remains one native EventSource with its own browser reconnect behavior.
|
||||
const REFRESH_DEFAULTS = Object.freeze({
|
||||
live_enabled: true, overview_seconds: 2, traffic_seconds: 5,
|
||||
cache_seconds: 5, system_info_seconds: 60
|
||||
});
|
||||
const REFRESH_CHOICES = Object.freeze({
|
||||
overview_seconds: [1, 2, 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;
|
||||
const visible = () => !document.hidden;
|
||||
function allowed(task) {
|
||||
return ready && authenticated && 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 || (!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;
|
||||
// A manual request remains valid with Live switched off, but not in a stale view.
|
||||
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()) {
|
||||
// A failure is isolated to this source; other timers continue.
|
||||
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 && visible() && options.live_enabled) startLive();
|
||||
else stopLive();
|
||||
}
|
||||
function onVisibility() { sync(); if (visible() && ready && authenticated) void revalidateSession(); }
|
||||
function start() {
|
||||
ready = true; panel = currentPanel;
|
||||
settingsTab = window.rustProxySettings?.current?.() || "listeners";
|
||||
if (!watchingVisibility) { document.addEventListener("visibilitychange", onVisibility); watchingVisibility = true; }
|
||||
sync();
|
||||
}
|
||||
function stop() {
|
||||
ready = false;
|
||||
for (const task of tasks.values()) {
|
||||
clear(task, true); task.queued = false; task.enabled = false;
|
||||
}
|
||||
if (watchingVisibility) { document.removeEventListener("visibilitychange", onVisibility); 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,
|
||||
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"));
|
||||
Reference in new issue
Block a user