101 lines
2.9 KiB
TypeScript
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();
|
|
}
|
|
}
|