perf(web): cache dashboard tile responses
This commit is contained in:
parent
cda40401ca
commit
bc1f64d566
2 changed files with 386 additions and 4 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import type { DashboardDocument } from "@dimensionlab/dashboard-model";
|
||||
import {
|
||||
loadDashboardRuntime,
|
||||
type DashboardRuntimeOptions,
|
||||
|
|
@ -19,6 +20,63 @@ export interface LoadDashboardResponseOptions
|
|||
DatasourceResolutionOptions {
|
||||
disableLiveDatasources?: 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(
|
||||
|
|
@ -88,14 +146,25 @@ export async function loadDashboardTileResponse(
|
|||
};
|
||||
}
|
||||
|
||||
return resolveDashboardTile(dashboard.document, tile, options);
|
||||
const cache = options.tileCache || defaultDashboardTileCache;
|
||||
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> {
|
||||
return Response.json(await loadDashboardResponse());
|
||||
}
|
||||
|
||||
export async function handleDashboardTileRoute(pathname: string): Promise<Response> {
|
||||
export async function handleDashboardTileRoute(
|
||||
pathname: string,
|
||||
options: LoadDashboardResponseOptions = {},
|
||||
): Promise<Response> {
|
||||
const tile = parseDashboardTilePath(pathname);
|
||||
if (!tile) {
|
||||
return Response.json(
|
||||
|
|
@ -104,12 +173,42 @@ export async function handleDashboardTileRoute(pathname: string): Promise<Respon
|
|||
);
|
||||
}
|
||||
|
||||
const response = await loadDashboardTileResponse(tile);
|
||||
const response = await loadDashboardTileResponse(tile, options);
|
||||
return Response.json(response, {
|
||||
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 {
|
||||
const parts = pathname.split("/").filter(Boolean);
|
||||
const [, dashboard, tileRoot, kind, firstId, secondId] = parts;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue