diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index c886391..206b74f 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -7,11 +7,6 @@ import type { TelemetryCard, } from "@dimensionlab/dashboard-model"; import type { DashboardRuntimeState } from "$lib/server/dashboard"; -import { - attachDashboardRefreshLifecycle, - createDashboardRequestAborter, - shouldPauseDashboardRefresh, -} from "$lib/client/dashboard-refresh"; import { dashboardDocumentToUiDashboard } from "$lib/ui-adapter/model-renderer"; import { DashboardFrame, @@ -156,97 +151,65 @@ export default function App() { let cancelled = false; let refreshTimer: number | undefined; let hydrationRun = 0; - const requestAborter = createDashboardRequestAborter(); - function refreshPaused() { - return shouldPauseDashboardRefresh({ - visibilityState: document.visibilityState, - online: navigator.onLine, - }); - } + async function loadDashboard() { + const response = await fetch("/api/dashboard"); + const nextDashboard = (await response.json()) as DashboardRuntimeState; + + if (cancelled) return; + setDashboard(nextDashboard); + + const currentRun = ++hydrationRun; + if ( + nextDashboard.state === "ready" && + nextDashboard.liveDatasourceHydration?.enabled !== false + ) { + const tiles = dashboardHydrationTiles(nextDashboard.document); + setHydratingItemIds(new Set(tiles.map(dashboardTileKey))); + hydrateDashboardTiles(tiles, currentRun); + } else { + setHydratingItemIds(new Set()); + } - function clearRefreshTimer() { if (refreshTimer) { window.clearInterval(refreshTimer); refreshTimer = undefined; } - } - function pauseRefreshes() { - clearRefreshTimer(); - requestAborter.abortActiveRequests(); - setHydratingItemIds(new Set()); - } + const refreshIntervalSeconds = + nextDashboard.state === "ready" + ? nextDashboard.document.metadata.refreshIntervalSeconds + : undefined; - async function loadDashboard() { - if (cancelled || refreshPaused()) return; - - const shellSignal = requestAborter.beginShellRun(); - - try { - const response = await fetch("/api/dashboard", { signal: shellSignal }); - const nextDashboard = (await response.json()) as DashboardRuntimeState; - - if (cancelled || shellSignal.aborted || refreshPaused()) return; - setDashboard(nextDashboard); - - const currentRun = ++hydrationRun; - if ( - nextDashboard.state === "ready" && - nextDashboard.liveDatasourceHydration?.enabled !== false - ) { - const tiles = dashboardHydrationTiles(nextDashboard.document); - const tileSignal = requestAborter.beginTileRun(); - setHydratingItemIds(new Set(tiles.map(dashboardTileKey))); - hydrateDashboardTiles(tiles, currentRun, tileSignal); - } else { - setHydratingItemIds(new Set()); - } - - clearRefreshTimer(); - - const refreshIntervalSeconds = - nextDashboard.state === "ready" - ? nextDashboard.document.metadata.refreshIntervalSeconds - : undefined; - - if (refreshIntervalSeconds && !refreshPaused()) { - refreshTimer = window.setInterval( - () => void loadDashboard(), - refreshIntervalSeconds * 1000, - ); - } - } catch (error) { - if (!isAbortError(error) && !cancelled) { - console.error("Dashboard refresh failed", error); - } + if (refreshIntervalSeconds) { + refreshTimer = window.setInterval( + () => void loadDashboard(), + refreshIntervalSeconds * 1000, + ); } } function hydrateDashboardTiles( tiles: DashboardTileReference[], run: number, - signal: AbortSignal, ) { tiles.forEach((tile) => { - void hydrateDashboardTile(tile, run, signal); + void hydrateDashboardTile(tile, run); }); } async function hydrateDashboardTile( tile: DashboardTileReference, run: number, - signal: AbortSignal, ) { const key = dashboardTileKey(tile); try { - const response = await fetch(dashboardTileUrl(tile), { signal }); + const response = await fetch(dashboardTileUrl(tile)); const tileResponse = (await response.json()) as DashboardTileResponse; if ( cancelled || - signal.aborted || run !== hydrationRun || !response.ok || tileResponse.state !== "ready" @@ -263,12 +226,8 @@ export default function App() { } : current, ); - } catch (error) { - if (!isAbortError(error) && !cancelled) { - console.error("Dashboard tile hydration failed", error); - } } finally { - if (!cancelled && !signal.aborted && run === hydrationRun) { + if (!cancelled && run === hydrationRun) { setHydratingItemIds((current) => { const next = new Set(current); next.delete(key); @@ -278,21 +237,11 @@ export default function App() { } } - const detachRefreshLifecycle = attachDashboardRefreshLifecycle({ - documentTarget: document, - windowTarget: window, - loadDashboard, - pauseRefreshes, - refreshPaused: () => cancelled || refreshPaused(), - }); - void loadDashboard(); return () => { cancelled = true; - clearRefreshTimer(); - requestAborter.abortActiveRequests(); - detachRefreshLifecycle(); + if (refreshTimer) window.clearInterval(refreshTimer); }; }, []); @@ -326,15 +275,6 @@ function getThemeStorage(): Storage | undefined { } } -function isAbortError(error: unknown): boolean { - return ( - typeof error === "object" && - error !== null && - "name" in error && - error.name === "AbortError" - ); -} - function markHydratingItems( dashboard: UiDashboardPreview, hydratingItemIds?: ReadonlySet, diff --git a/apps/web/src/lib/client/dashboard-refresh.test.ts b/apps/web/src/lib/client/dashboard-refresh.test.ts deleted file mode 100644 index 75305d5..0000000 --- a/apps/web/src/lib/client/dashboard-refresh.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { - attachDashboardRefreshLifecycle, - createDashboardRequestAborter, - shouldPauseDashboardRefresh, -} from "./dashboard-refresh"; - -describe("dashboard refresh lifecycle", () => { - test("pauses refreshes when the document is hidden or the browser is offline", () => { - expect( - shouldPauseDashboardRefresh({ visibilityState: "visible", online: true }), - ).toBe(false); - expect( - shouldPauseDashboardRefresh({ visibilityState: "hidden", online: true }), - ).toBe(true); - expect( - shouldPauseDashboardRefresh({ visibilityState: "visible", online: false }), - ).toBe(true); - }); - - test("aborts shell and tile requests when a new shell run starts", () => { - const aborter = createDashboardRequestAborter(); - const shellSignal = aborter.beginShellRun(); - const tileSignal = aborter.beginTileRun(); - - const nextShellSignal = aborter.beginShellRun(); - - expect(shellSignal.aborted).toBe(true); - expect(tileSignal.aborted).toBe(true); - expect(nextShellSignal.aborted).toBe(false); - }); - - test("aborts active requests when refreshes are paused", () => { - const aborter = createDashboardRequestAborter(); - const shellSignal = aborter.beginShellRun(); - const tileSignal = aborter.beginTileRun(); - - aborter.abortActiveRequests(); - - expect(shellSignal.aborted).toBe(true); - expect(tileSignal.aborted).toBe(true); - }); - - test("pauses on hidden, offline, and pagehide events then resumes immediately when visible", () => { - const documentTarget = new EventTarget(); - const windowTarget = new EventTarget(); - const calls: string[] = []; - let paused = true; - - const detach = attachDashboardRefreshLifecycle({ - documentTarget, - windowTarget, - loadDashboard: () => { - calls.push("load"); - }, - pauseRefreshes: () => { - calls.push("pause"); - }, - refreshPaused: () => paused, - }); - - documentTarget.dispatchEvent(new Event("visibilitychange")); - windowTarget.dispatchEvent(new Event("offline")); - windowTarget.dispatchEvent(new Event("pagehide")); - - paused = false; - documentTarget.dispatchEvent(new Event("visibilitychange")); - windowTarget.dispatchEvent(new Event("online")); - - detach(); - documentTarget.dispatchEvent(new Event("visibilitychange")); - windowTarget.dispatchEvent(new Event("online")); - - expect(calls).toEqual(["pause", "pause", "pause", "load", "load"]); - }); -}); diff --git a/apps/web/src/lib/client/dashboard-refresh.ts b/apps/web/src/lib/client/dashboard-refresh.ts deleted file mode 100644 index 1124b27..0000000 --- a/apps/web/src/lib/client/dashboard-refresh.ts +++ /dev/null @@ -1,90 +0,0 @@ -export interface DashboardRefreshPauseState { - visibilityState: DocumentVisibilityState; - online: boolean; -} - -export function shouldPauseDashboardRefresh( - state: DashboardRefreshPauseState, -): boolean { - return state.visibilityState !== "visible" || !state.online; -} - -export function createDashboardRequestAborter() { - let shellController: AbortController | undefined; - let tileController: AbortController | undefined; - - function abortController(controller: AbortController | undefined) { - if (controller && !controller.signal.aborted) { - controller.abort(); - } - } - - return { - beginShellRun(): AbortSignal { - abortController(shellController); - abortController(tileController); - shellController = new AbortController(); - tileController = undefined; - return shellController.signal; - }, - beginTileRun(): AbortSignal { - abortController(tileController); - tileController = new AbortController(); - return tileController.signal; - }, - abortActiveRequests(): void { - abortController(shellController); - abortController(tileController); - shellController = undefined; - tileController = undefined; - }, - }; -} - -type DashboardRefreshEventTarget = Pick< - EventTarget, - "addEventListener" | "removeEventListener" ->; - -export interface DashboardRefreshLifecycleOptions { - documentTarget: DashboardRefreshEventTarget; - windowTarget: DashboardRefreshEventTarget; - loadDashboard: () => void | Promise; - pauseRefreshes: () => void; - refreshPaused: () => boolean; -} - -export function attachDashboardRefreshLifecycle( - options: DashboardRefreshLifecycleOptions, -): () => void { - function handleRefreshLifecycleChange() { - if (options.refreshPaused()) { - options.pauseRefreshes(); - return; - } - - void options.loadDashboard(); - } - - function handlePageHide() { - options.pauseRefreshes(); - } - - options.documentTarget.addEventListener( - "visibilitychange", - handleRefreshLifecycleChange, - ); - options.windowTarget.addEventListener("online", handleRefreshLifecycleChange); - options.windowTarget.addEventListener("offline", handleRefreshLifecycleChange); - options.windowTarget.addEventListener("pagehide", handlePageHide); - - return () => { - options.documentTarget.removeEventListener( - "visibilitychange", - handleRefreshLifecycleChange, - ); - options.windowTarget.removeEventListener("online", handleRefreshLifecycleChange); - options.windowTarget.removeEventListener("offline", handleRefreshLifecycleChange); - options.windowTarget.removeEventListener("pagehide", handlePageHide); - }; -}