LRU Cache With TTL Expiry
A generic LRU cache with per-entry TTL that evicts least recently used keys and reports hits, misses and evictions for observability.
Code
export class LruCache<T> {
private readonly store = new Map<string, { value: T; expiresAt: number }>();
private hits = 0;
private misses = 0;
private evictions = 0;
private readonly now: () => number;
private readonly capacity: number;
private readonly ttlMs: number;
constructor(options: { capacity: number; ttlMs: number; now?: () => number }) {
if (!Number.isInteger(options.capacity) || options.capacity < 1) {
throw new RangeError("capacity must be a positive integer");
}
if (!Number.isFinite(options.ttlMs) || options.ttlMs <= 0) {
throw new RangeError("ttlMs must be a positive number");
}
this.capacity = options.capacity;
this.ttlMs = options.ttlMs;
this.now = options.now ?? (() => Date.now());
}
get size(): number {
return this.store.size;
}
stats(): { hits: number; misses: number; evictions: number } {
return { hits: this.hits, misses: this.misses, evictions: this.evictions };
}
get(key: string): T | undefined {
const entry = this.store.get(key);
if (!entry) {
this.misses += 1;
return undefined;
}
if (this.now() >= entry.expiresAt) {
this.store.delete(key);
this.misses += 1;
return undefined;
}
// refresh recency
this.store.delete(key);
this.store.set(key, entry);
this.hits += 1;
return entry.value;
}
set(key: string, value: T): void {
if (this.store.has(key)) this.store.delete(key);
while (this.store.size >= this.capacity) {
const oldest = this.store.keys().next();
if (oldest.done) break;
this.store.delete(oldest.value);
this.evictions += 1;
}
this.store.set(key, { value, expiresAt: this.now() + this.ttlMs });
}
}Report a problem with this exampleOpens a pre-filled issue on GitHub.