39 lines
1.4 KiB
TypeScript
39 lines
1.4 KiB
TypeScript
// Tiny in-memory TTL cache keyed by string. Status pages serve the same payload
|
|
// to many visitors during an outage; we don't want every page hit to fan out to
|
|
// Postgres. The cache is per-process; behind a load balancer each replica fills
|
|
// independently, which is fine - short TTLs converge quickly.
|
|
|
|
interface Entry<T> { value: T; expires: number }
|
|
|
|
const store = new Map<string, Entry<unknown>>();
|
|
|
|
export function cacheGet<T>(key: string): T | null {
|
|
const entry = store.get(key) as Entry<T> | undefined;
|
|
if (!entry) return null;
|
|
if (Date.now() > entry.expires) {
|
|
store.delete(key);
|
|
return null;
|
|
}
|
|
return entry.value;
|
|
}
|
|
|
|
export function cacheSet<T>(key: string, value: T, ttlSeconds: number): void {
|
|
// Soft cap so a runaway path can't blow memory. LRU-ish: oldest entries get
|
|
// dropped first by insertion order (Map preserves it).
|
|
if (store.size > 5000) {
|
|
const firstKey = store.keys().next().value;
|
|
if (firstKey) store.delete(firstKey);
|
|
}
|
|
store.set(key, { value, expires: Date.now() + ttlSeconds * 1000 });
|
|
}
|
|
|
|
// Convenience wrapper: get-or-fill. The producer runs at most once per key
|
|
// during the TTL window across this process.
|
|
export async function cached<T>(key: string, ttlSeconds: number, producer: () => Promise<T>): Promise<T> {
|
|
const hit = cacheGet<T>(key);
|
|
if (hit !== null) return hit;
|
|
const value = await producer();
|
|
cacheSet(key, value, ttlSeconds);
|
|
return value;
|
|
}
|