258 lines
8.0 KiB
JavaScript
258 lines
8.0 KiB
JavaScript
/**
|
|
* 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);
|
|
}); |