Compare commits
No commits in common. "9aa30b07db04ca457969996dfb4879b948edaa40" and "cda40401caf4d30dd85dce0d77e537e6a20614ca" have entirely different histories.
9aa30b07db
...
cda40401ca
2 changed files with 4 additions and 386 deletions
|
|
@ -1,12 +1,6 @@
|
||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
|
import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
|
||||||
import {
|
import { loadDashboardResponse, loadDashboardTileResponse } from "./dashboard";
|
||||||
createDashboardTileCache,
|
|
||||||
dashboardTileCacheKey,
|
|
||||||
handleDashboardTileRoute,
|
|
||||||
loadDashboardResponse,
|
|
||||||
loadDashboardTileResponse,
|
|
||||||
} from "./dashboard";
|
|
||||||
|
|
||||||
describe("dashboard API route", () => {
|
describe("dashboard API route", () => {
|
||||||
test("returns the ready dashboard shell without hydrating live datasources", async () => {
|
test("returns the ready dashboard shell without hydrating live datasources", async () => {
|
||||||
|
|
@ -151,243 +145,6 @@ describe("dashboard API route", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("caches ready tile responses until the tile ttl expires", async () => {
|
|
||||||
const cache = createDashboardTileCache();
|
|
||||||
let now = 1_000;
|
|
||||||
const fetch = vi.fn(async () =>
|
|
||||||
jsonResponse({
|
|
||||||
status: "UP",
|
|
||||||
ping: 42,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
|
|
||||||
|
|
||||||
const first = await loadDashboardTileResponse(tile, {
|
|
||||||
fetch,
|
|
||||||
now: () => now,
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
});
|
|
||||||
const second = await loadDashboardTileResponse(tile, {
|
|
||||||
fetch,
|
|
||||||
now: () => now + 29_999,
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
});
|
|
||||||
now += 30_001;
|
|
||||||
const third = await loadDashboardTileResponse(tile, {
|
|
||||||
fetch,
|
|
||||||
now: () => now,
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(first).toEqual(second);
|
|
||||||
expect(third.state).toBe("ready");
|
|
||||||
expect(fetch).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("keeps telemetry tiles cached for fifteen seconds", async () => {
|
|
||||||
const cache = createDashboardTileCache();
|
|
||||||
let now = 1_000;
|
|
||||||
const fetch = telemetryFetch();
|
|
||||||
const tile = { kind: "telemetry", id: "infra-ram" } as const;
|
|
||||||
|
|
||||||
await loadDashboardTileResponse(tile, {
|
|
||||||
fetch,
|
|
||||||
now: () => now,
|
|
||||||
prometheusBaseUrl: "https://prometheus.example",
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
});
|
|
||||||
await loadDashboardTileResponse(tile, {
|
|
||||||
fetch,
|
|
||||||
now: () => now + 14_999,
|
|
||||||
prometheusBaseUrl: "https://prometheus.example",
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
});
|
|
||||||
now += 15_001;
|
|
||||||
await loadDashboardTileResponse(tile, {
|
|
||||||
fetch,
|
|
||||||
now: () => now,
|
|
||||||
prometheusBaseUrl: "https://prometheus.example",
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledTimes(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("keeps weather module tiles cached for ten minutes", async () => {
|
|
||||||
const cache = createDashboardTileCache();
|
|
||||||
let now = 1_000;
|
|
||||||
const fetch = vi.fn(async () =>
|
|
||||||
jsonResponse({
|
|
||||||
current: {
|
|
||||||
apparent_temperature: 19,
|
|
||||||
temperature_2m: 20,
|
|
||||||
weather_code: 0,
|
|
||||||
wind_speed_10m: 11,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const tile = { kind: "module", id: "weather-amsterdam" } as const;
|
|
||||||
|
|
||||||
await loadDashboardTileResponse(tile, {
|
|
||||||
fetch,
|
|
||||||
now: () => now,
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
});
|
|
||||||
await loadDashboardTileResponse(tile, {
|
|
||||||
fetch,
|
|
||||||
now: () => now + 599_999,
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
});
|
|
||||||
now += 600_001;
|
|
||||||
await loadDashboardTileResponse(tile, {
|
|
||||||
fetch,
|
|
||||||
now: () => now,
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("coalesces concurrent tile requests for the same cache key", async () => {
|
|
||||||
const cache = createDashboardTileCache();
|
|
||||||
let resolveFetch: ((response: Response) => void) | undefined;
|
|
||||||
const fetch = vi.fn(() =>
|
|
||||||
new Promise<Response>((resolve) => {
|
|
||||||
resolveFetch = resolve;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
|
|
||||||
|
|
||||||
const first = loadDashboardTileResponse(tile, {
|
|
||||||
fetch,
|
|
||||||
now: () => 1_000,
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
});
|
|
||||||
const second = loadDashboardTileResponse(tile, {
|
|
||||||
fetch,
|
|
||||||
now: () => 1_000,
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.resolve();
|
|
||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
resolveFetch?.(jsonResponse({ status: "UP", ping: 42 }));
|
|
||||||
expect(await first).toEqual(await second);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("uses structured tile cache keys when identifiers contain delimiters", () => {
|
|
||||||
expect(
|
|
||||||
dashboardTileCacheKey({ kind: "service", groupId: "a:b", id: "c" }),
|
|
||||||
).not.toBe(
|
|
||||||
dashboardTileCacheKey({ kind: "service", groupId: "a", id: "b:c" }),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
dashboardTileCacheKey({ kind: "status", stripId: "a:b", id: "c" }),
|
|
||||||
).not.toBe(
|
|
||||||
dashboardTileCacheKey({ kind: "status", stripId: "a", id: "b:c" }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("uses thirty-second status aggregate and five-minute static status ttl buckets", async () => {
|
|
||||||
const cache = createDashboardTileCache();
|
|
||||||
const fetch = vi.fn(async () => jsonResponse({ status: "UP", ping: 42 }));
|
|
||||||
const serviceCheckCount = dimensionLabDashboardFixture.serviceGroups
|
|
||||||
.flatMap((group) => group.services).length;
|
|
||||||
|
|
||||||
await loadDashboardTileResponse(
|
|
||||||
{ kind: "status", stripId: "footer-status", id: "system-status" },
|
|
||||||
{
|
|
||||||
fetch,
|
|
||||||
now: () => 1_000,
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await loadDashboardTileResponse(
|
|
||||||
{ kind: "status", stripId: "footer-status", id: "system-status" },
|
|
||||||
{
|
|
||||||
fetch,
|
|
||||||
now: () => 31_001,
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await loadDashboardTileResponse(
|
|
||||||
{ kind: "status", stripId: "footer-status", id: "auto-refresh" },
|
|
||||||
{
|
|
||||||
fetch,
|
|
||||||
now: () => 1_000,
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await loadDashboardTileResponse(
|
|
||||||
{ kind: "status", stripId: "footer-status", id: "auto-refresh" },
|
|
||||||
{
|
|
||||||
fetch,
|
|
||||||
now: () => 300_999,
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await loadDashboardTileResponse(
|
|
||||||
{ kind: "status", stripId: "footer-status", id: "auto-refresh" },
|
|
||||||
{
|
|
||||||
fetch,
|
|
||||||
now: () => 301_001,
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: cache,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledTimes(serviceCheckCount * 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("serves tile route responses with short private cache headers", async () => {
|
|
||||||
const response = await handleDashboardTileRoute(
|
|
||||||
"/api/dashboard/tile/service/essentials/vaultwarden",
|
|
||||||
{
|
|
||||||
fetch: vi.fn(async () => jsonResponse({ status: "UP", ping: 42 })),
|
|
||||||
refreshSeedDocument: true,
|
|
||||||
seedIfEmpty: true,
|
|
||||||
tileCache: createDashboardTileCache(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response.headers.get("cache-control")).toBe(
|
|
||||||
"private, max-age=5, stale-while-revalidate=30",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does not hydrate tile routes when live datasources are disabled", async () => {
|
test("does not hydrate tile routes when live datasources are disabled", async () => {
|
||||||
const previous = process.env.DISABLE_LIVE_DATASOURCES;
|
const previous = process.env.DISABLE_LIVE_DATASOURCES;
|
||||||
process.env.DISABLE_LIVE_DATASOURCES = "1";
|
process.env.DISABLE_LIVE_DATASOURCES = "1";
|
||||||
|
|
@ -422,43 +179,3 @@ function jsonResponse(payload: unknown): Response {
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function telemetryFetch() {
|
|
||||||
return vi.fn(async (input: RequestInfo | URL) => {
|
|
||||||
const url = String(input);
|
|
||||||
|
|
||||||
if (url.startsWith("https://prometheus.example/api/v1/query_range")) {
|
|
||||||
return jsonResponse({
|
|
||||||
status: "success",
|
|
||||||
data: {
|
|
||||||
result: [
|
|
||||||
{
|
|
||||||
metric: { host: "linux-infra" },
|
|
||||||
values: [
|
|
||||||
[1771430000, "10"],
|
|
||||||
[1771430060, "20"],
|
|
||||||
[1771430120, "42"],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.startsWith("https://prometheus.example/api/v1/query")) {
|
|
||||||
return jsonResponse({
|
|
||||||
status: "success",
|
|
||||||
data: {
|
|
||||||
result: [
|
|
||||||
{
|
|
||||||
metric: { host: "linux-infra" },
|
|
||||||
value: [1771430400, "42"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unhandled test request: ${url}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import type { DashboardDocument } from "@dimensionlab/dashboard-model";
|
|
||||||
import {
|
import {
|
||||||
loadDashboardRuntime,
|
loadDashboardRuntime,
|
||||||
type DashboardRuntimeOptions,
|
type DashboardRuntimeOptions,
|
||||||
|
|
@ -20,63 +19,6 @@ export interface LoadDashboardResponseOptions
|
||||||
DatasourceResolutionOptions {
|
DatasourceResolutionOptions {
|
||||||
disableLiveDatasources?: boolean;
|
disableLiveDatasources?: boolean;
|
||||||
hydrateLiveDatasources?: boolean;
|
hydrateLiveDatasources?: boolean;
|
||||||
now?: () => number;
|
|
||||||
tileCache?: DashboardTileCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DashboardTileCacheEntry {
|
|
||||||
expiresAt: number;
|
|
||||||
response: DashboardTileResolution;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DashboardTileCache {
|
|
||||||
resolve(
|
|
||||||
key: string,
|
|
||||||
ttlMs: number,
|
|
||||||
now: number,
|
|
||||||
load: () => Promise<DashboardTileResolution>,
|
|
||||||
): Promise<DashboardTileResolution>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dashboardTileResponseHeaders = {
|
|
||||||
"Cache-Control": "private, max-age=5, stale-while-revalidate=30",
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultDashboardTileCache = createDashboardTileCache();
|
|
||||||
|
|
||||||
export function createDashboardTileCache(): DashboardTileCache {
|
|
||||||
const entries = new Map<string, DashboardTileCacheEntry>();
|
|
||||||
const inFlight = new Map<string, Promise<DashboardTileResolution>>();
|
|
||||||
|
|
||||||
return {
|
|
||||||
async resolve(key, ttlMs, now, load) {
|
|
||||||
const cached = entries.get(key);
|
|
||||||
if (cached && cached.expiresAt > now) {
|
|
||||||
return cached.response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const active = inFlight.get(key);
|
|
||||||
if (active) return active;
|
|
||||||
|
|
||||||
const request = load()
|
|
||||||
.then((response) => {
|
|
||||||
if (response.state === "ready") {
|
|
||||||
entries.set(key, {
|
|
||||||
expiresAt: now + ttlMs,
|
|
||||||
response,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
inFlight.delete(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
inFlight.set(key, request);
|
|
||||||
return request;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadDashboardResponse(
|
export async function loadDashboardResponse(
|
||||||
|
|
@ -146,25 +88,14 @@ export async function loadDashboardTileResponse(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const cache = options.tileCache || defaultDashboardTileCache;
|
return resolveDashboardTile(dashboard.document, tile, options);
|
||||||
const now = options.now?.() ?? Date.now();
|
|
||||||
|
|
||||||
return cache.resolve(
|
|
||||||
dashboardTileCacheKey(tile),
|
|
||||||
dashboardTileTtlMs(dashboard.document, tile),
|
|
||||||
now,
|
|
||||||
() => resolveDashboardTile(dashboard.document, tile, options),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleDashboardRoute(): Promise<Response> {
|
export async function handleDashboardRoute(): Promise<Response> {
|
||||||
return Response.json(await loadDashboardResponse());
|
return Response.json(await loadDashboardResponse());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleDashboardTileRoute(
|
export async function handleDashboardTileRoute(pathname: string): Promise<Response> {
|
||||||
pathname: string,
|
|
||||||
options: LoadDashboardResponseOptions = {},
|
|
||||||
): Promise<Response> {
|
|
||||||
const tile = parseDashboardTilePath(pathname);
|
const tile = parseDashboardTilePath(pathname);
|
||||||
if (!tile) {
|
if (!tile) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
|
|
@ -173,42 +104,12 @@ export async function handleDashboardTileRoute(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await loadDashboardTileResponse(tile, options);
|
const response = await loadDashboardTileResponse(tile);
|
||||||
return Response.json(response, {
|
return Response.json(response, {
|
||||||
status: response.state === "not_found" ? 404 : 200,
|
status: response.state === "not_found" ? 404 : 200,
|
||||||
headers: dashboardTileResponseHeaders,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dashboardTileCacheKey(tile: DashboardTileReference): string {
|
|
||||||
return JSON.stringify(tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dashboardTileTtlMs(
|
|
||||||
document: DashboardDocument,
|
|
||||||
tile: DashboardTileReference,
|
|
||||||
): number {
|
|
||||||
if (tile.kind === "telemetry") return 15_000;
|
|
||||||
if (tile.kind === "service") return 30_000;
|
|
||||||
|
|
||||||
if (tile.kind === "module") {
|
|
||||||
const module = document.modules?.find((item) => item.id === tile.id);
|
|
||||||
if (
|
|
||||||
module?.datasource?.type === "external" &&
|
|
||||||
module.datasource.adapter === "weather"
|
|
||||||
) {
|
|
||||||
return 10 * 60_000;
|
|
||||||
}
|
|
||||||
return 30_000;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (["system-status", "uptime", "load-avg"].includes(tile.id)) {
|
|
||||||
return 30_000;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 5 * 60_000;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseDashboardTilePath(pathname: string): DashboardTileReference | null {
|
function parseDashboardTilePath(pathname: string): DashboardTileReference | null {
|
||||||
const parts = pathname.split("/").filter(Boolean);
|
const parts = pathname.split("/").filter(Boolean);
|
||||||
const [, dashboard, tileRoot, kind, firstId, secondId] = parts;
|
const [, dashboard, tileRoot, kind, firstId, secondId] = parts;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue