/** * Cache Manager Module * * Provides intelligent caching for API responses and computed values. * Features: * - Memory cache * - IndexedDB cache * - Cache invalidation strategies * - Cache warming * - Cache analytics * - Integration with RequestDeduplicator */ import { Logger } from '../../core/logger.js'; /** * Cache strategies */ export const CacheStrategy = { NO_CACHE: 'no-cache', CACHE_FIRST: 'cache-first', NETWORK_FIRST: 'network-first', STALE_WHILE_REVALIDATE: 'stale-while-revalidate', NETWORK_ONLY: 'network-only', CACHE_ONLY: 'cache-only' }; /** * CacheManager - Intelligent caching system */ export class CacheManager { constructor(config = {}) { this.config = { defaultStrategy: config.defaultStrategy || CacheStrategy.STALE_WHILE_REVALIDATE, defaultTTL: config.defaultTTL || 3600000, // 1 hour maxMemorySize: config.maxMemorySize || 50, // Max 50 items in memory enableIndexedDB: config.enableIndexedDB ?? true, indexedDBName: config.indexedDBName || 'app-cache', indexedDBVersion: config.indexedDBVersion || 1, enableAnalytics: config.enableAnalytics ?? false, ...config }; this.memoryCache = new Map(); // Map this.indexedDBCache = null; this.analytics = { hits: 0, misses: 0, sets: 0, deletes: 0 }; // Initialize this.init(); } /** * Create a new CacheManager instance */ static create(config = {}) { return new CacheManager(config); } /** * Initialize cache manager */ async init() { // Initialize IndexedDB if enabled if (this.config.enableIndexedDB && 'indexedDB' in window) { try { await this.initIndexedDB(); } catch (error) { Logger.error('[CacheManager] Failed to initialize IndexedDB', error); this.config.enableIndexedDB = false; } } Logger.info('[CacheManager] Initialized', { strategy: this.config.defaultStrategy, memoryCache: true, indexedDB: this.config.enableIndexedDB }); } /** * Initialize IndexedDB */ async initIndexedDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.config.indexedDBName, this.config.indexedDBVersion); request.onerror = () => reject(request.error); request.onsuccess = () => { this.indexedDBCache = request.result; resolve(); }; request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains('cache')) { db.createObjectStore('cache', { keyPath: 'key' }); } }; }); } /** * Get value from cache */ async get(key, options = {}) { const strategy = options.strategy || this.config.defaultStrategy; const ttl = options.ttl || this.config.defaultTTL; // Check memory cache first const memoryEntry = this.memoryCache.get(key); if (memoryEntry && !this.isExpired(memoryEntry, ttl)) { this.analytics.hits++; Logger.debug('[CacheManager] Cache hit (memory)', key); return memoryEntry.value; } // Check IndexedDB if enabled if (this.config.enableIndexedDB && this.indexedDBCache) { try { const indexedDBEntry = await this.getFromIndexedDB(key); if (indexedDBEntry && !this.isExpired(indexedDBEntry, ttl)) { // Promote to memory cache this.memoryCache.set(key, indexedDBEntry); this.analytics.hits++; Logger.debug('[CacheManager] Cache hit (IndexedDB)', key); return indexedDBEntry.value; } } catch (error) { Logger.error('[CacheManager] IndexedDB get error', error); } } this.analytics.misses++; Logger.debug('[CacheManager] Cache miss', key); return null; } /** * Set value in cache */ async set(key, value, options = {}) { const ttl = options.ttl || this.config.defaultTTL; const strategy = options.strategy || this.config.defaultStrategy; const entry = { key, value, timestamp: Date.now(), ttl, strategy }; // Store in memory cache this.memoryCache.set(key, entry); // Limit memory cache size if (this.memoryCache.size > this.config.maxMemorySize) { const firstKey = this.memoryCache.keys().next().value; this.memoryCache.delete(firstKey); } // Store in IndexedDB if enabled if (this.config.enableIndexedDB && this.indexedDBCache) { try { await this.setInIndexedDB(entry); } catch (error) { Logger.error('[CacheManager] IndexedDB set error', error); } } this.analytics.sets++; Logger.debug('[CacheManager] Cache set', key); } /** * Get or compute value (cache-aside pattern) */ async getOrSet(key, computeFn, options = {}) { const strategy = options.strategy || this.config.defaultStrategy; // Try cache first (unless network-only) if (strategy !== CacheStrategy.NETWORK_ONLY) { const cached = await this.get(key, options); if (cached !== null) { return cached; } } // Compute value const value = await computeFn(); // Store in cache (unless no-cache) if (strategy !== CacheStrategy.NO_CACHE) { await this.set(key, value, options); } return value; } /** * Delete value from cache */ async delete(key) { // Delete from memory this.memoryCache.delete(key); // Delete from IndexedDB if (this.config.enableIndexedDB && this.indexedDBCache) { try { await this.deleteFromIndexedDB(key); } catch (error) { Logger.error('[CacheManager] IndexedDB delete error', error); } } this.analytics.deletes++; Logger.debug('[CacheManager] Cache delete', key); } /** * Clear all cache */ async clear() { this.memoryCache.clear(); if (this.config.enableIndexedDB && this.indexedDBCache) { try { await this.clearIndexedDB(); } catch (error) { Logger.error('[CacheManager] IndexedDB clear error', error); } } Logger.info('[CacheManager] Cache cleared'); } /** * Invalidate cache by pattern */ async invalidate(pattern) { const keysToDelete = []; // Find matching keys in memory for (const key of this.memoryCache.keys()) { if (this.matchesPattern(key, pattern)) { keysToDelete.push(key); } } // Delete matching keys for (const key of keysToDelete) { await this.delete(key); } Logger.info('[CacheManager] Invalidated cache', { pattern, count: keysToDelete.length }); } /** * Check if key matches pattern */ matchesPattern(key, pattern) { if (typeof pattern === 'string') { return key.includes(pattern); } if (pattern instanceof RegExp) { return pattern.test(key); } if (typeof pattern === 'function') { return pattern(key); } return false; } /** * Check if cache entry is expired */ isExpired(entry, ttl) { if (!entry || !entry.timestamp) { return true; } const age = Date.now() - entry.timestamp; return age > (entry.ttl || ttl); } /** * Get from IndexedDB */ async getFromIndexedDB(key) { return new Promise((resolve, reject) => { const transaction = this.indexedDBCache.transaction(['cache'], 'readonly'); const store = transaction.objectStore('cache'); const request = store.get(key); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } /** * Set in IndexedDB */ async setInIndexedDB(entry) { return new Promise((resolve, reject) => { const transaction = this.indexedDBCache.transaction(['cache'], 'readwrite'); const store = transaction.objectStore('cache'); const request = store.put(entry); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } /** * Delete from IndexedDB */ async deleteFromIndexedDB(key) { return new Promise((resolve, reject) => { const transaction = this.indexedDBCache.transaction(['cache'], 'readwrite'); const store = transaction.objectStore('cache'); const request = store.delete(key); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } /** * Clear IndexedDB */ async clearIndexedDB() { return new Promise((resolve, reject) => { const transaction = this.indexedDBCache.transaction(['cache'], 'readwrite'); const store = transaction.objectStore('cache'); const request = store.clear(); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } /** * Warm cache (preload values) */ async warm(keys, computeFn) { Logger.info('[CacheManager] Warming cache', { count: keys.length }); for (const key of keys) { try { await this.getOrSet(key, () => computeFn(key)); } catch (error) { Logger.error(`[CacheManager] Failed to warm cache for ${key}`, error); } } } /** * Get cache analytics */ getAnalytics() { const total = this.analytics.hits + this.analytics.misses; const hitRate = total > 0 ? (this.analytics.hits / total) * 100 : 0; return { ...this.analytics, total, hitRate: Math.round(hitRate * 100) / 100, memorySize: this.memoryCache.size, memoryMaxSize: this.config.maxMemorySize }; } /** * Reset analytics */ resetAnalytics() { this.analytics = { hits: 0, misses: 0, sets: 0, deletes: 0 }; } /** * Destroy cache manager */ destroy() { this.memoryCache.clear(); this.indexedDBCache = null; Logger.info('[CacheManager] Destroyed'); } }