lru-cache.ts 1.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. /**
  2. * Simple LRU cache backed by JavaScript's insertion-ordered Map.
  3. *
  4. * Used by ReferenceResolver to bound the per-resolver caches that
  5. * previously grew without limit and OOM'd on large codebases (20k+
  6. * files). Each cache is sized independently — see `index.ts` for
  7. * the chosen limits per cache type.
  8. *
  9. * Eviction is plain LRU: on `set`, if the cache is full, the
  10. * least-recently-used entry (the first one in iteration order) is
  11. * evicted. Touching via `get` moves the entry to the most-recently-used
  12. * position so hot keys survive eviction passes.
  13. */
  14. export class LRUCache<K, V> {
  15. private readonly max: number;
  16. private readonly store = new Map<K, V>();
  17. constructor(max: number) {
  18. if (!Number.isFinite(max) || max <= 0) {
  19. throw new Error(`LRUCache max must be a positive finite number, got ${max}`);
  20. }
  21. this.max = Math.floor(max);
  22. }
  23. get size(): number {
  24. return this.store.size;
  25. }
  26. get(key: K): V | undefined {
  27. const value = this.store.get(key);
  28. if (value === undefined) {
  29. // Distinguish "missing" from "stored undefined" by checking has().
  30. // We don't store undefined in practice, but be defensive.
  31. return this.store.has(key) ? value : undefined;
  32. }
  33. // Refresh recency by re-inserting.
  34. this.store.delete(key);
  35. this.store.set(key, value);
  36. return value;
  37. }
  38. has(key: K): boolean {
  39. return this.store.has(key);
  40. }
  41. set(key: K, value: V): void {
  42. if (this.store.has(key)) {
  43. this.store.delete(key);
  44. } else if (this.store.size >= this.max) {
  45. // Evict the oldest entry — first key in iteration order.
  46. const oldest = this.store.keys().next().value;
  47. if (oldest !== undefined) {
  48. this.store.delete(oldest);
  49. }
  50. }
  51. this.store.set(key, value);
  52. }
  53. clear(): void {
  54. this.store.clear();
  55. }
  56. }