206 lines
4.6 KiB
JavaScript
206 lines
4.6 KiB
JavaScript
|
|
/**
|
||
|
|
* 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();
|