perf(web): adapt dashboard refresh hydration

This commit is contained in:
vince 2026-06-20 14:47:53 +02:00
parent 30bff9da37
commit a071df3194
3 changed files with 502 additions and 16 deletions

View file

@ -1,11 +1,17 @@
import { describe, expect, test } from "vitest";
import {
attachDashboardRefreshLifecycle,
collectVisibleDashboardModelIds,
createDashboardRefreshDelay,
createDashboardTileSnapshotStore,
createDashboardTileBackoff,
createDashboardRequestAborter,
runDashboardHydrationQueue,
runViewportAwareDashboardHydrationQueue,
shouldPauseDashboardRefresh,
splitDashboardHydrationItemsByVisibility,
waitForDashboardHydrationIdle,
type DashboardIntersectionEntry,
} from "./dashboard-refresh";
describe("dashboard refresh lifecycle", () => {
@ -113,6 +119,134 @@ describe("dashboard refresh lifecycle", () => {
expect(maxActive).toBe(2);
});
test("hydrates visible items before deferred items and waits for idle", async () => {
const calls: string[] = [];
const idleReleases: Array<() => void> = [];
const hydration = runViewportAwareDashboardHydrationQueue({
collectVisibleModelIds: async () => new Set(["b"]),
concurrency: 1,
getModelId: (item) => item,
hydrate: async (item) => {
calls.push(item);
},
items: ["a", "b", "c"],
signal: new AbortController().signal,
waitForIdle: async () => {
calls.push("idle");
await new Promise<void>((resolve) => idleReleases.push(resolve));
},
});
await waitFor(() => calls.includes("idle"));
expect(calls).toEqual(["b", "idle"]);
idleReleases.shift()?.();
await hydration;
expect(calls).toEqual(["b", "idle", "a", "c"]);
});
test("splits visible hydration items while preserving document order", () => {
expect(splitDashboardHydrationItemsByVisibility({
getModelId: (item) => item.id,
items: [{ id: "status" }, { id: "telemetry" }, { id: "service" }],
visibleModelIds: new Set(["service", "status"]),
})).toEqual({
visible: [{ id: "status" }, { id: "service" }],
deferred: [{ id: "telemetry" }],
});
});
test("collects visible data-model-id elements with IntersectionObserver", async () => {
const elements = [
modelElement("status"),
modelElement("telemetry"),
modelElement("unrelated"),
];
let callback:
| ((entries: DashboardIntersectionEntry[]) => void)
| undefined;
let finishObservation: (() => void) | undefined;
let disconnected = false;
const observed: string[] = [];
const visible = collectVisibleDashboardModelIds({
createObserver: (observerCallback) => {
callback = observerCallback;
return {
disconnect() {
disconnected = true;
},
observe(element) {
observed.push(element.getAttribute("data-model-id") || "");
},
};
},
documentTarget: {
querySelectorAll: () => elements,
},
modelIds: ["status", "telemetry"],
setTimeout: (handler) => {
finishObservation = handler;
return 1 as unknown as ReturnType<typeof setTimeout>;
},
clearTimeout: () => undefined,
signal: new AbortController().signal,
});
callback?.([
{
isIntersecting: false,
intersectionRatio: 0,
target: elements[0],
},
{
isIntersecting: true,
target: elements[1],
},
]);
finishObservation?.();
expect(await visible).toEqual(new Set(["telemetry"]));
expect(observed).toEqual(["status", "telemetry"]);
expect(disconnected).toBe(true);
});
test("waits for requestIdleCallback when available", async () => {
let idleCallback: (() => void) | undefined;
let cancelledIdle: number | undefined;
const wait = waitForDashboardHydrationIdle({
cancelIdleCallback: (handle) => {
cancelledIdle = handle;
},
requestIdleCallback: (callback) => {
idleCallback = callback;
return 7;
},
signal: new AbortController().signal,
});
expect(cancelledIdle).toBeUndefined();
idleCallback?.();
await wait;
expect(cancelledIdle).toBe(7);
});
test("jitters refresh delays and slows repeated failures", () => {
const delay = createDashboardRefreshDelay({
jitterRatio: 0.1,
random: () => 1,
});
expect(delay.nextDelayMs(1_000)).toBe(1_100);
delay.recordFailure();
expect(delay.nextDelayMs(1_000)).toBe(2_200);
delay.recordFailure();
expect(delay.nextDelayMs(1_000)).toBe(4_400);
delay.recordSuccess();
expect(delay.nextDelayMs(1_000)).toBe(1_100);
});
test("backs off failed tile keys and resets after success", () => {
const backoff = createDashboardTileBackoff();
@ -299,3 +433,11 @@ function createMemoryStorage(): Storage {
},
};
}
function modelElement(modelId: string) {
return {
getAttribute(name: string) {
return name === "data-model-id" ? modelId : null;
},
};
}