feat(web-ui): pause dashboard polling, sockets, and chart updates when the tab is hidden
This commit is contained in:
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user