/** * Upstash Client-Side Cache Integration * Provides intelligent caching for API calls and dynamic content */ class UpstashClient { constructor() { this.cacheEndpoint = '/api/cache'; this.localCache = new Map(); this.pendingRequests = new Map(); } /** * Get data from cache or fetch if not available */ async get(key, fetcher, ttl = 3600) { // Check local memory cache first const localData = this.localCache.get(key); if (localData && Date.now() - localData.timestamp < localData.ttl * 1000) { console.log(`Local cache HIT: ${key}`); return localData.data; } // Check if request is already pending if (this.pendingRequests.has(key)) { console.log(`Waiting for pending request: ${key}`); return this.pendingRequests.get(key); } // Create promise for this request const promise = this._fetchWithCache(key, fetcher, ttl); this.pendingRequests.set(key, promise); try { const result = await promise; return result; } finally { this.pendingRequests.delete(key); } } async _fetchWithCache(key, fetcher, ttl) { try { // Try to get from Upstash cache const cached = await this._getFromUpstash(key); if (cached) { console.log(`Upstash cache HIT: ${key}`); // Store in local cache this.localCache.set(key, { data: cached, timestamp: Date.now(), ttl: ttl }); return cached; } console.log(`Cache MISS: ${key} - fetching fresh data`); // Fetch fresh data const freshData = await fetcher(); // Store in both Upstash and local cache await this._setInUpstash(key, freshData, ttl); this.localCache.set(key, { data: freshData, timestamp: Date.now(), ttl: ttl }); return freshData; } catch (error) { console.error(`Cache error for ${key}:`, error); // Fallback to fetcher on error return fetcher(); } } async _getFromUpstash(key) { try { const response = await fetch(`${this.cacheEndpoint}?action=get&key=${encodeURIComponent(key)}`); if (response.ok) { const result = await response.json(); return result.data; } } catch (error) { console.error('Upstash get error:', error); } return null; } async _setInUpstash(key, value, ttl) { try { await fetch(this.cacheEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'set', key: key, value: JSON.stringify(value), ttl: ttl }) }); } catch (error) { console.error('Upstash set error:', error); } } /** * Invalidate cache for a specific key or pattern */ async invalidate(pattern) { // Clear local cache if (pattern === '*') { this.localCache.clear(); } else { for (const [key] of this.localCache) { if (key.includes(pattern)) { this.localCache.delete(key); } } } // Invalidate Upstash cache try { await fetch('/api/cache/invalidate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pattern }) }); } catch (error) { console.error('Cache invalidation error:', error); } } /** * Preload data into cache */ async preload(key, fetcher, ttl = 3600) { // Don't wait for result this.get(key, fetcher, ttl).catch(console.error); } /** * Get cache statistics */ async getStats() { try { const response = await fetch('/api/cache/stats'); if (response.ok) { const stats = await response.json(); stats.localCacheSize = this.localCache.size; return stats; } } catch (error) { console.error('Stats error:', error); } return { localCacheSize: this.localCache.size }; } } // Initialize global cache client window.upstashCache = new UpstashClient(); // Intelligent prefetching based on user behavior document.addEventListener('DOMContentLoaded', () => { // Prefetch on link hover let prefetchedUrls = new Set(); document.querySelectorAll('a[href^="/"]').forEach(link => { link.addEventListener('mouseenter', () => { const href = link.getAttribute('href'); if (!prefetchedUrls.has(href)) { prefetchedUrls.add(href); // Prefetch the page content const cacheKey = `page:${href}`; window.upstashCache.preload(cacheKey, async () => { const response = await fetch(href); return response.text(); }, 3600); } }); }); // Cache API responses const originalFetch = window.fetch; window.fetch = function(url, options = {}) { // Only cache GET requests to our API if (typeof url === 'string' && url.startsWith('/api/') && (!options.method || options.method === 'GET') && !url.includes('/cache')) { const cacheKey = `api:${url}`; return window.upstashCache.get(cacheKey, () => originalFetch(url, options), 1800); } return originalFetch(url, options); }; // Performance monitoring if ('PerformanceObserver' in window) { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.entryType === 'navigation') { console.log('Page Load Performance:', { domContentLoaded: entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart, loadComplete: entry.loadEventEnd - entry.loadEventStart, totalTime: entry.loadEventEnd - entry.fetchStart }); // Report to analytics if needed if (window.gtag) { window.gtag('event', 'page_load_time', { value: Math.round(entry.loadEventEnd - entry.fetchStart), metric_name: 'load_time' }); } } } }); observer.observe({ entryTypes: ['navigation'] }); } // Cache warming for critical resources const criticalResources = [ '/odoo/', '/odoo-ia/', '/odoo-hosting/' ]; // Warm cache in idle time if ('requestIdleCallback' in window) { requestIdleCallback(() => { criticalResources.forEach(resource => { const cacheKey = `page:${resource}`; window.upstashCache.preload(cacheKey, async () => { const response = await fetch(resource); return response.text(); }, 7200); // 2 hours for directory pages }); }); } }); // Export for use in other scripts window.UpstashClient = UpstashClient; // Log cache status console.log('🚀 Upstash client-side caching initialized'); window.upstashCache.getStats().then(stats => { console.log('📊 Cache stats:', stats); });