perf(web): restore last known dashboard tiles

This commit is contained in:
vince 2026-06-20 14:25:43 +02:00
parent 243d71686d
commit 4ce42fb52b
4 changed files with 412 additions and 8 deletions

View file

@ -1,6 +1,7 @@
import { describe, expect, test } from "vitest";
import {
attachDashboardRefreshLifecycle,
createDashboardTileSnapshotStore,
createDashboardTileBackoff,
createDashboardRequestAborter,
runDashboardHydrationQueue,
@ -131,6 +132,67 @@ describe("dashboard refresh lifecycle", () => {
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",
});
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([]);
});
});
async function waitFor(predicate: () => boolean) {
@ -141,3 +203,27 @@ async function waitFor(predicate: () => boolean) {
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);
},
};
}