| 56 | * Simple LRU backed by `Map` (insertion-ordered) + promote-on-hit. |
| 57 | */ |
| 58 | export class LruEmbedCache implements EmbedCache { |
| 59 | private readonly map = new Map<string, EmbeddingVector>(); |
| 60 | private readonly maxItems: number; |
| 61 | private hits = 0; |
| 62 | private misses = 0; |
| 63 | private evictions = 0; |
| 64 | |
| 65 | constructor(maxItems: number) { |
| 66 | if (!Number.isFinite(maxItems) || maxItems < 0) { |
| 67 | throw new Error(`[embedding.cache] invalid maxItems: ${maxItems}`); |
| 68 | } |
| 69 | this.maxItems = Math.floor(maxItems); |
| 70 | } |
| 71 | |
| 72 | get(key: string): EmbeddingVector | undefined { |
| 73 | const v = this.map.get(key); |
| 74 | if (v === undefined) { |
| 75 | this.misses++; |
| 76 | return undefined; |
| 77 | } |
| 78 | // Promote: delete + re-set moves the entry to the "most recent" slot. |
| 79 | this.map.delete(key); |
| 80 | this.map.set(key, v); |
| 81 | this.hits++; |
| 82 | return v; |
| 83 | } |
| 84 | |
| 85 | set(key: string, vec: EmbeddingVector): void { |
| 86 | if (this.maxItems === 0) return; |
| 87 | if (this.map.has(key)) { |
| 88 | this.map.delete(key); |
| 89 | } |
| 90 | this.map.set(key, vec); |
| 91 | while (this.map.size > this.maxItems) { |
| 92 | const oldest = this.map.keys().next().value as string | undefined; |
| 93 | if (oldest === undefined) break; |
| 94 | this.map.delete(oldest); |
| 95 | this.evictions++; |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | has(key: string): boolean { |
| 100 | return this.map.has(key); |
| 101 | } |
| 102 | |
| 103 | clear(): void { |
| 104 | const hadSize = this.map.size; |
| 105 | this.map.clear(); |
| 106 | this.hits = 0; |
| 107 | this.misses = 0; |
| 108 | this.evictions = 0; |
| 109 | if (hadSize > 0) log.debug("cleared", { hadSize }); |
| 110 | } |
| 111 | |
| 112 | stats(): EmbedCacheStats { |
| 113 | return { |
| 114 | size: this.map.size, |
| 115 | maxItems: this.maxItems, |
nothing calls this directly
no outgoing calls
no test coverage detected