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.

TypescriptData Fetching

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 });
  }
}