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

301 lines
8.5 KiB
TypeScript

import { describe, expect, test } from "vitest";
import {
attachDashboardRefreshLifecycle,
createDashboardTileSnapshotStore,
createDashboardTileBackoff,
createDashboardRequestAborter,
runDashboardHydrationQueue,
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"]);
});
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("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);
},
};
}