- Optimize get_ui_state backend by reading /proc natively instead of spawning pidof - Avoid spawning readlink and redundant ubus queries in state.uc - Cache positive tachyon_runtime_network_configured checks for 5s - Back off idle UI state polling interval from 3s to 10s (60s when tab is hidden) - Back off TachyonLogWatcher interval to 30s - Pause Dashboard and Monitoring polling when browser tab is inactive (Page Visibility API) - Fix cleanTabId to ensure inactive tab pollers and timers unmount properly
174 lines
4.7 KiB
TypeScript
174 lines
4.7 KiB
TypeScript
import { TachyonShellMethods } from '../methods';
|
|
import { Tachyon } from '../types';
|
|
import { logger } from './logger.service';
|
|
import { applyUiStateToStore } from './uiState.service';
|
|
|
|
const RUNTIME_UI_STATE_REFRESH_MIN_INTERVAL_MS = 500;
|
|
const RUNTIME_UI_STATE_IDLE_POLL_INTERVAL_MS = 10_000;
|
|
const RUNTIME_UI_STATE_ACTIVE_POLL_INTERVAL_MS = 500;
|
|
// A hidden tab must not keep spawning shell commands on the router:
|
|
// back off to one request per 60s while hidden (a force refresh on
|
|
// visibilitychange covers the moment the tab comes back).
|
|
const RUNTIME_UI_STATE_HIDDEN_POLL_INTERVAL_MS = 60_000;
|
|
type RuntimeUiStateListener = (uiState: Tachyon.UiState) => void;
|
|
|
|
let runtimeUiStateRefreshPromise: Promise<Tachyon.UiState | undefined> | null =
|
|
null;
|
|
let lastRuntimeUiStateRefreshAt = 0;
|
|
let lastRuntimeUiState: Tachyon.UiState | undefined;
|
|
let runtimeStateResumeRefreshRegistered = false;
|
|
let runtimeStatePollTimer: ReturnType<typeof setTimeout> | null = null;
|
|
let runtimeStatePollingStarted = false;
|
|
let runtimeStateHasRunningAction = false;
|
|
const runtimeUiStateListeners = new Set<RuntimeUiStateListener>();
|
|
|
|
function isDocumentVisible() {
|
|
return (
|
|
typeof document === 'undefined' ||
|
|
!document.visibilityState ||
|
|
document.visibilityState === 'visible'
|
|
);
|
|
}
|
|
|
|
function hasRunningAction(uiState: Tachyon.UiState) {
|
|
return Object.values(uiState.actions).some((actions) =>
|
|
actions.some((action) => action.running),
|
|
);
|
|
}
|
|
|
|
function getNextPollDelay() {
|
|
if (!isDocumentVisible()) {
|
|
return RUNTIME_UI_STATE_HIDDEN_POLL_INTERVAL_MS;
|
|
}
|
|
return runtimeStateHasRunningAction
|
|
? RUNTIME_UI_STATE_ACTIVE_POLL_INTERVAL_MS
|
|
: RUNTIME_UI_STATE_IDLE_POLL_INTERVAL_MS;
|
|
}
|
|
|
|
function scheduleRuntimeUiStatePoll(delay = getNextPollDelay()) {
|
|
if (
|
|
!runtimeStatePollingStarted ||
|
|
runtimeStatePollTimer ||
|
|
typeof window === 'undefined'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
runtimeStatePollTimer = window.setTimeout(() => {
|
|
runtimeStatePollTimer = null;
|
|
void refreshRuntimeUiState()
|
|
.catch(() => undefined)
|
|
.finally(() => {
|
|
scheduleRuntimeUiStatePoll();
|
|
});
|
|
}, delay);
|
|
}
|
|
|
|
function notifyRuntimeUiStateListeners(uiState: Tachyon.UiState) {
|
|
for (const listener of runtimeUiStateListeners) {
|
|
try {
|
|
listener(uiState);
|
|
} catch (error) {
|
|
logger.error('[RUNTIME_UI_STATE]', 'listener failed', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function refreshRuntimeUiState({
|
|
force = false,
|
|
}: { force?: boolean } = {}): Promise<Tachyon.UiState | undefined> {
|
|
if (!isDocumentVisible()) {
|
|
return undefined;
|
|
}
|
|
|
|
if (runtimeUiStateRefreshPromise) {
|
|
return runtimeUiStateRefreshPromise;
|
|
}
|
|
|
|
const now = Date.now();
|
|
if (
|
|
!force &&
|
|
now - lastRuntimeUiStateRefreshAt < RUNTIME_UI_STATE_REFRESH_MIN_INTERVAL_MS
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
lastRuntimeUiStateRefreshAt = now;
|
|
|
|
const promise = TachyonShellMethods.getUiState()
|
|
.then((response) => {
|
|
if (!response.success) {
|
|
return undefined;
|
|
}
|
|
|
|
applyUiStateToStore(response.data);
|
|
lastRuntimeUiState = response.data;
|
|
runtimeStateHasRunningAction = hasRunningAction(response.data);
|
|
notifyRuntimeUiStateListeners(response.data);
|
|
return response.data;
|
|
})
|
|
.catch((error) => {
|
|
logger.error('[RUNTIME_UI_STATE]', 'refresh failed', error);
|
|
return undefined;
|
|
})
|
|
.finally(() => {
|
|
if (runtimeUiStateRefreshPromise === promise) {
|
|
runtimeUiStateRefreshPromise = null;
|
|
}
|
|
});
|
|
|
|
runtimeUiStateRefreshPromise = promise;
|
|
return runtimeUiStateRefreshPromise;
|
|
}
|
|
|
|
export function subscribeRuntimeUiState(listener: RuntimeUiStateListener) {
|
|
runtimeUiStateListeners.add(listener);
|
|
|
|
if (lastRuntimeUiState) {
|
|
try {
|
|
listener(lastRuntimeUiState);
|
|
} catch (error) {
|
|
logger.error('[RUNTIME_UI_STATE]', 'listener failed', error);
|
|
}
|
|
}
|
|
|
|
return () => {
|
|
runtimeUiStateListeners.delete(listener);
|
|
};
|
|
}
|
|
|
|
export function getCachedRuntimeUiState() {
|
|
return lastRuntimeUiState;
|
|
}
|
|
|
|
export function registerRuntimeStateResumeRefresh() {
|
|
if (runtimeStateResumeRefreshRegistered || typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
runtimeStateResumeRefreshRegistered = true;
|
|
|
|
const refreshOnResume = () => {
|
|
if (!isDocumentVisible()) {
|
|
return;
|
|
}
|
|
|
|
void refreshRuntimeUiState({ force: true });
|
|
};
|
|
|
|
document.addEventListener('visibilitychange', refreshOnResume);
|
|
window.addEventListener('pageshow', refreshOnResume);
|
|
window.addEventListener('focus', refreshOnResume);
|
|
}
|
|
|
|
export function startRuntimeUiStatePolling() {
|
|
if (runtimeStatePollingStarted || typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
runtimeStatePollingStarted = true;
|
|
void refreshRuntimeUiState({ force: true }).finally(() => {
|
|
scheduleRuntimeUiStatePoll();
|
|
});
|
|
}
|