import { TagCache, type CacheEvent, type CacheSetOptions, type TagCacheOptions } from "./memory.ts"; export type CacheLayerName = "data" | "component" | "page"; export interface CacheInspection { layers: Record["snapshot"]>>; recentEvents: Array; } export interface CacheCoordinatorOptions extends Omit { eventLimit?: number; onEvent?: (event: CacheEvent & { layer: CacheLayerName }) => void; } /** A request-lifetime cache: deduplicates work without leaking values between requests. */ export class RequestCache { private pending = new Map>(); getOrLoad(key: string, loader: () => V | Promise): Promise { const existing = this.pending.get(key); if (existing) return existing as Promise; const value = Promise.resolve().then(loader); this.pending.set(key, value); return value as Promise; } clear(): void { this.pending.clear(); } } /** Owns the three cross-request cache layers and creates isolated request caches. */ export class CacheCoordinator { readonly data: TagCache; readonly component: TagCache; readonly page: TagCache; private readonly events: Array = []; private readonly eventLimit: number; constructor(options: CacheCoordinatorOptions = {}) { const { eventLimit = 200, onEvent, ...cacheOptions } = options; this.eventLimit = Math.max(1, eventLimit); const create = (layer: CacheLayerName) => new TagCache({ ...cacheOptions, onEvent: (event) => { const item = { ...event, layer }; this.events.push(item); if (this.events.length > this.eventLimit) this.events.splice(0, this.events.length - this.eventLimit); onEvent?.(item); }, }); this.data = create("data"); this.component = create("component"); this.page = create("page"); } request(): RequestCache { return new RequestCache(); } layer(name: CacheLayerName): TagCache { return this[name]; } getOrLoad( layer: CacheLayerName, key: string, loader: () => V | Promise, options?: CacheSetOptions, ): Promise { return this.layer(layer).getOrLoad(key, loader, options) as Promise; } invalidateTags(tags: Iterable): number { return ( this.data.invalidateTags(tags) + this.component.invalidateTags(tags) + this.page.invalidateTags(tags) ); } inspect(): CacheInspection { return { layers: { data: this.data.snapshot(), component: this.component.snapshot(), page: this.page.snapshot(), }, recentEvents: this.events.map((event) => ({ ...event, tags: event.tags && [...event.tags] })), }; } clear(): void { this.data.clear(); this.component.clear(); this.page.clear(); } }