Files
WRNexusJS/packages/cache/src/coordinator.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

101 lines
2.9 KiB
TypeScript

import { TagCache, type CacheEvent, type CacheSetOptions, type TagCacheOptions } from "./memory.ts";
export type CacheLayerName = "data" | "component" | "page";
export interface CacheInspection {
layers: Record<CacheLayerName, ReturnType<TagCache<unknown>["snapshot"]>>;
recentEvents: Array<CacheEvent & { layer: CacheLayerName }>;
}
export interface CacheCoordinatorOptions extends Omit<TagCacheOptions, "onEvent"> {
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<string, Promise<unknown>>();
getOrLoad<V>(key: string, loader: () => V | Promise<V>): Promise<V> {
const existing = this.pending.get(key);
if (existing) return existing as Promise<V>;
const value = Promise.resolve().then(loader);
this.pending.set(key, value);
return value as Promise<V>;
}
clear(): void {
this.pending.clear();
}
}
/** Owns the three cross-request cache layers and creates isolated request caches. */
export class CacheCoordinator {
readonly data: TagCache<unknown>;
readonly component: TagCache<unknown>;
readonly page: TagCache<unknown>;
private readonly events: Array<CacheEvent & { layer: CacheLayerName }> = [];
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<unknown>({
...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<unknown> {
return this[name];
}
getOrLoad<V>(
layer: CacheLayerName,
key: string,
loader: () => V | Promise<V>,
options?: CacheSetOptions,
): Promise<V> {
return this.layer(layer).getOrLoad(key, loader, options) as Promise<V>;
}
invalidateTags(tags: Iterable<string>): 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();
}
}