new site
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user