Spaces:
Paused
Paused
| /** | |
| * LRU Cache Layer — FASE-08 LLM Proxy Advanced | |
| * | |
| * In-memory LRU cache for LLM prompt/response pairs. | |
| * Uses content hashing for cache keys to handle semantic deduplication. | |
| * Memory-optimized with byte-based limits. | |
| * | |
| * @module lib/cacheLayer | |
| */ | |
| import crypto from "crypto"; | |
| /** | |
| * @typedef {Object} CacheEntry | |
| * @property {string} key - Cache key (hash) | |
| * @property {*} value - Cached value | |
| * @property {number} createdAt - Timestamp | |
| * @property {number} ttl - TTL in ms | |
| * @property {number} size - Approximate size in bytes | |
| * @property {number} hits - Number of times this entry was accessed | |
| */ | |
| const DEFAULT_MAX_ENTRIES = 50; | |
| const DEFAULT_MAX_BYTES = 2 * 1024 * 1024; | |
| const DEFAULT_TTL = 300000; | |
| export class LRUCache { | |
| /** @type {Map<string, CacheEntry>} */ | |
| #cache = new Map(); | |
| #maxSize; | |
| #maxBytes; | |
| #defaultTTL; | |
| #currentSize = 0; | |
| #currentBytes = 0; | |
| #stats = { hits: 0, misses: 0, evictions: 0 }; | |
| /** | |
| * @param {Object} options | |
| * @param {number} [options.maxSize=50] - Max number of entries (reduced for memory) | |
| * @param {number} [options.maxBytes=2097152] - Max bytes (default: 2MB) | |
| * @param {number} [options.defaultTTL=300000] - Default TTL in ms (5 min) | |
| */ | |
| constructor(options: { maxSize?: number; maxBytes?: number; defaultTTL?: number } = {}) { | |
| this.#maxSize = options.maxSize ?? DEFAULT_MAX_ENTRIES; | |
| this.#maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; | |
| this.#defaultTTL = options.defaultTTL ?? DEFAULT_TTL; | |
| } | |
| /** | |
| * Generate a cache key from input. | |
| * @param {Object} params - Parameters to hash | |
| * @returns {string} Cache key | |
| */ | |
| static generateKey(params: Record<string, unknown>) { | |
| const normalized = JSON.stringify(params, Object.keys(params).sort()); | |
| return crypto.createHash("sha256").update(normalized).digest("hex").slice(0, 16); | |
| } | |
| /** | |
| * Get a value from the cache. | |
| * @param {string} key | |
| * @returns {*|undefined} | |
| */ | |
| get(key: string) { | |
| const entry = this.#cache.get(key); | |
| if (!entry) { | |
| this.#stats.misses++; | |
| return undefined; | |
| } | |
| if (Date.now() - entry.createdAt > entry.ttl) { | |
| this.#deleteEntry(key, entry); | |
| this.#stats.misses++; | |
| return undefined; | |
| } | |
| this.#cache.delete(key); | |
| entry.hits++; | |
| this.#cache.set(key, entry); | |
| this.#stats.hits++; | |
| return entry.value; | |
| } | |
| /** | |
| * Set a value in the cache. | |
| * @param {string} key | |
| * @param {*} value | |
| * @param {number} [ttl] - Override default TTL | |
| */ | |
| set(key: string, value: unknown, ttl?: number) { | |
| const entrySize = this.#estimateSize(value); | |
| if (this.#cache.has(key)) { | |
| const oldEntry = this.#cache.get(key)!; | |
| this.#currentBytes -= oldEntry.size || 0; | |
| this.#currentSize--; | |
| this.#cache.delete(key); | |
| } | |
| while ( | |
| (this.#currentSize >= this.#maxSize || this.#currentBytes + entrySize > this.#maxBytes) && | |
| this.#cache.size > 0 | |
| ) { | |
| const oldestKey = this.#cache.keys().next().value; | |
| const oldestEntry = this.#cache.get(oldestKey); | |
| if (oldestEntry) { | |
| this.#deleteEntry(oldestKey, oldestEntry); | |
| } | |
| this.#stats.evictions++; | |
| } | |
| const entry = { | |
| key, | |
| value, | |
| createdAt: Date.now(), | |
| ttl: ttl ?? this.#defaultTTL, | |
| size: entrySize, | |
| hits: 0, | |
| }; | |
| this.#cache.set(key, entry); | |
| this.#currentSize++; | |
| this.#currentBytes += entrySize; | |
| } | |
| /** | |
| * Estimate size of a value in bytes. | |
| */ | |
| #estimateSize(value: unknown): number { | |
| try { | |
| return JSON.stringify(value).length * 2; | |
| } catch { | |
| return 1024; | |
| } | |
| } | |
| /** | |
| * Delete an entry and update counters. | |
| */ | |
| #deleteEntry(key: string, entry: { size?: number }) { | |
| this.#cache.delete(key); | |
| this.#currentSize--; | |
| this.#currentBytes -= entry.size || 0; | |
| if (this.#currentBytes < 0) this.#currentBytes = 0; | |
| } | |
| /** | |
| * Check if a key exists (without promoting it). | |
| * @param {string} key | |
| * @returns {boolean} | |
| */ | |
| has(key: string) { | |
| const entry = this.#cache.get(key); | |
| if (!entry) return false; | |
| if (Date.now() - entry.createdAt > entry.ttl) { | |
| this.#deleteEntry(key, entry); | |
| return false; | |
| } | |
| return true; | |
| } | |
| /** | |
| * Delete a specific key. | |
| * @param {string} key | |
| * @returns {boolean} | |
| */ | |
| delete(key: string) { | |
| const entry = this.#cache.get(key); | |
| if (entry) { | |
| this.#deleteEntry(key, entry); | |
| return true; | |
| } | |
| return false; | |
| } | |
| /** Clear the entire cache. */ | |
| clear() { | |
| this.#cache.clear(); | |
| this.#currentSize = 0; | |
| this.#currentBytes = 0; | |
| } | |
| /** @returns {{ size: number, maxSize: number, bytes: number, maxBytes: number, hits: number, misses: number, evictions: number, hitRate: number }} */ | |
| getStats() { | |
| const total = this.#stats.hits + this.#stats.misses; | |
| return { | |
| size: this.#currentSize, | |
| maxSize: this.#maxSize, | |
| bytes: this.#currentBytes, | |
| maxBytes: this.#maxBytes, | |
| ...this.#stats, | |
| hitRate: total > 0 ? (this.#stats.hits / total) * 100 : 0, | |
| }; | |
| } | |
| } | |
| // ─── Prompt Cache Singleton ───────────────── | |
| let promptCache: LRUCache | null = null; | |
| /** | |
| * Get the global prompt cache instance. | |
| * @param {Object} [options] | |
| * @returns {LRUCache} | |
| */ | |
| export function getPromptCache( | |
| options?: { maxSize?: number; maxBytes?: number; defaultTTL?: number } & Record<string, unknown> | |
| ) { | |
| if (!promptCache) { | |
| promptCache = new LRUCache({ | |
| maxSize: parseInt(process.env.PROMPT_CACHE_MAX_SIZE || "50", 10), | |
| maxBytes: parseInt(process.env.PROMPT_CACHE_MAX_BYTES || String(2 * 1024 * 1024), 10), | |
| defaultTTL: parseInt(process.env.PROMPT_CACHE_TTL_MS || "300000", 10), | |
| ...options, | |
| }); | |
| } | |
| return promptCache; | |
| } | |