/** * API Cache and Request Deduplication Manager * Optimizes frontend-backend communication by: * - Caching API responses * - Deduplicating simultaneous requests * - Reducing unnecessary network traffic */ class APICache { constructor() { this.cache = new Map(); this.pendingRequests = new Map(); this.defaultTTL = 5 * 60 * 1000; // 5 minutes default // Custom TTL for different endpoints this.ttlConfig = { "/api/products": 5 * 60 * 1000, // 5 min "/api/products/featured": 10 * 60 * 1000, // 10 min "/api/categories": 30 * 60 * 1000, // 30 min "/api/portfolio/projects": 10 * 60 * 1000, // 10 min "/api/blog/posts": 5 * 60 * 1000, // 5 min "/api/pages": 10 * 60 * 1000, // 10 min }; // Start periodic cleanup this.startCleanup(); } /** * Get cache key from URL */ getCacheKey(url) { return url; } /** * Get TTL for specific endpoint */ getTTL(url) { // Match base endpoint for (const [endpoint, ttl] of Object.entries(this.ttlConfig)) { if (url.startsWith(endpoint)) { return ttl; } } return this.defaultTTL; } /** * Check if cache entry is valid */ isValid(entry) { return entry && Date.now() - entry.timestamp < entry.ttl; } /** * Get from cache */ get(url) { const key = this.getCacheKey(url); const entry = this.cache.get(key); if (this.isValid(entry)) { console.log(`[Cache] HIT: ${url}`); return entry.data; } console.log(`[Cache] MISS: ${url}`); return null; } /** * Set cache entry */ set(url, data) { const key = this.getCacheKey(url); const ttl = this.getTTL(url); this.cache.set(key, { data, timestamp: Date.now(), ttl, }); console.log(`[Cache] SET: ${url} (TTL: ${ttl}ms)`); } /** * Clear specific cache entry */ clear(url) { const key = this.getCacheKey(url); this.cache.delete(key); console.log(`[Cache] CLEAR: ${url}`); } /** * Clear all cache */ clearAll() { this.cache.clear(); console.log("[Cache] CLEARED ALL"); } /** * Fetch with caching and deduplication */ async fetch(url, options = {}) { // Only cache GET requests if (options.method && options.method !== "GET") { return fetch(url, options); } // Check cache first const cached = this.get(url); if (cached) { return { json: async () => cached, ok: true, status: 200, fromCache: true, }; } // Check if request is already pending if (this.pendingRequests.has(url)) { console.log(`[Cache] DEDUP: ${url} - Waiting for pending request`); return this.pendingRequests.get(url); } // Make new request console.log(`[Cache] FETCH: ${url}`); const requestPromise = fetch(url, options) .then(async (response) => { if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); // Cache successful response this.set(url, data); // Remove from pending this.pendingRequests.delete(url); return { json: async () => data, ok: true, status: response.status, fromCache: false, }; }) .catch((error) => { // Remove from pending on error this.pendingRequests.delete(url); throw error; }); // Store as pending this.pendingRequests.set(url, requestPromise); return requestPromise; } /** * Periodic cleanup of expired entries */ startCleanup() { setInterval(() => { let removed = 0; for (const [key, entry] of this.cache.entries()) { if (!this.isValid(entry)) { this.cache.delete(key); removed++; } } if (removed > 0) { console.log(`[Cache] Cleanup: Removed ${removed} expired entries`); } }, 60000); // Run every minute } /** * Get cache statistics */ getStats() { return { size: this.cache.size, pending: this.pendingRequests.size, entries: Array.from(this.cache.entries()).map(([key, entry]) => ({ url: key, age: Date.now() - entry.timestamp, ttl: entry.ttl, valid: this.isValid(entry), })), }; } } // Create global instance window.apiCache = new APICache(); // Expose cache clearing for manual control window.clearAPICache = () => window.apiCache.clearAll();