diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx index 5053640..4db0cbc 100644 --- a/apps/web/src/App.test.tsx +++ b/apps/web/src/App.test.tsx @@ -4,7 +4,6 @@ import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures" import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; import { AppStateView, - dashboardTileMatchKey, dashboardHydrationTiles, restoreDashboardTileSnapshots, } from "./App"; @@ -160,17 +159,4 @@ describe("React app dashboard state view", () => { severity: "stale", }); }); - - test("uses structured tile match keys for delimiter-bearing ids", () => { - expect( - dashboardTileMatchKey({ kind: "service", groupId: "a:b", id: "c" }), - ).not.toBe( - dashboardTileMatchKey({ kind: "service", groupId: "a", id: "b:c" }), - ); - expect( - dashboardTileMatchKey({ kind: "status", stripId: "a:b", id: "c" }), - ).not.toBe( - dashboardTileMatchKey({ kind: "status", stripId: "a", id: "b:c" }), - ); - }); }); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index fdb610e..ac3b31b 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -52,11 +52,6 @@ type DashboardTileResponse = message: string; }; -type DashboardTilesBatchResponse = { - state: "ready"; - tiles: DashboardTileResponse[]; -}; - type DashboardTileHydrationResult = "aborted" | "failed" | "ready"; const dashboardTileHydrationConcurrency = 6; @@ -297,7 +292,6 @@ export default function App() { const modelIds = tiles.map(dashboardTileModelId); void runViewportAwareDashboardHydrationQueue({ - batchSize: dashboardTileHydrationConcurrency, collectVisibleModelIds: async () => { await waitForDashboardRenderFrame(signal); return collectVisibleDashboardModelIds({ @@ -327,23 +321,6 @@ export default function App() { tileBackoff.recordFailure(key); } }, - hydrateBatch: async (batch) => { - const results = await hydrateDashboardTileBatch( - batch, - run, - signal, - snapshotContext, - ); - - for (const { result, tile } of results) { - const key = dashboardTileKey(tile); - if (result === "ready") { - tileBackoff.recordSuccess(key); - } else if (result === "failed") { - tileBackoff.recordFailure(key); - } - } - }, items: tiles, onAllItemsSettled: () => performanceMarks.markAllTilesSettled(), onVisibleItemsSettled: () => performanceMarks.markVisibleTilesReady(), @@ -358,69 +335,6 @@ export default function App() { }); } - async function hydrateDashboardTileBatch( - tiles: DashboardTileReference[], - run: number, - signal: AbortSignal, - snapshotContext: DashboardTileSnapshotStoreContext, - ): Promise> { - try { - const response = await fetch("/api/dashboard/tiles", { - body: JSON.stringify({ tiles }), - headers: { "Content-Type": "application/json" }, - method: "POST", - signal, - }); - const batchResponse = (await response.json()) as DashboardTilesBatchResponse; - - if ( - cancelled || - signal.aborted || - run !== hydrationRun || - !response.ok || - batchResponse.state !== "ready" - ) { - throw new Error("Dashboard tile batch hydration failed"); - } - - const responsesByKey = new Map( - batchResponse.tiles.map((tileResponse) => [ - dashboardTileMatchKey(tileResponse.tile), - tileResponse, - ]), - ); - - return tiles.map((tile) => { - const key = dashboardTileKey(tile); - try { - return { - tile, - result: applyDashboardTileHydrationResponse( - tile, - responsesByKey.get(dashboardTileMatchKey(tile)), - run, - signal, - snapshotContext, - ), - }; - } finally { - finishDashboardTileHydration(key, run, signal); - } - }); - } catch (error) { - if (signal.aborted || cancelled || run !== hydrationRun) { - return tiles.map((tile) => ({ result: "aborted", tile })); - } - - return Promise.all( - tiles.map(async (tile) => ({ - tile, - result: await hydrateDashboardTile(tile, run, signal, snapshotContext), - })), - ); - } - } - async function hydrateDashboardTile( tile: DashboardTileReference, run: number, @@ -433,23 +347,32 @@ export default function App() { const response = await fetch(dashboardTileUrl(tile), { signal }); const tileResponse = (await response.json()) as DashboardTileResponse; - if (!response.ok) { - return "failed"; - } - - const result = applyDashboardTileHydrationResponse( - tile, - tileResponse, - run, - signal, - snapshotContext, - ); - if (result !== "ready") { + if ( + cancelled || + signal.aborted || + run !== hydrationRun || + !response.ok || + tileResponse.state !== "ready" + ) { return signal.aborted || cancelled || run !== hydrationRun ? "aborted" : "failed"; } + const readyTileResponse = tileResponse; + setDashboard((current) => + current?.state === "ready" + ? { + ...current, + document: applyDashboardTile(current.document, readyTileResponse), + } + : current, + ); + tileSnapshotStore.saveReadyTile({ + ...snapshotContext, + response: readyTileResponse, + }); + performanceMarks.markFirstTileReady(); return "ready"; } catch (error) { if (!isAbortError(error) && !cancelled) { @@ -458,58 +381,13 @@ export default function App() { } return "aborted"; } finally { - finishDashboardTileHydration(key, run, signal); - } - } - - function applyDashboardTileHydrationResponse( - tile: DashboardTileReference, - tileResponse: DashboardTileResponse | undefined, - run: number, - signal: AbortSignal, - snapshotContext: DashboardTileSnapshotStoreContext, - ): DashboardTileHydrationResult { - if ( - cancelled || - signal.aborted || - run !== hydrationRun || - !tileResponse || - dashboardTileMatchKey(tileResponse.tile) !== dashboardTileMatchKey(tile) || - tileResponse.state !== "ready" - ) { - return signal.aborted || cancelled || run !== hydrationRun - ? "aborted" - : "failed"; - } - - const readyTileResponse = tileResponse; - setDashboard((current) => - current?.state === "ready" - ? { - ...current, - document: applyDashboardTile(current.document, readyTileResponse), - } - : current, - ); - tileSnapshotStore.saveReadyTile({ - ...snapshotContext, - response: readyTileResponse, - }); - performanceMarks.markFirstTileReady(); - return "ready"; - } - - function finishDashboardTileHydration( - key: string, - run: number, - signal: AbortSignal, - ) { - if (!cancelled && !signal.aborted && run === hydrationRun) { - setHydratingItemIds((current) => { - const next = new Set(current); - next.delete(key); - return next; - }); + if (!cancelled && !signal.aborted && run === hydrationRun) { + setHydratingItemIds((current) => { + const next = new Set(current); + next.delete(key); + return next; + }); + } } } @@ -749,10 +627,6 @@ function dashboardTileKey(tile: DashboardTileReference): string { return `${tile.kind}:${tile.id}`; } -export function dashboardTileMatchKey(tile: DashboardTileReference): string { - return JSON.stringify(tile); -} - function dashboardTileModelId(tile: DashboardTileReference): string { if (tile.kind === "status") return `${tile.stripId}:${tile.id}`; return tile.id; diff --git a/apps/web/src/lib/client/dashboard-refresh.test.ts b/apps/web/src/lib/client/dashboard-refresh.test.ts index e17eab2..2a2f25d 100644 --- a/apps/web/src/lib/client/dashboard-refresh.test.ts +++ b/apps/web/src/lib/client/dashboard-refresh.test.ts @@ -9,7 +9,6 @@ import { createDashboardTileBackoff, createDashboardRequestAborter, runDashboardHydrationQueue, - runDashboardHydrationBatchQueue, runViewportAwareDashboardHydrationQueue, shouldPauseDashboardRefresh, splitDashboardHydrationItemsByVisibility, @@ -122,25 +121,6 @@ describe("dashboard refresh lifecycle", () => { expect(maxActive).toBe(2); }); - test("hydrates queued work in fixed-size batches", async () => { - const batches: number[][] = []; - - await runDashboardHydrationBatchQueue({ - batchSize: 3, - hydrateBatch: (items) => { - batches.push(items); - }, - items: [1, 2, 3, 4, 5, 6, 7], - signal: new AbortController().signal, - }); - - expect(batches).toEqual([ - [1, 2, 3], - [4, 5, 6], - [7], - ]); - }); - test("hydrates visible items before deferred items and waits for idle", async () => { const calls: string[] = []; const idleReleases: Array<() => void> = []; diff --git a/apps/web/src/lib/client/dashboard-refresh.ts b/apps/web/src/lib/client/dashboard-refresh.ts index 76cc83d..a9aa3a5 100644 --- a/apps/web/src/lib/client/dashboard-refresh.ts +++ b/apps/web/src/lib/client/dashboard-refresh.ts @@ -152,12 +152,10 @@ export async function runDashboardHydrationQueue( } export interface DashboardViewportHydrationQueueOptions { - batchSize?: number; collectVisibleModelIds: () => Promise>; concurrency: number; getModelId: (item: TItem) => string; hydrate: (item: TItem) => Promise | void; - hydrateBatch?: (items: TItem[]) => Promise | void; items: TItem[]; onAllItemsSettled?: () => void; onVisibleItemsSettled?: () => void; @@ -177,7 +175,12 @@ export async function runViewportAwareDashboardHydrationQueue( visibleModelIds, }); - await runDashboardHydrationItems(options, visible); + await runDashboardHydrationQueue({ + concurrency: options.concurrency, + hydrate: options.hydrate, + items: visible, + signal: options.signal, + }); if (options.signal.aborted) return; options.onVisibleItemsSettled?.(); @@ -189,44 +192,13 @@ export async function runViewportAwareDashboardHydrationQueue( await options.waitForIdle(); if (options.signal.aborted) return; - await runDashboardHydrationItems(options, deferred); - options.onAllItemsSettled?.(); -} - -async function runDashboardHydrationItems( - options: DashboardViewportHydrationQueueOptions, - items: TItem[], -): Promise { - if (!options.hydrateBatch) { - await runDashboardHydrationQueue({ - concurrency: options.concurrency, - hydrate: options.hydrate, - items, - signal: options.signal, - }); - return; - } - - await runDashboardHydrationBatchQueue({ - batchSize: options.batchSize ?? options.concurrency, - hydrateBatch: options.hydrateBatch, - items, + await runDashboardHydrationQueue({ + concurrency: options.concurrency, + hydrate: options.hydrate, + items: deferred, signal: options.signal, }); -} - -export async function runDashboardHydrationBatchQueue(options: { - batchSize: number; - hydrateBatch: (items: TItem[]) => Promise | void; - items: TItem[]; - signal: AbortSignal; -}): Promise { - const batchSize = Math.max(1, Math.floor(options.batchSize)); - - for (let index = 0; index < options.items.length; index += batchSize) { - if (options.signal.aborted) return; - await options.hydrateBatch(options.items.slice(index, index + batchSize)); - } + options.onAllItemsSettled?.(); } export function splitDashboardHydrationItemsByVisibility(options: { diff --git a/apps/web/src/server/index.test.ts b/apps/web/src/server/index.test.ts deleted file mode 100644 index 9e5a34d..0000000 --- a/apps/web/src/server/index.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { afterAll, describe, expect, test, vi } from "vitest"; -import { handleRequest } from "./index"; - -describe("server request routing", () => { - const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => undefined); - - afterAll(() => { - consoleInfo.mockRestore(); - }); - - test("routes dashboard tile batch requests", async () => { - const response = await handleRequest( - new Request("https://example.test/api/dashboard/tiles", { - method: "POST", - body: JSON.stringify({ - tiles: [ - { - kind: "status", - stripId: "footer-status", - id: "auto-refresh", - }, - ], - }), - }), - ); - - expect(response.status).toBe(200); - await expect(response.json()).resolves.toMatchObject({ - state: "ready", - tiles: [ - { - state: "ready", - tile: { - kind: "status", - stripId: "footer-status", - id: "auto-refresh", - }, - }, - ], - }); - }); - - test("rejects non-post dashboard tile batch requests", async () => { - const response = await handleRequest( - new Request("https://example.test/api/dashboard/tiles", { - method: "GET", - }), - ); - - expect(response.status).toBe(405); - expect(response.headers.get("allow")).toBe("POST"); - }); -}); diff --git a/apps/web/src/server/index.ts b/apps/web/src/server/index.ts index cf624ed..ba2135f 100644 --- a/apps/web/src/server/index.ts +++ b/apps/web/src/server/index.ts @@ -1,10 +1,6 @@ import { extname, normalize } from "node:path"; import { handleAgentDashboardRoute } from "./routes/agent-dashboard"; -import { - handleDashboardRoute, - handleDashboardTileRoute, - handleDashboardTilesRoute, -} from "./routes/dashboard"; +import { handleDashboardRoute, handleDashboardTileRoute } from "./routes/dashboard"; const host = process.env.HOST || "0.0.0.0"; const port = Number(process.env.PORT || 3000); @@ -27,11 +23,6 @@ export async function handleRequest(request: Request): Promise { return handleDashboardRoute(); } - if (url.pathname === "/api/dashboard/tiles") { - if (request.method !== "POST") return methodNotAllowed(["POST"]); - return handleDashboardTilesRoute(request); - } - 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..d40e263 100644 --- a/apps/web/src/server/routes/dashboard.test.ts +++ b/apps/web/src/server/routes/dashboard.test.ts @@ -3,10 +3,8 @@ import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; import { createDashboardTileCache, dashboardTileCacheKey, - handleDashboardTilesRoute, handleDashboardTileRoute, loadDashboardResponse, - loadDashboardTilesResponse, loadDashboardTileResponse, type DashboardTileResolutionLogEvent, } from "./dashboard"; @@ -565,115 +563,6 @@ describe("dashboard API route", () => { ); }); - test("serves batch tile route responses", async () => { - const response = await handleDashboardTilesRoute( - new Request("https://example.test/api/dashboard/tiles", { - method: "POST", - body: JSON.stringify({ - tiles: [ - { - kind: "status", - stripId: "footer-status", - id: "auto-refresh", - }, - ], - }), - }), - { - refreshSeedDocument: true, - seedIfEmpty: true, - tileCache: createDashboardTileCache(), - }, - ); - - expect(response.status).toBe(200); - expect(response.headers.get("cache-control")).toBe( - "private, max-age=5, stale-while-revalidate=30", - ); - await expect(response.json()).resolves.toMatchObject({ - state: "ready", - tiles: [ - { - state: "ready", - tile: { - kind: "status", - stripId: "footer-status", - id: "auto-refresh", - }, - }, - ], - }); - }); - - test("rejects invalid batch tile requests", async () => { - const response = await handleDashboardTilesRoute( - new Request("https://example.test/api/dashboard/tiles", { - method: "POST", - body: JSON.stringify({ - tiles: [{ kind: "service", id: "missing-group" }], - }), - }), - ); - - expect(response.status).toBe(400); - }); - - test("resolves batch tile responses with server concurrency capped at six", async () => { - let active = 0; - let maxActive = 0; - const started: string[] = []; - const releases = new Map void>(); - const service = dimensionLabDashboardFixture.serviceGroups - .flatMap((group) => group.services)[0]; - if (!service) throw new Error("missing service fixture"); - const tiles = Array.from({ length: 7 }, (_, index) => ({ - kind: "service" as const, - groupId: "essentials", - id: `service-${index}`, - })); - - const batch = loadDashboardTilesResponse(tiles, { - refreshSeedDocument: true, - seedIfEmpty: true, - tileCache: { - async resolve(key) { - active += 1; - maxActive = Math.max(maxActive, active); - started.push(key); - await new Promise((resolve) => releases.set(key, resolve)); - active -= 1; - - return { - cache: "miss", - coalesced: false, - response: { - state: "ready", - tile: JSON.parse(key), - item: service, - }, - }; - }, - }, - }); - - await waitFor(() => started.length === 6); - expect(maxActive).toBe(6); - releases.get(started[0])?.(); - await waitFor(() => started.length === 7); - for (const release of releases.values()) release(); - - await expect(batch).resolves.toMatchObject({ - state: "ready", - tiles: expect.arrayContaining([ - expect.objectContaining({ - state: "ready", - tile: tiles[0], - }), - ]), - }); - expect(maxActive).toBe(6); - }); - test("does not hydrate tile routes when live datasources are disabled", async () => { const previous = process.env.DISABLE_LIVE_DATASOURCES; process.env.DISABLE_LIVE_DATASOURCES = "1"; @@ -709,15 +598,6 @@ function jsonResponse(payload: unknown): Response { }); } -async function waitFor(predicate: () => boolean) { - for (let attempt = 0; attempt < 20; attempt += 1) { - if (predicate()) return; - await Promise.resolve(); - } - - throw new Error("condition was not met"); -} - function telemetryFetch() { return vi.fn(async (input: RequestInfo | URL) => { const url = String(input); diff --git a/apps/web/src/server/routes/dashboard.ts b/apps/web/src/server/routes/dashboard.ts index 2917689..42c4088 100644 --- a/apps/web/src/server/routes/dashboard.ts +++ b/apps/web/src/server/routes/dashboard.ts @@ -43,8 +43,6 @@ const dashboardTileResponseHeaders = { "Cache-Control": "private, max-age=5, stale-while-revalidate=30", }; -const dashboardBatchTileConcurrency = 6; - const defaultDashboardTileCache = createDashboardTileCache(); interface DashboardTileCacheResult { @@ -62,11 +60,6 @@ export interface DashboardTileResolutionLogEvent { tileKey: string; } -export interface DashboardTilesBatchResponse { - state: "ready"; - tiles: DashboardTileResolution[]; -} - export function createDashboardTileCache(): DashboardTileCache { const entries = new Map(); const inFlight = new Map>(); @@ -243,16 +236,6 @@ export async function loadDashboardTileResponse( } } -export async function loadDashboardTilesResponse( - tiles: DashboardTileReference[], - options: LoadDashboardResponseOptions = {}, -): Promise { - return { - state: "ready", - tiles: await resolveDashboardTilesBatch(tiles, options), - }; -} - export async function handleDashboardRoute(): Promise { return Response.json(await loadDashboardResponse()); } @@ -276,23 +259,6 @@ export async function handleDashboardTileRoute( }); } -export async function handleDashboardTilesRoute( - request: Request, - options: LoadDashboardResponseOptions = {}, -): Promise { - const tiles = await parseDashboardTilesBatchRequest(request); - if (!tiles) { - return Response.json( - { ok: false, message: "Invalid dashboard tiles request" }, - { status: 400 }, - ); - } - - return Response.json(await loadDashboardTilesResponse(tiles, options), { - headers: dashboardTileResponseHeaders, - }); -} - export function dashboardTileCacheKey(tile: DashboardTileReference): string { return JSON.stringify(tile); } @@ -330,98 +296,6 @@ function dashboardTileErrorCategory(error: unknown): string { return "unknown"; } -async function resolveDashboardTilesBatch( - tiles: DashboardTileReference[], - options: LoadDashboardResponseOptions, -): Promise { - const results: DashboardTileResolution[] = new Array(tiles.length); - let nextIndex = 0; - - async function worker() { - while (nextIndex < tiles.length) { - const index = nextIndex; - nextIndex += 1; - results[index] = await loadDashboardTileResponse(tiles[index], options); - } - } - - await Promise.all( - Array.from( - { length: Math.min(dashboardBatchTileConcurrency, tiles.length) }, - () => worker(), - ), - ); - - return results; -} - -async function parseDashboardTilesBatchRequest( - request: Request, -): Promise { - try { - const body = await request.json() as unknown; - if ( - typeof body !== "object" || - body === null || - !("tiles" in body) || - !Array.isArray(body.tiles) - ) { - return null; - } - - const tiles = body.tiles.map(parseDashboardTileReference); - return tiles.every((tile): tile is DashboardTileReference => tile !== null) - ? tiles - : null; - } catch { - return null; - } -} - -function parseDashboardTileReference(value: unknown): DashboardTileReference | null { - if (typeof value !== "object" || value === null || !("kind" in value)) { - return null; - } - - if ( - (value.kind === "telemetry" || value.kind === "module") && - "id" in value && - typeof value.id === "string" - ) { - return { kind: value.kind, id: value.id }; - } - - if ( - value.kind === "service" && - "groupId" in value && - typeof value.groupId === "string" && - "id" in value && - typeof value.id === "string" - ) { - return { - kind: "service", - groupId: value.groupId, - id: value.id, - }; - } - - if ( - value.kind === "status" && - "stripId" in value && - typeof value.stripId === "string" && - "id" in value && - typeof value.id === "string" - ) { - return { - kind: "status", - stripId: value.stripId, - id: value.id, - }; - } - - return null; -} - class DashboardTileCacheResolutionError extends Error { readonly cache: "hit" | "miss"; readonly coalesced: boolean;