Files
pikasTech-HWLAB/web/hwlab-cloud-web/src/utils/scoped-cache.ts
T
2026-06-30 18:39:37 +08:00

108 lines
2.7 KiB
TypeScript

// SPEC: PJ2026-0106050514 Workbench实时运行面 draft-2026-06-30-p0-1297-spec-first.
// Responsibility: LRU/TTL scoped cache for Workbench runtime objects. Mechanically migrated from OpenCode app/utils/scoped-cache.ts with policy supplied by caller.
export interface ScopedCacheOptions<T> {
maxEntries?: number | null;
ttlMs?: number | null;
dispose?: (value: T, key: string) => void;
now?: () => number;
}
interface Entry<T> {
value: T;
touchedAt: number;
}
export function createScopedCache<T>(createValue: (key: string) => T, options: ScopedCacheOptions<T> = {}) {
const store = new Map<string, Entry<T>>();
const now = options.now ?? Date.now;
const dispose = (key: string, entry: Entry<T>) => {
options.dispose?.(entry.value, key);
};
const expired = (entry: Entry<T>) => {
if (options.ttlMs === undefined || options.ttlMs === null) return false;
return now() - entry.touchedAt >= options.ttlMs;
};
const sweep = () => {
if (options.ttlMs === undefined || options.ttlMs === null) return;
for (const [key, entry] of store) {
if (!expired(entry)) continue;
store.delete(key);
dispose(key, entry);
}
};
const touch = (key: string, entry: Entry<T>) => {
entry.touchedAt = now();
store.delete(key);
store.set(key, entry);
};
const prune = () => {
if (options.maxEntries === undefined || options.maxEntries === null) return;
while (store.size > options.maxEntries) {
const key = store.keys().next().value as string | undefined;
if (!key) return;
const entry = store.get(key);
store.delete(key);
if (!entry) continue;
dispose(key, entry);
}
};
const remove = (key: string) => {
const entry = store.get(key);
if (!entry) return undefined;
store.delete(key);
dispose(key, entry);
return entry.value;
};
const peek = (key: string) => {
sweep();
const entry = store.get(key);
if (!entry) return undefined;
if (!expired(entry)) return entry.value;
store.delete(key);
dispose(key, entry);
return undefined;
};
const get = (key: string) => {
sweep();
const entry = store.get(key);
if (entry && !expired(entry)) {
touch(key, entry);
return entry.value;
}
if (entry) {
store.delete(key);
dispose(key, entry);
}
const created = {
value: createValue(key),
touchedAt: now()
};
store.set(key, created);
prune();
return created.value;
};
const clear = () => {
for (const [key, entry] of store) dispose(key, entry);
store.clear();
};
return {
get,
peek,
delete: remove,
clear,
size: () => store.size
};
}