From 8bd9b1dae702ad16d48a1ff34749fa61e35c6911 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Thu, 12 Jun 2025 16:24:28 +1000 Subject: [PATCH] perf: reduce timetable resource consuption and update notification collector --- .../built-in/notificationCollector/index.ts | 53 ++++++++++++- src/seqta/utils/updateTimetableTimes.ts | 76 +++++++++++++------ 2 files changed, 104 insertions(+), 25 deletions(-) diff --git a/src/plugins/built-in/notificationCollector/index.ts b/src/plugins/built-in/notificationCollector/index.ts index 6233909f..9763f4dd 100644 --- a/src/plugins/built-in/notificationCollector/index.ts +++ b/src/plugins/built-in/notificationCollector/index.ts @@ -3,6 +3,7 @@ import type { Plugin } from "../../core/types"; interface NotificationCollectorStorage { lastNotificationCount: number; lastCheckedTime: string; + consecutiveErrors: number; } const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = { @@ -15,13 +16,24 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = { run: async (api) => { let pollInterval: number | null = null; + let isVisible = !document.hidden; + let baseInterval = 30000; // 30 seconds + const maxInterval = 300000; // 5 minutes max // Store last notification count in storage if (!api.storage.lastNotificationCount) { api.storage.lastNotificationCount = 0; } + if (!api.storage.consecutiveErrors) { + api.storage.consecutiveErrors = 0; + } const checkNotifications = async () => { + // Skip if tab is not visible to save battery + if (!isVisible) { + return; + } + try { const alertDiv = document.querySelector( "[class*='notifications__bubble___']", @@ -51,6 +63,9 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = { const notificationCount = data.payload.notifications.length; api.storage.lastNotificationCount = notificationCount; api.storage.lastCheckedTime = new Date().toISOString(); + + // Reset error count on success + api.storage.consecutiveErrors = 0; if (alertDiv) { alertDiv.textContent = notificationCount.toString(); @@ -59,18 +74,37 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = { } } catch (error) { console.error("[BetterSEQTA+] Error fetching notifications:", error); + api.storage.consecutiveErrors = (api.storage.consecutiveErrors || 0) + 1; } }; + const getNextInterval = () => { + // Exponential backoff on errors, max 5 minutes + const errorMultiplier = Math.min(Math.pow(2, api.storage.consecutiveErrors || 0), 10); + return Math.min(baseInterval * errorMultiplier, maxInterval); + }; + const startPolling = () => { if (pollInterval) return; // Already polling checkNotifications(); - pollInterval = window.setInterval(checkNotifications, 30000); + + const scheduleNext = () => { + const interval = getNextInterval(); + pollInterval = window.setTimeout(() => { + checkNotifications().then(() => { + if (pollInterval) { // Only continue if not stopped + scheduleNext(); + } + }); + }, interval); + }; + + scheduleNext(); }; const stopPolling = () => { if (pollInterval) { - window.clearInterval(pollInterval); + window.clearTimeout(pollInterval); pollInterval = null; const alertDiv = document.querySelector( "[class*='notifications__bubble___']", @@ -85,12 +119,27 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = { } }; + // Listen for visibility changes to pause/resume polling + const handleVisibilityChange = () => { + isVisible = !document.hidden; + if (isVisible && !pollInterval) { + // Resume polling when tab becomes visible + const alertDiv = document.querySelector("[class*='notifications__bubble___']"); + if (alertDiv) { + startPolling(); + } + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + api.seqta.onMount("[class*='notifications__bubble___']", (_) => { startPolling(); }); return () => { stopPolling(); + document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, }; diff --git a/src/seqta/utils/updateTimetableTimes.ts b/src/seqta/utils/updateTimetableTimes.ts index 98579c0e..8047d6fe 100644 --- a/src/seqta/utils/updateTimetableTimes.ts +++ b/src/seqta/utils/updateTimetableTimes.ts @@ -4,7 +4,8 @@ import { waitForElm } from "./waitForElm"; let timetableObserver: MutationObserver | null = null; let isOnTimetablePage = false; -let urlCheckInterval: number | null = null; +let isInitialized = false; +let abortController: AbortController | null = null; function updateTimeElements(): void { if (!settingsState.timeFormat || settingsState.timeFormat !== "12") return; @@ -90,30 +91,54 @@ function stopTimetableMonitoring(): void { } } -function startUrlMonitoring(): void { - if (urlCheckInterval) return; - - // Check URL every 100ms when on timetable page - urlCheckInterval = window.setInterval(() => { - const currentlyOnTimetable = checkIfOnTimetablePage(); +function handleUrlChange(): void { + const currentlyOnTimetable = checkIfOnTimetablePage(); + + if (currentlyOnTimetable !== isOnTimetablePage) { + isOnTimetablePage = currentlyOnTimetable; - if (currentlyOnTimetable !== isOnTimetablePage) { - isOnTimetablePage = currentlyOnTimetable; - - if (isOnTimetablePage) { - // Wait a bit for the page to load, then start monitoring - setTimeout(() => { - updateTimeElements(); - startTimetableMonitoring(); - }, 100); - } else { - stopTimetableMonitoring(); - } - } else if (isOnTimetablePage) { - // Even if we're still on timetable page, update times in case navigation happened - updateTimeElements(); + if (isOnTimetablePage) { + // Wait a bit for the page to load, then start monitoring + setTimeout(() => { + updateTimeElements(); + startTimetableMonitoring(); + }, 100); + } else { + stopTimetableMonitoring(); } - }, 100); + } else if (isOnTimetablePage) { + // Even if we're still on timetable page, update times in case navigation happened + updateTimeElements(); + } +} + +function startUrlMonitoring(): void { + if (isInitialized) return; + isInitialized = true; + + // Create abort controller for cleanup + abortController = new AbortController(); + const signal = abortController.signal; + + // Listen for hash changes (more efficient than polling) + window.addEventListener('hashchange', handleUrlChange, { signal }); + window.addEventListener('popstate', handleUrlChange, { signal }); + + // Initial check + handleUrlChange(); +} + +function stopUrlMonitoring(): void { + if (!isInitialized) return; + isInitialized = false; + + // Abort all event listeners at once + if (abortController) { + abortController.abort(); + abortController = null; + } + + stopTimetableMonitoring(); } export async function updateTimetableTimes(): Promise { @@ -144,3 +169,8 @@ if (typeof window !== "undefined") { // Start URL monitoring immediately startUrlMonitoring(); } + +// Cleanup function for when the module is unloaded +export function cleanup(): void { + stopUrlMonitoring(); +}