| 1 | interface CacheEntry<T> { |
| 2 | value: T; |
| 3 | approximateSize: number; |
| 4 | } |
| 5 | |
| 6 | export class LRUCache<T> { |
| 7 | private cache = new Map<string, CacheEntry<T>>(); |
| 8 | private totalSize = 0; |
| 9 | |
| 10 | constructor( |
| 11 | private readonly maxEntries: number, |
| 12 | private readonly maxMemoryBytes: number, |
| 13 | ) {} |
| 14 | |
| 15 | get(key: string): T | null { |
| 16 | const entry = this.cache.get(key); |
| 17 | if (!entry) { |
| 18 | return null; |
| 19 | } |
| 20 | |
| 21 | this.promote(key, entry); |
| 22 | return entry.value; |
| 23 | } |
| 24 | |
| 25 | set(key: string, value: T, approximateSize: number): void { |
| 26 | if (approximateSize > this.maxMemoryBytes) { |
| 27 | return; |
| 28 | } |
| 29 | |
| 30 | const existing = this.cache.get(key); |
| 31 | if (existing) { |
| 32 | this.totalSize -= existing.approximateSize; |
| 33 | this.cache.delete(key); |
| 34 | } |
| 35 | |
| 36 | this.evictIfNeeded(approximateSize); |
| 37 | this.cache.set(key, { value, approximateSize }); |
| 38 | this.totalSize += approximateSize; |
| 39 | } |
| 40 | |
| 41 | clear(): void { |
| 42 | this.cache.clear(); |
| 43 | this.totalSize = 0; |
| 44 | } |
| 45 | |
| 46 | private promote(key: string, entry: CacheEntry<T>): void { |
| 47 | this.cache.delete(key); |
| 48 | this.cache.set(key, entry); |
| 49 | } |
| 50 | |
| 51 | private evictIfNeeded(incomingSize: number): void { |
| 52 | while ( |
| 53 | (this.cache.size >= this.maxEntries || this.totalSize + incomingSize > this.maxMemoryBytes) && |
| 54 | this.cache.size > 0 |
| 55 | ) { |
| 56 | const oldestKey = this.cache.keys().next().value; |
| 57 | if (oldestKey === undefined) { |
| 58 | break; |
| 59 | } |
| 60 | |
| 61 | const oldestEntry = this.cache.get(oldestKey); |
| 62 | if (oldestEntry) { |
| 63 | this.totalSize -= oldestEntry.approximateSize; |
| 64 | } |
| 65 | this.cache.delete(oldestKey); |
| 66 | } |
| 67 | } |
| 68 | } |