dimensionlab-website/apps/web/src/lib/client/dashboard-refresh.test.ts

498 lines
14 KiB
TypeScript

import { describe, expect, test } from "vitest";
import {
attachDashboardRefreshLifecycle,
collectVisibleDashboardModelIds,
createDashboardPerformanceMarks,
createDashboardRefreshDelay,
dashboardPerformanceMarks,
createDashboardTileSnapshotStore,
createDashboardTileBackoff,
createDashboardRequestAborter,
runDashboardHydrationQueue,
runDashboardHydrationBatchQueue,
runViewportAwareDashboardHydrationQueue,
shouldPauseDashboardRefresh,
splitDashboardHydrationItemsByVisibility,
waitForDashboardHydrationIdle,
type DashboardIntersectionEntry,
} 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"]);
});
test("limits tile hydration concurrency", async () => {
let active = 0;
let maxActive = 0;
const started: number[] = [];
const releases = new Map<number, () => void>();
const queue = runDashboardHydrationQueue({
concurrency: 2,
items: [1, 2, 3, 4],
signal: new AbortController().signal,
hydrate: async (item) => {
active += 1;
maxActive = Math.max(maxActive, active);
started.push(item);
await new Promise<void>((resolve) => releases.set(item, resolve));
active -= 1;
},
});
await waitFor(() => started.length === 2);
expect(started).toEqual([1, 2]);
expect(maxActive).toBe(2);
releases.get(1)?.();
await waitFor(() => started.length === 3);
expect(started).toEqual([1, 2, 3]);
expect(maxActive).toBe(2);
releases.get(2)?.();
releases.get(3)?.();
await waitFor(() => started.length === 4);
releases.get(4)?.();
await queue;
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> = [];
const hydration = runViewportAwareDashboardHydrationQueue({
collectVisibleModelIds: async () => new Set(["b"]),
concurrency: 1,
getModelId: (item) => item,
hydrate: async (item) => {
calls.push(item);
},
items: ["a", "b", "c"],
onAllItemsSettled: () => calls.push("all-settled"),
onVisibleItemsSettled: () => calls.push("visible-settled"),
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", "visible-settled", "idle"]);
idleReleases.shift()?.();
await hydration;
expect(calls).toEqual([
"b",
"visible-settled",
"idle",
"a",
"c",
"all-settled",
]);
});
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("marks dashboard performance milestones once per shell run", () => {
const marks: string[] = [];
const performanceMarks = createDashboardPerformanceMarks({
mark: (name) => marks.push(name),
});
performanceMarks.markShellLoad();
performanceMarks.markFirstTileReady();
performanceMarks.markFirstTileReady();
performanceMarks.markVisibleTilesReady();
performanceMarks.markAllTilesSettled();
performanceMarks.markShellLoad();
performanceMarks.markFirstTileReady();
expect(marks).toEqual([
dashboardPerformanceMarks.shellLoad,
dashboardPerformanceMarks.firstTileReady,
dashboardPerformanceMarks.visibleTilesReady,
dashboardPerformanceMarks.allTilesSettled,
dashboardPerformanceMarks.shellLoad,
dashboardPerformanceMarks.firstTileReady,
]);
});
test("backs off failed tile keys and resets after success", () => {
const backoff = createDashboardTileBackoff();
backoff.recordFailure("telemetry:infra-ram", 1_000);
expect(backoff.canAttempt("telemetry:infra-ram", 15_999)).toBe(false);
expect(backoff.canAttempt("telemetry:infra-ram", 16_000)).toBe(true);
backoff.recordFailure("telemetry:infra-ram", 16_000);
expect(backoff.canAttempt("telemetry:infra-ram", 45_999)).toBe(false);
expect(backoff.canAttempt("telemetry:infra-ram", 46_000)).toBe(true);
backoff.recordFailure("telemetry:infra-ram", 46_000);
backoff.recordFailure("telemetry:infra-ram", 106_000);
expect(backoff.canAttempt("telemetry:infra-ram", 225_999)).toBe(false);
expect(backoff.canAttempt("telemetry:infra-ram", 226_000)).toBe(true);
backoff.recordSuccess("telemetry:infra-ram");
expect(backoff.canAttempt("telemetry:infra-ram", 107_000)).toBe(true);
});
test("stores only ready tile snapshots and restores them for matching revisions", () => {
const storage = createMemoryStorage();
let now = 1_000;
const store = createDashboardTileSnapshotStore(storage, {
now: () => now,
});
store.saveReadyTile({
currentRevisionId: "revision-a",
response: {
state: "ready",
tile: { kind: "telemetry", id: "infra-ram" },
item: {
id: "infra-ram",
label: "Infra RAM",
value: { kind: "percent", value: 42 },
detail: "live",
severity: "ok",
},
},
schemaVersion: "dashboard.v1",
});
store.saveTile({
currentRevisionId: "revision-a",
response: {
state: "not_found",
tile: { kind: "telemetry", id: "missing" },
message: "Missing",
},
schemaVersion: "dashboard.v1",
});
store.saveTile({
currentRevisionId: "revision-a",
response: {
state: "disabled",
tile: { kind: "module", id: "disabled-module" },
message: "Disabled",
},
schemaVersion: "dashboard.v1",
});
now = 61_000;
const restored = store.restore({
currentRevisionId: "revision-a",
schemaVersion: "dashboard.v1",
});
expect(restored).toEqual([
{
ageMs: 60_000,
response: {
state: "ready",
tile: { kind: "telemetry", id: "infra-ram" },
item: {
id: "infra-ram",
label: "Infra RAM",
value: { kind: "percent", value: 42 },
detail: "live - stale 60s",
severity: "stale",
},
},
},
]);
expect(store.restore({
currentRevisionId: "revision-b",
schemaVersion: "dashboard.v1",
})).toEqual([]);
});
test("ignores malformed stored tile snapshots", () => {
const storage = createMemoryStorage();
storage.setItem(
"dimensionlab.dashboard.tiles.v1",
JSON.stringify({
currentRevisionId: "revision-a",
schemaVersion: "dashboard.v1",
tiles: [
{},
{
item: {
id: "incomplete",
detail: "live",
},
savedAt: 1_000,
tile: { kind: "telemetry", id: "incomplete" },
},
{
item: {
id: "infra-ram",
label: "Infra RAM",
value: { kind: "percent", value: 42 },
detail: "live",
severity: "ok",
},
savedAt: 1_000,
tile: { kind: "telemetry", id: "infra-ram" },
},
{
item: { id: "broken-detail", detail: 42 },
savedAt: 1_000,
tile: { kind: "telemetry", id: "broken-detail" },
},
],
version: 1,
}),
);
const store = createDashboardTileSnapshotStore(storage, {
now: () => 16_000,
});
expect(store.restore({
currentRevisionId: "revision-a",
schemaVersion: "dashboard.v1",
})).toEqual([
{
ageMs: 15_000,
response: {
state: "ready",
tile: { kind: "telemetry", id: "infra-ram" },
item: {
id: "infra-ram",
label: "Infra RAM",
value: { kind: "percent", value: 42 },
detail: "live - stale 15s",
severity: "stale",
},
},
},
]);
});
});
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 createMemoryStorage(): Storage {
const values = new Map<string, string>();
return {
get length() {
return values.size;
},
clear() {
values.clear();
},
getItem(key) {
return values.get(key) ?? null;
},
key(index) {
return [...values.keys()][index] ?? null;
},
removeItem(key) {
values.delete(key);
},
setItem(key, value) {
values.set(key, value);
},
};
}
function modelElement(modelId: string) {
return {
getAttribute(name: string) {
return name === "data-model-id" ? modelId : null;
},
};
}