Files
dockhand-upstream/src/lib/server/vulnerabilities-cache.ts
T
2026-07-11 15:31:52 +02:00

76 lines
3.0 KiB
TypeScript

/**
* Short-TTL per-environment cache for the aggregated vulnerability findings.
*
* Lives in its own module (importing only client-safe types) so that db.ts can
* invalidate it from inside saveVulnerabilityScan WITHOUT a circular import
* (vulnerabilities.ts imports db.ts). That way every writer of a scan row
* automatically refreshes the dashboard — no caller can forget.
*/
import { EMPTY_SUMMARY } from '../utils/vulnerability';
import type { Finding, VulnerabilitySummary } from '$lib/utils/vulnerability';
export const CACHE_TTL_MS = 30_000;
/** Per-env cap on memoized filter/sort views (bounds server memory). */
export const MAX_VIEWS = 8;
/** Cap on cached environments — bounds total memory when many envs are viewed.
* Least-recently-inserted env entry is evicted past this. */
export const MAX_ENVS = 16;
export interface AggregatedVulnerabilities {
findings: Finding[];
summary: VulnerabilitySummary;
}
export interface VulnerabilitiesMeta {
total: number;
summary: VulnerabilitySummary;
options: { images: string[]; containers: string[]; stacks: string[] };
}
/** Empty meta for the no-environment case — shared so the shape isn't re-typed. */
export const EMPTY_META: VulnerabilitiesMeta = {
total: 0,
summary: EMPTY_SUMMARY,
options: { images: [], containers: [], stacks: [] }
};
export interface CacheEntry {
at: number;
data: AggregatedVulnerabilities;
/** Memoized filtered+sorted arrays within this cache window, keyed by query. */
views: Map<string, Finding[]>;
/** Memoized meta (distinct filter options) — computed once per window. */
meta?: VulnerabilitiesMeta;
}
// Keyed by envId. The null-environment ("local"/default) is keyed as 0 since the
// dashboard aggregation only ever runs for a concrete numeric env id.
export const aggregateCache = new Map<number, CacheEntry>();
/** Collapse concurrent cold-start requests into one aggregation. */
export const inflight = new Map<number, Promise<AggregatedVulnerabilities>>();
/**
* Drop cached findings. Pass an env id to clear just that environment; pass
* nothing to clear all (e.g. a broad change where the affected env is unknown).
*/
/** Cache occupancy — for the metrics endpoint. `envs` = cached environments,
* `views` = total memoized filter/sort views, `inflight` = cold aggregations running. */
export function getVulnerabilitiesCacheStats(): { envs: number; views: number; inflight: number } {
let views = 0;
for (const entry of aggregateCache.values()) views += entry.views.size;
return { envs: aggregateCache.size, views, inflight: inflight.size };
}
export function invalidateVulnerabilitiesCache(envIdNum?: number | null): void {
if (envIdNum === undefined || envIdNum === null) {
aggregateCache.clear();
// Also drop in-flight aggregations: one racing a scan-save would otherwise
// resolve to pre-scan data and get installed with a fresh TTL, masking the
// new scan for a full window. Dropping it forces the next reader to re-run.
inflight.clear();
} else {
aggregateCache.delete(envIdNum);
inflight.delete(envIdNum);
}
}