From 41fb964ebbf914cab96344fef91110684b5e48bd Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 15:23:14 +0200 Subject: [PATCH] perf(web): stream dashboard tile events --- apps/web/src/App.tsx | 51 +++++++++++ .../src/lib/client/dashboard-refresh.test.ts | 64 ++++++++++++++ apps/web/src/lib/client/dashboard-refresh.ts | 45 ++++++++++ apps/web/src/server/index.test.ts | 24 ++++++ apps/web/src/server/index.ts | 6 ++ apps/web/src/server/routes/dashboard.test.ts | 40 +++++++++ apps/web/src/server/routes/dashboard.ts | 85 +++++++++++++++++++ 7 files changed, 315 insertions(+) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index fdb610e..826c1a3 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -17,6 +17,7 @@ import { createDashboardRequestAborter, runViewportAwareDashboardHydrationQueue, shouldPauseDashboardRefresh, + subscribeToDashboardTileEvents, waitForDashboardHydrationIdle, type DashboardIntersectionObserverFactory, type DashboardTileReference, @@ -177,6 +178,7 @@ export default function App() { useEffect(() => { let cancelled = false; let refreshTimer: number | undefined; + let unsubscribeTileEvents: (() => void) | undefined; let lastRefreshIntervalMs = dashboardFallbackRefreshIntervalMs; let hydrationRun = 0; const requestAborter = createDashboardRequestAborter(); @@ -213,6 +215,8 @@ export default function App() { function pauseRefreshes() { clearRefreshTimer(); + unsubscribeTileEvents?.(); + unsubscribeTileEvents = undefined; requestAborter.abortActiveRequests(); setHydratingItemIds(new Set()); } @@ -266,6 +270,14 @@ export default function App() { schemaVersion: restored.dashboard.schemaVersion, }, ); + subscribeDashboardTileEvents( + currentRun, + tileSignal, + { + currentRevisionId: restored.dashboard.currentRevisionId, + schemaVersion: restored.dashboard.schemaVersion, + }, + ); } else { setHydratingItemIds(new Set()); } @@ -358,6 +370,30 @@ export default function App() { }); } + function subscribeDashboardTileEvents( + run: number, + signal: AbortSignal, + snapshotContext: DashboardTileSnapshotStoreContext, + ) { + unsubscribeTileEvents?.(); + unsubscribeTileEvents = subscribeToDashboardTileEvents({ + onTile: (event) => { + const tileResponse = event as DashboardTileResponse; + if (!isDashboardTileResponse(tileResponse)) return; + applyDashboardTileHydrationResponse( + tileResponse.tile, + tileResponse, + run, + signal, + snapshotContext, + ); + }, + onUnavailable: () => { + unsubscribeTileEvents = undefined; + }, + }); + } + async function hydrateDashboardTileBatch( tiles: DashboardTileReference[], run: number, @@ -526,6 +562,7 @@ export default function App() { return () => { cancelled = true; clearRefreshTimer(); + unsubscribeTileEvents?.(); requestAborter.abortActiveRequests(); detachRefreshLifecycle(); }; @@ -625,6 +662,20 @@ function isAbortError(error: unknown): boolean { ); } +function isDashboardTileResponse(value: unknown): value is DashboardTileResponse { + return ( + typeof value === "object" && + value !== null && + "state" in value && + (value.state === "ready" || + value.state === "not_found" || + value.state === "disabled") && + "tile" in value && + typeof value.tile === "object" && + value.tile !== null + ); +} + export function restoreDashboardTileSnapshots( dashboard: DashboardRuntimeState, snapshots: RestoredDashboardTileSnapshot[], diff --git a/apps/web/src/lib/client/dashboard-refresh.test.ts b/apps/web/src/lib/client/dashboard-refresh.test.ts index e17eab2..7da37ea 100644 --- a/apps/web/src/lib/client/dashboard-refresh.test.ts +++ b/apps/web/src/lib/client/dashboard-refresh.test.ts @@ -13,6 +13,7 @@ import { runViewportAwareDashboardHydrationQueue, shouldPauseDashboardRefresh, splitDashboardHydrationItemsByVisibility, + subscribeToDashboardTileEvents, waitForDashboardHydrationIdle, type DashboardIntersectionEntry, } from "./dashboard-refresh"; @@ -302,6 +303,69 @@ describe("dashboard refresh lifecycle", () => { ]); }); + test("subscribes to dashboard tile events", () => { + const received: unknown[] = []; + let listener: ((event: MessageEvent) => void) | undefined; + let closed = false; + + const unsubscribe = subscribeToDashboardTileEvents({ + createEventSource: (url) => { + expect(url).toBe("/api/dashboard/events"); + return { + addEventListener(_type, eventListener) { + listener = eventListener; + }, + close() { + closed = true; + }, + onerror: null, + }; + }, + onTile: (tile) => received.push(tile), + }); + + listener?.({ data: JSON.stringify({ state: "ready" }) } as MessageEvent); + expect(received).toEqual([{ state: "ready" }]); + unsubscribe(); + expect(closed).toBe(true); + }); + + test("falls back when dashboard tile events are unavailable or fail", () => { + let unavailableCount = 0; + + subscribeToDashboardTileEvents({ + createEventSource: undefined, + onTile: () => undefined, + onUnavailable: () => { + unavailableCount += 1; + }, + }); + + let errorHandler: (() => void) | null = null; + const unsubscribe = subscribeToDashboardTileEvents({ + createEventSource: () => ({ + addEventListener: () => undefined, + close: () => undefined, + get onerror() { + return errorHandler; + }, + set onerror(handler) { + errorHandler = handler; + }, + }), + onTile: () => undefined, + onUnavailable: () => { + unavailableCount += 1; + }, + }); + + if (!errorHandler) throw new Error("expected error handler"); + const triggerError = errorHandler as unknown as () => void; + triggerError(); + unsubscribe(); + expect(unavailableCount).toBe(2); + }); + test("backs off failed tile keys and resets after success", () => { const backoff = createDashboardTileBackoff(); diff --git a/apps/web/src/lib/client/dashboard-refresh.ts b/apps/web/src/lib/client/dashboard-refresh.ts index 76cc83d..989816c 100644 --- a/apps/web/src/lib/client/dashboard-refresh.ts +++ b/apps/web/src/lib/client/dashboard-refresh.ts @@ -489,6 +489,51 @@ export function createDashboardPerformanceMarks( }; } +export interface DashboardTileEventSource { + addEventListener( + type: "dashboard-tile", + listener: (event: MessageEvent) => void, + ): void; + close(): void; + onerror: (() => void) | null; +} + +export interface DashboardTileEventSubscriptionOptions { + createEventSource?: (url: string) => DashboardTileEventSource; + onTile: (data: unknown) => void; + onUnavailable?: () => void; + url?: string; +} + +export function subscribeToDashboardTileEvents( + options: DashboardTileEventSubscriptionOptions, +): () => void { + const createEventSource = options.createEventSource || + (typeof globalThis.EventSource !== "undefined" + ? (url: string) => new globalThis.EventSource(url) + : undefined); + + if (!createEventSource) { + options.onUnavailable?.(); + return () => undefined; + } + + const source = createEventSource(options.url || "/api/dashboard/events"); + source.addEventListener("dashboard-tile", (event) => { + try { + options.onTile(JSON.parse(event.data)); + } catch { + // Ignore malformed diagnostics from an optional live transport. + } + }); + source.onerror = () => { + source.close(); + options.onUnavailable?.(); + }; + + return () => source.close(); +} + const dashboardTileBackoffDelaysMs = [15_000, 30_000, 60_000, 120_000]; export function createDashboardTileBackoff() { diff --git a/apps/web/src/server/index.test.ts b/apps/web/src/server/index.test.ts index 9e5a34d..af1035d 100644 --- a/apps/web/src/server/index.test.ts +++ b/apps/web/src/server/index.test.ts @@ -50,4 +50,28 @@ describe("server request routing", () => { expect(response.status).toBe(405); expect(response.headers.get("allow")).toBe("POST"); }); + + test("routes dashboard event stream requests", async () => { + const response = await handleRequest( + new Request("https://example.test/api/dashboard/events", { + method: "GET", + }), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe( + "text/event-stream; charset=utf-8", + ); + }); + + test("rejects non-get dashboard event stream requests", async () => { + const response = await handleRequest( + new Request("https://example.test/api/dashboard/events", { + method: "POST", + }), + ); + + expect(response.status).toBe(405); + expect(response.headers.get("allow")).toBe("GET"); + }); }); diff --git a/apps/web/src/server/index.ts b/apps/web/src/server/index.ts index cf624ed..9cbcbd9 100644 --- a/apps/web/src/server/index.ts +++ b/apps/web/src/server/index.ts @@ -1,6 +1,7 @@ import { extname, normalize } from "node:path"; import { handleAgentDashboardRoute } from "./routes/agent-dashboard"; import { + handleDashboardEventsRoute, handleDashboardRoute, handleDashboardTileRoute, handleDashboardTilesRoute, @@ -32,6 +33,11 @@ export async function handleRequest(request: Request): Promise { return handleDashboardTilesRoute(request); } + if (url.pathname === "/api/dashboard/events") { + if (request.method !== "GET") return methodNotAllowed(["GET"]); + return handleDashboardEventsRoute({ signal: request.signal }); + } + if (url.pathname.startsWith("/api/dashboard/tile/")) { if (request.method !== "GET") return methodNotAllowed(["GET"]); return handleDashboardTileRoute(url.pathname); diff --git a/apps/web/src/server/routes/dashboard.test.ts b/apps/web/src/server/routes/dashboard.test.ts index a0054e2..a4cf7ef 100644 --- a/apps/web/src/server/routes/dashboard.test.ts +++ b/apps/web/src/server/routes/dashboard.test.ts @@ -3,6 +3,7 @@ import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; import { createDashboardTileCache, dashboardTileCacheKey, + handleDashboardEventsRoute, handleDashboardTilesRoute, handleDashboardTileRoute, loadDashboardResponse, @@ -605,6 +606,45 @@ describe("dashboard API route", () => { }); }); + test("streams ready dashboard tile events", async () => { + const controller = new AbortController(); + const response = await handleDashboardEventsRoute({ + refreshSeedDocument: true, + seedIfEmpty: true, + signal: controller.signal, + tileCache: { + async resolve(key) { + controller.abort(); + const tile = JSON.parse(key); + + return { + cache: "miss", + coalesced: false, + response: { + state: "ready", + tile, + item: { + id: tile.id, + label: "Status", + severity: "ok", + value: "ok", + }, + }, + }; + }, + }, + }); + + expect(response.headers.get("content-type")).toBe( + "text/event-stream; charset=utf-8", + ); + expect(response.headers.get("cache-control")).toBe("no-cache"); + const body = await response.text(); + expect(body).toContain("event: dashboard-tile"); + expect(body).toContain('"state":"ready"'); + expect(body).toContain('"tile"'); + }); + test("rejects invalid batch tile requests", async () => { const response = await handleDashboardTilesRoute( new Request("https://example.test/api/dashboard/tiles", { diff --git a/apps/web/src/server/routes/dashboard.ts b/apps/web/src/server/routes/dashboard.ts index 2917689..c42227c 100644 --- a/apps/web/src/server/routes/dashboard.ts +++ b/apps/web/src/server/routes/dashboard.ts @@ -25,6 +25,10 @@ export interface LoadDashboardResponseOptions tileCache?: DashboardTileCache; } +export interface DashboardEventsRouteOptions extends LoadDashboardResponseOptions { + signal?: AbortSignal; +} + interface DashboardTileCacheEntry { expiresAt: number; response: DashboardTileResolution; @@ -293,6 +297,48 @@ export async function handleDashboardTilesRoute( }); } +export async function handleDashboardEventsRoute( + options: DashboardEventsRouteOptions = {}, +): Promise { + const stream = new ReadableStream({ + async start(controller) { + const encoder = new TextEncoder(); + const dashboard = await loadDashboardResponse({ + ...options, + hydrateLiveDatasources: false, + }); + + if (dashboard.state !== "ready" || options.signal?.aborted) { + controller.close(); + return; + } + + try { + for (const tile of dashboardEventTiles(dashboard.document)) { + if (options.signal?.aborted) break; + const resolution = await loadDashboardTileResponse(tile, options); + if (resolution.state === "ready") { + controller.enqueue( + encoder.encode(dashboardTileEventChunk(resolution)), + ); + } + } + } finally { + controller.close(); + } + }, + }); + + return new Response(stream, { + headers: { + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Content-Type": "text/event-stream; charset=utf-8", + "X-Accel-Buffering": "no", + }, + }); +} + export function dashboardTileCacheKey(tile: DashboardTileReference): string { return JSON.stringify(tile); } @@ -422,6 +468,45 @@ function parseDashboardTileReference(value: unknown): DashboardTileReference | n return null; } +function dashboardEventTiles(document: DashboardDocument): DashboardTileReference[] { + const status = document.statusStrips.flatMap((strip) => + strip.items.map((item): DashboardTileReference => ({ + kind: "status", + stripId: strip.id, + id: item.id, + })), + ); + const telemetry = document.telemetry + .filter((card) => card.datasource?.type === "external") + .map((card): DashboardTileReference => ({ kind: "telemetry", id: card.id })); + const modules = (document.modules || []) + .filter((module) => + module.datasource?.type === "external" || + module.id === "runtime-health-summary" + ) + .map((module): DashboardTileReference => ({ kind: "module", id: module.id })); + const services = document.serviceGroups.flatMap((group) => + group.services + .filter((service) => service.datasource?.type === "external") + .map((service): DashboardTileReference => ({ + kind: "service", + groupId: group.id, + id: service.id, + })), + ); + + return [...status, ...telemetry, ...modules, ...services]; +} + +function dashboardTileEventChunk(resolution: DashboardTileResolution): string { + return [ + "event: dashboard-tile", + `data: ${JSON.stringify(resolution)}`, + "", + "", + ].join("\n"); +} + class DashboardTileCacheResolutionError extends Error { readonly cache: "hit" | "miss"; readonly coalesced: boolean;