/** * Advanced caching utility for Vue 3 applications * Supports TTL, memory limits, persistence, and cache invalidation */ import { ref, computed, onUnmounted } from 'vue'; export interface CacheOptions { ttl?: number; // Time to live in milliseconds maxSize?: number; // Maximum cache size persistent?: boolean; // Store in localStorage keyPrefix?: string; // Prefix for localStorage keys } interface CacheEntry { data: T; timestamp: number; ttl?: number; hits: number; lastAccessed: number; } class CacheManager { private cache = new Map>(); private options: Required; private cleanupInterval?: number; constructor(options: CacheOptions = {}) { this.options = { ttl: 5 * 60 * 1000, // 5 minutes default maxSize: 100, persistent: false, keyPrefix: 'app_cache_', ...options }; // Setup cleanup interval for expired entries this.cleanupInterval = window.setInterval(() => { this.cleanup(); }, 60000); // Cleanup every minute // Load from localStorage if persistent if (this.options.persistent) { this.loadFromStorage(); } } /** * Store data in cache */ set(key: string, data: T, customTtl?: number): void { const now = Date.now(); const ttl = customTtl || this.options.ttl; // Remove oldest entries if cache is full if (this.cache.size >= this.options.maxSize) { this.evictLRU(); } const entry: CacheEntry = { data, timestamp: now, ttl, hits: 0, lastAccessed: now }; this.cache.set(key, entry); // Persist to localStorage if enabled if (this.options.persistent) { this.saveToStorage(key, entry); } } /** * Get data from cache */ get(key: string): T | null { const entry = this.cache.get(key); if (!entry) { return null; } const now = Date.now(); // Check if entry has expired if (entry.ttl && (now - entry.timestamp) > entry.ttl) { this.delete(key); return null; } // Update access statistics entry.hits++; entry.lastAccessed = now; return entry.data; } /** * Check if key exists and is not expired */ has(key: string): boolean { return this.get(key) !== null; } /** * Delete specific key */ delete(key: string): boolean { const deleted = this.cache.delete(key); if (deleted && this.options.persistent) { localStorage.removeItem(this.options.keyPrefix + key); } return deleted; } /** * Clear all cache entries */ clear(): void { if (this.options.persistent) { // Remove all persistent entries Object.keys(localStorage).forEach(key => { if (key.startsWith(this.options.keyPrefix)) { localStorage.removeItem(key); } }); } this.cache.clear(); } /** * Get cache statistics */ getStats() { const entries = Array.from(this.cache.values()); const now = Date.now(); return { size: this.cache.size, maxSize: this.options.maxSize, expired: entries.filter(e => e.ttl && (now - e.timestamp) > e.ttl).length, totalHits: entries.reduce((sum, e) => sum + e.hits, 0), averageAge: entries.length > 0 ? entries.reduce((sum, e) => sum + (now - e.timestamp), 0) / entries.length : 0 }; } /** * Remove expired entries */ private cleanup(): void { const now = Date.now(); const keysToDelete: string[] = []; this.cache.forEach((entry, key) => { if (entry.ttl && (now - entry.timestamp) > entry.ttl) { keysToDelete.push(key); } }); keysToDelete.forEach(key => this.delete(key)); } /** * Evict least recently used entry */ private evictLRU(): void { let oldestKey = ''; let oldestTime = Date.now(); this.cache.forEach((entry, key) => { if (entry.lastAccessed < oldestTime) { oldestTime = entry.lastAccessed; oldestKey = key; } }); if (oldestKey) { this.delete(oldestKey); } } /** * Save entry to localStorage */ private saveToStorage(key: string, entry: CacheEntry): void { try { const storageKey = this.options.keyPrefix + key; localStorage.setItem(storageKey, JSON.stringify(entry)); } catch (error) { console.warn('Failed to save cache entry to localStorage:', error); } } /** * Load cache from localStorage */ private loadFromStorage(): void { try { Object.keys(localStorage).forEach(storageKey => { if (storageKey.startsWith(this.options.keyPrefix)) { const key = storageKey.replace(this.options.keyPrefix, ''); const entryData = localStorage.getItem(storageKey); if (entryData) { const entry: CacheEntry = JSON.parse(entryData); const now = Date.now(); // Check if entry is still valid if (!entry.ttl || (now - entry.timestamp) <= entry.ttl) { this.cache.set(key, entry); } else { // Remove expired entry from storage localStorage.removeItem(storageKey); } } } }); } catch (error) { console.warn('Failed to load cache from localStorage:', error); } } /** * Cleanup resources */ destroy(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } } } // Global cache instances const apiCache = new CacheManager({ ttl: 5 * 60 * 1000, // 5 minutes maxSize: 50, persistent: true, keyPrefix: 'api_' }); const uiCache = new CacheManager({ ttl: 30 * 60 * 1000, // 30 minutes maxSize: 100, persistent: false, keyPrefix: 'ui_' }); /** * Composable for reactive caching */ export function useCache( key: string, fetcher: () => Promise, options: CacheOptions = {} ) { const cache = options.persistent !== false ? apiCache : uiCache; const data = ref(cache.get(key)); const isLoading = ref(false); const error = ref(null); const isStale = computed(() => { return data.value === null; }); const refresh = async (force = false): Promise => { if (isLoading.value) return data.value; if (!force && !isStale.value) { return data.value; } isLoading.value = true; error.value = null; try { const result = await fetcher(); cache.set(key, result, options.ttl); data.value = result; return result; } catch (err) { error.value = err instanceof Error ? err : new Error(String(err)); throw err; } finally { isLoading.value = false; } }; const invalidate = () => { cache.delete(key); data.value = null; }; // Auto-fetch if data is stale if (isStale.value) { refresh(); } // Cleanup on unmount onUnmounted(() => { // Cache persists beyond component lifecycle }); return { data: computed(() => data.value), isLoading: computed(() => isLoading.value), error: computed(() => error.value), isStale, refresh, invalidate }; } /** * Preload data into cache */ export async function preloadCache( key: string, fetcher: () => Promise, options: CacheOptions = {} ): Promise { const cache = options.persistent !== false ? apiCache : uiCache; if (!cache.has(key)) { try { const data = await fetcher(); cache.set(key, data, options.ttl); } catch (error) { console.warn(`Failed to preload cache for key: ${key}`, error); } } } /** * Cache invalidation utilities */ export const cacheUtils = { invalidatePattern: (pattern: string) => { [apiCache, uiCache].forEach(cache => { Array.from(cache['cache'].keys()).forEach(key => { if (key.includes(pattern)) { cache.delete(key); } }); }); }, invalidateAll: () => { apiCache.clear(); uiCache.clear(); }, getStats: () => ({ api: apiCache.getStats(), ui: uiCache.getStats() }) }; // Export instances for direct use export { apiCache, uiCache }; export default CacheManager;