feat(web-ui): pause dashboard polling, sockets, and chart updates when the tab is hidden

This commit is contained in:
2026-03-27 18:46:11 +00:00
parent 29d6076355
commit 6c4adf70c7
13 changed files with 367 additions and 292 deletions

View File

@@ -1186,18 +1186,33 @@ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
let socketClient: plugins.typedsocket.TypedSocket | null = null;
const socketRouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
// Batched log entry handler — buffers incoming entries and flushes once per animation frame
let logEntryBuffer: interfaces.data.ILogEntry[] = [];
let logFlushScheduled = false;
function flushLogEntries() {
logFlushScheduled = false;
if (logEntryBuffer.length === 0) return;
const current = logStatePart.getState()!;
const updated = [...current.recentLogs, ...logEntryBuffer];
logEntryBuffer = [];
// Cap at 2000 entries
if (updated.length > 2000) {
updated.splice(0, updated.length - 2000);
}
logStatePart.setState({ ...current, recentLogs: updated } as ILogState);
}
// Register handler for pushed log entries from the server
socketRouter.addTypedHandler(
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushLogEntry>(
'pushLogEntry',
async (dataArg) => {
const current = logStatePart.getState()!;
const updated = [...current.recentLogs, dataArg.entry];
// Cap at 2000 entries
if (updated.length > 2000) {
updated.splice(0, updated.length - 2000);
logEntryBuffer.push(dataArg.entry);
if (!logFlushScheduled) {
logFlushScheduled = true;
requestAnimationFrame(flushLogEntries);
}
logStatePart.setState({ ...current, recentLogs: updated } as ILogState);
return {};
}
)
@@ -1228,8 +1243,21 @@ async function disconnectSocket() {
}
}
// In-flight guard to prevent concurrent refresh requests
let isRefreshing = false;
// Combined refresh action for efficient polling
async function dispatchCombinedRefreshAction() {
if (isRefreshing) return;
isRefreshing = true;
try {
await dispatchCombinedRefreshActionInner();
} finally {
isRefreshing = false;
}
}
async function dispatchCombinedRefreshActionInner() {
const context = getActionContext();
if (!context.identity) return;
const currentView = uiStatePart.getState()!.activeView;
@@ -1355,48 +1383,48 @@ async function dispatchCombinedRefreshAction() {
}
}
// Initialize auto-refresh
let refreshInterval: NodeJS.Timeout | null = null;
let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessary restarts
// Create a proper action for the combined refresh so we can use createScheduledAction
const combinedRefreshAction = statsStatePart.createAction<void>(async (statePartArg) => {
await dispatchCombinedRefreshAction();
// Return current state — dispatchCombinedRefreshAction already updates all state parts directly
return statePartArg.getState()!;
});
// Initialize auto-refresh when UI state is ready
(() => {
const startAutoRefresh = () => {
const uiState = uiStatePart.getState()!;
const loginState = loginStatePart.getState()!;
// Scheduled refresh process with autoPause: 'visibility' — automatically pauses when tab is hidden
let refreshProcess: ReturnType<typeof statsStatePart.createScheduledAction> | null = null;
// Only start if conditions are met and not already running at the same rate
if (uiState.autoRefresh && loginState.isLoggedIn) {
// Check if we need to restart the interval (rate changed or not running)
if (!refreshInterval || currentRefreshRate !== uiState.refreshInterval) {
stopAutoRefresh();
currentRefreshRate = uiState.refreshInterval;
refreshInterval = setInterval(() => {
// Use combined refresh action for efficiency
dispatchCombinedRefreshAction();
}, uiState.refreshInterval);
}
} else {
stopAutoRefresh();
const startAutoRefresh = () => {
const uiState = uiStatePart.getState()!;
const loginState = loginStatePart.getState()!;
if (uiState.autoRefresh && loginState.isLoggedIn) {
// Dispose old process if interval changed or not running
if (refreshProcess) {
refreshProcess.dispose();
refreshProcess = null;
}
};
const stopAutoRefresh = () => {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
currentRefreshRate = 0;
refreshProcess = statsStatePart.createScheduledAction({
action: combinedRefreshAction,
payload: undefined,
intervalMs: uiState.refreshInterval,
autoPause: 'visibility',
});
} else {
if (refreshProcess) {
refreshProcess.dispose();
refreshProcess = null;
}
};
}
};
// Watch for relevant changes only
let previousAutoRefresh = uiStatePart.getState()!.autoRefresh;
let previousRefreshInterval = uiStatePart.getState()!.refreshInterval;
let previousIsLoggedIn = loginStatePart.getState()!.isLoggedIn;
uiStatePart.state.subscribe((state) => {
// Only restart if relevant values changed
if (state.autoRefresh !== previousAutoRefresh ||
// Watch for relevant changes
let previousAutoRefresh = uiStatePart.getState()!.autoRefresh;
let previousRefreshInterval = uiStatePart.getState()!.refreshInterval;
let previousIsLoggedIn = loginStatePart.getState()!.isLoggedIn;
uiStatePart.select((s) => ({ autoRefresh: s.autoRefresh, refreshInterval: s.refreshInterval }))
.subscribe((state) => {
if (state.autoRefresh !== previousAutoRefresh ||
state.refreshInterval !== previousRefreshInterval) {
previousAutoRefresh = state.autoRefresh;
previousRefreshInterval = state.refreshInterval;
@@ -1404,26 +1432,33 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
}
});
loginStatePart.state.subscribe((state) => {
// Only restart if login state changed
if (state.isLoggedIn !== previousIsLoggedIn) {
previousIsLoggedIn = state.isLoggedIn;
startAutoRefresh();
loginStatePart.select((s) => s.isLoggedIn).subscribe((isLoggedIn) => {
if (isLoggedIn !== previousIsLoggedIn) {
previousIsLoggedIn = isLoggedIn;
startAutoRefresh();
// Connect/disconnect TypedSocket based on login state
if (state.isLoggedIn) {
connectSocket();
} else {
disconnectSocket();
}
// Connect/disconnect TypedSocket based on login state
if (isLoggedIn) {
connectSocket();
} else {
disconnectSocket();
}
});
}
});
// Initial start
startAutoRefresh();
// Connect TypedSocket if already logged in (e.g., persistent session)
if (loginStatePart.getState()!.isLoggedIn) {
// Pause/resume WebSocket when tab visibility changes
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
disconnectSocket();
} else if (loginStatePart.getState()!.isLoggedIn) {
connectSocket();
}
})();
});
// Initial start
startAutoRefresh();
// Connect TypedSocket if already logged in (e.g., persistent session)
if (loginStatePart.getState()!.isLoggedIn) {
connectSocket();
}