import { createHash } from 'node:crypto';
import { config } from '../config.js';
interface CacheEntry<T> {
value: T;
createdAt: number;
}
/**
* In-memory LRU cache with TTL.
* Key is derived from embedding hash of the query.
*/
export class SemanticCache<T = string> {
private cache = new Map<string, CacheEntry<T>>();
private maxEntries: number;
private ttlMs: number;
constructor(opts?: { maxEntries?: number; ttlMs?: number }) {
this.maxEntries = opts?.maxEntries ?? config.cache.maxEntries;
this.ttlMs = opts?.ttlMs ?? config.cache.ttlMs;
}
/**
* Generate cache key from a Float32Array embedding.
*/
static keyFromEmbedding(embedding: Float32Array): string {
const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
return createHash('md5').update(buf).digest('hex');
}
/**
* Generate cache key from a string.
*/
static keyFromString(text: string): string {
return createHash('md5').update(text).digest('hex');
}
get(key: string): T | undefined {
const entry = this.cache.get(key);
if (!entry) return undefined;
// Check TTL
if (Date.now() - entry.createdAt > this.ttlMs) {
this.cache.delete(key);
return undefined;
}
// Move to end (most recently used) - Map preserves insertion order
this.cache.delete(key);
this.cache.set(key, entry);
return entry.value;
}
set(key: string, value: T): void {
// Delete first to update position
this.cache.delete(key);
// Evict oldest if at capacity
if (this.cache.size >= this.maxEntries) {
const oldest = this.cache.keys().next().value;
if (oldest !== undefined) this.cache.delete(oldest);
}
this.cache.set(key, { value, createdAt: Date.now() });
}
has(key: string): boolean {
return this.get(key) !== undefined;
}
clear(): void {
this.cache.clear();
}
get size(): number {
return this.cache.size;
}
}