UNPKG

@ai-growth/nextjs

Version:

Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering

741 lines (740 loc) 25.9 kB
/** * @fileoverview Advanced Cache Management System * * This module provides a comprehensive caching solution with multi-layer storage, * SWR patterns, cache invalidation, and performance monitoring for CMS content. */ // ============================================================================ // CACHE MANAGER CLASS // ============================================================================ /** * Advanced multi-layer cache manager */ export class CacheManager { constructor(config = {}) { this.memoryCache = new Map(); this.operationTimes = []; this.lastMetricsReset = Date.now(); this.config = { storage: 'memory', fallbackStorage: ['localStorage', 'sessionStorage'], defaultTTL: 30 * 60 * 1000, // 30 minutes defaultStaleTime: 5 * 60 * 1000, // 5 minutes maxSize: 1000, maxMemorySize: 50 * 1024 * 1024, // 50MB debug: false, enableMetrics: true, keyPrefix: 'cms-cache:', ...config, }; this.metrics = { totalOperations: 0, hits: 0, misses: 0, hitRatio: 0, averageResponseTime: 0, cacheSize: 0, memoryUsage: 0, operationsPerSecond: 0, }; // Initialize cleanup intervals this.startCleanupInterval(); this.startMetricsCalculation(); } // ============================================================================ // CORE CACHE OPERATIONS // ============================================================================ /** * Get data from cache */ async get(key) { const startTime = performance.now(); const fullKey = this.buildKey(key); try { // Check memory cache first const memoryResult = this.getFromMemory(fullKey); if (memoryResult.hit) { this.recordMetrics('hit', startTime); return memoryResult; } // Check persistent storage const persistentResult = await this.getFromPersistentStorage(fullKey); if (persistentResult.hit && persistentResult.data && persistentResult.metadata) { // Promote to memory cache const fullMetadata = persistentResult.metadata; this.setInMemory(fullKey, persistentResult.data, fullMetadata); this.recordMetrics('hit', startTime); return persistentResult; } this.recordMetrics('miss', startTime); return { data: null, hit: false, stale: false }; } catch (error) { this.log('Error getting from cache:', error); this.recordMetrics('miss', startTime); return { data: null, hit: false, stale: false }; } } /** * Set data in cache */ async set(key, data, options = {}) { const startTime = performance.now(); const fullKey = this.buildKey(key); const entry = { data, timestamp: Date.now(), ttl: options.ttl || this.config.defaultTTL, staleTime: options.staleTime || this.config.defaultStaleTime, tags: options.tags || [], version: options.version || 1, accessCount: 1, lastAccessed: Date.now(), ...(options.etag && { etag: options.etag }), }; try { // Set in memory cache this.setInMemory(fullKey, data, entry); // Set in persistent storage await this.setInPersistentStorage(fullKey, entry); this.recordMetrics('set', startTime); } catch (error) { this.log('Error setting cache:', error); } } /** * Check if data exists and is fresh */ async has(key) { const result = await this.get(key); return { exists: result.hit, fresh: result.hit && !result.stale, stale: result.hit && result.stale, }; } /** * Delete specific cache entry */ async delete(key) { const fullKey = this.buildKey(key); try { // Delete from memory const memoryDeleted = this.memoryCache.delete(fullKey); // Delete from persistent storage const persistentDeleted = await this.deleteFromPersistentStorage(fullKey); return memoryDeleted || persistentDeleted; } catch (error) { this.log('Error deleting from cache:', error); return false; } } /** * Clear cache based on invalidation options */ async invalidate(options) { try { if (options.clearAll) { await this.clear(); return; } if (options.keys) { for (const key of options.keys) { await this.delete(key); } } if (options.tags) { await this.invalidateByTags(options.tags); } if (options.pattern) { await this.invalidateByPattern(options.pattern); } } catch (error) { this.log('Error invalidating cache:', error); } } /** * Clear entire cache */ async clear() { try { // Clear memory cache this.memoryCache.clear(); // Clear persistent storage await this.clearPersistentStorage(); this.log('Cache cleared'); } catch (error) { this.log('Error clearing cache:', error); } } // ============================================================================ // SWR (STALE-WHILE-REVALIDATE) OPERATIONS // ============================================================================ /** * Get data with SWR pattern */ async getWithSWR(key, fetcher, options = {}) { const cacheResult = await this.get(key); // Cache hit with fresh data if (cacheResult.hit && !cacheResult.stale) { return cacheResult; } // Cache hit with stale data - return stale and revalidate in background if (cacheResult.hit && cacheResult.stale && options.revalidateInBackground !== false) { // Return stale data immediately const result = { ...cacheResult, revalidating: true }; // Revalidate in background this.revalidateInBackground(key, fetcher, options); return result; } // Cache miss or no background revalidation - fetch fresh data try { const freshData = await fetcher(); await this.set(key, freshData, options); return { data: freshData, hit: false, stale: false }; } catch (error) { // If fetch fails and we have stale data, return it if (cacheResult.hit) { return { ...cacheResult, stale: true }; } throw error; } } /** * Revalidate data in background */ async revalidateInBackground(key, fetcher, options = {}) { try { const freshData = await fetcher(); await this.set(key, freshData, options); this.log(`Background revalidation completed for key: ${key}`); } catch (error) { this.log(`Background revalidation failed for key: ${key}`, error); } } // ============================================================================ // STORAGE LAYER IMPLEMENTATIONS // ============================================================================ /** * Get from memory cache */ getFromMemory(key) { const entry = this.memoryCache.get(key); if (!entry) { return { data: null, hit: false, stale: false }; } // Update access tracking entry.accessCount++; entry.lastAccessed = Date.now(); // Check if expired const now = Date.now(); const isExpired = now - entry.timestamp > entry.ttl; const isStale = now - entry.timestamp > entry.staleTime; if (isExpired) { this.memoryCache.delete(key); return { data: null, hit: false, stale: false }; } return { data: entry.data, hit: true, stale: isStale, timeToExpiry: entry.ttl - (now - entry.timestamp), metadata: entry, }; } /** * Set in memory cache */ setInMemory(key, data, entry) { // Check size limits if (this.memoryCache.size >= this.config.maxSize) { this.evictLRUEntries(); } this.memoryCache.set(key, entry); } /** * Get from persistent storage */ async getFromPersistentStorage(key) { for (const storageType of [this.config.storage, ...(this.config.fallbackStorage || [])]) { try { const data = await this.getFromStorage(storageType, key); if (data) { const entry = JSON.parse(data); const now = Date.now(); const isExpired = now - entry.timestamp > entry.ttl; const isStale = now - entry.timestamp > entry.staleTime; if (isExpired) { await this.deleteFromStorage(storageType, key); continue; } return { data: entry.data, hit: true, stale: isStale, timeToExpiry: entry.ttl - (now - entry.timestamp), metadata: entry, }; } } catch (error) { this.log(`Error reading from ${storageType}:`, error); continue; } } return { data: null, hit: false, stale: false }; } /** * Set in persistent storage */ async setInPersistentStorage(key, entry) { const serialized = JSON.stringify(entry); for (const storageType of [this.config.storage, ...(this.config.fallbackStorage || [])]) { try { await this.setInStorage(storageType, key, serialized); break; // Success, no need to try fallbacks } catch (error) { this.log(`Error writing to ${storageType}:`, error); continue; } } } /** * Delete from persistent storage */ async deleteFromPersistentStorage(key) { let deleted = false; for (const storageType of [this.config.storage, ...(this.config.fallbackStorage || [])]) { try { await this.deleteFromStorage(storageType, key); deleted = true; } catch (error) { this.log(`Error deleting from ${storageType}:`, error); } } return deleted; } /** * Clear persistent storage */ async clearPersistentStorage() { for (const storageType of [this.config.storage, ...(this.config.fallbackStorage || [])]) { try { await this.clearStorage(storageType); } catch (error) { this.log(`Error clearing ${storageType}:`, error); } } } // ============================================================================ // STORAGE ADAPTERS // ============================================================================ /** * Get from specific storage type */ async getFromStorage(storageType, key) { switch (storageType) { case 'localStorage': return typeof window !== 'undefined' ? localStorage.getItem(key) : null; case 'sessionStorage': return typeof window !== 'undefined' ? sessionStorage.getItem(key) : null; case 'indexedDB': return await this.getFromIndexedDB(key); default: return null; } } /** * Set in specific storage type */ async setInStorage(storageType, key, value) { switch (storageType) { case 'localStorage': if (typeof window !== 'undefined') { localStorage.setItem(key, value); } break; case 'sessionStorage': if (typeof window !== 'undefined') { sessionStorage.setItem(key, value); } break; case 'indexedDB': await this.setInIndexedDB(key, value); break; } } /** * Delete from specific storage type */ async deleteFromStorage(storageType, key) { switch (storageType) { case 'localStorage': if (typeof window !== 'undefined') { localStorage.removeItem(key); } break; case 'sessionStorage': if (typeof window !== 'undefined') { sessionStorage.removeItem(key); } break; case 'indexedDB': await this.deleteFromIndexedDB(key); break; } } /** * Clear specific storage type */ async clearStorage(storageType) { switch (storageType) { case 'localStorage': if (typeof window !== 'undefined') { // Only clear our prefixed keys const keysToDelete = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(this.config.keyPrefix)) { keysToDelete.push(key); } } keysToDelete.forEach(key => localStorage.removeItem(key)); } break; case 'sessionStorage': if (typeof window !== 'undefined') { // Only clear our prefixed keys const keysToDelete = []; for (let i = 0; i < sessionStorage.length; i++) { const key = sessionStorage.key(i); if (key && key.startsWith(this.config.keyPrefix)) { keysToDelete.push(key); } } keysToDelete.forEach(key => sessionStorage.removeItem(key)); } break; case 'indexedDB': await this.clearIndexedDB(); break; } } // ============================================================================ // INDEXEDDB IMPLEMENTATION // ============================================================================ async getFromIndexedDB(key) { // Simplified IndexedDB implementation // In production, you might want to use a library like Dexie.js return new Promise(resolve => { if (typeof window === 'undefined') { resolve(null); return; } const request = indexedDB.open('cms-cache', 1); request.onsuccess = () => { const db = request.result; const transaction = db.transaction(['cache'], 'readonly'); const store = transaction.objectStore('cache'); const getRequest = store.get(key); getRequest.onsuccess = () => { resolve(getRequest.result?.value || null); }; getRequest.onerror = () => { resolve(null); }; }; request.onerror = () => { resolve(null); }; request.onupgradeneeded = event => { const db = event.target.result; if (!db.objectStoreNames.contains('cache')) { db.createObjectStore('cache', { keyPath: 'key' }); } }; }); } async setInIndexedDB(key, value) { return new Promise((resolve, reject) => { if (typeof window === 'undefined') { resolve(); return; } const request = indexedDB.open('cms-cache', 1); request.onsuccess = () => { const db = request.result; const transaction = db.transaction(['cache'], 'readwrite'); const store = transaction.objectStore('cache'); store.put({ key, value }); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }; request.onerror = () => reject(request.error); }); } async deleteFromIndexedDB(key) { return new Promise((resolve, reject) => { if (typeof window === 'undefined') { resolve(); return; } const request = indexedDB.open('cms-cache', 1); request.onsuccess = () => { const db = request.result; const transaction = db.transaction(['cache'], 'readwrite'); const store = transaction.objectStore('cache'); store.delete(key); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }; request.onerror = () => reject(request.error); }); } async clearIndexedDB() { return new Promise((resolve, reject) => { if (typeof window === 'undefined') { resolve(); return; } const request = indexedDB.open('cms-cache', 1); request.onsuccess = () => { const db = request.result; const transaction = db.transaction(['cache'], 'readwrite'); const store = transaction.objectStore('cache'); store.clear(); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }; request.onerror = () => reject(request.error); }); } // ============================================================================ // CACHE INVALIDATION // ============================================================================ /** * Invalidate by tags */ async invalidateByTags(tags) { const keysToDelete = []; // Check memory cache for (const [key, entry] of this.memoryCache.entries()) { if (entry.tags.some(tag => tags.includes(tag))) { keysToDelete.push(key); } } // Delete found keys for (const key of keysToDelete) { await this.delete(key.replace(this.config.keyPrefix, '')); } } /** * Invalidate by pattern */ async invalidateByPattern(pattern) { const keysToDelete = []; // Check memory cache for (const key of this.memoryCache.keys()) { if (pattern.test(key)) { keysToDelete.push(key); } } // Delete found keys for (const key of keysToDelete) { await this.delete(key.replace(this.config.keyPrefix, '')); } } // ============================================================================ // CACHE MAINTENANCE // ============================================================================ /** * Evict LRU entries when cache is full */ evictLRUEntries() { const entries = Array.from(this.memoryCache.entries()); entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed); // Remove oldest 10% of entries const toRemove = Math.ceil(entries.length * 0.1); for (let i = 0; i < toRemove; i++) { this.memoryCache.delete(entries[i][0]); } this.log(`Evicted ${toRemove} LRU entries`); } /** * Start cleanup interval */ startCleanupInterval() { setInterval(() => { this.cleanupExpiredEntries(); }, 5 * 60 * 1000); // Every 5 minutes } /** * Clean up expired entries */ cleanupExpiredEntries() { const now = Date.now(); let cleaned = 0; for (const [key, entry] of this.memoryCache.entries()) { if (now - entry.timestamp > entry.ttl) { this.memoryCache.delete(key); cleaned++; } } if (cleaned > 0) { this.log(`Cleaned up ${cleaned} expired entries`); } } // ============================================================================ // METRICS AND MONITORING // ============================================================================ /** * Record cache operation metrics */ recordMetrics(operation, startTime) { if (!this.config.enableMetrics) return; const duration = performance.now() - startTime; this.operationTimes.push(duration); this.metrics.totalOperations++; if (operation === 'hit') this.metrics.hits++; if (operation === 'miss') this.metrics.misses++; // Keep only last 1000 operation times if (this.operationTimes.length > 1000) { this.operationTimes = this.operationTimes.slice(-1000); } } /** * Start metrics calculation interval */ startMetricsCalculation() { if (!this.config.enableMetrics) return; setInterval(() => { this.updateMetrics(); }, 10000); // Every 10 seconds } /** * Update calculated metrics */ updateMetrics() { this.metrics.hitRatio = this.metrics.totalOperations > 0 ? this.metrics.hits / this.metrics.totalOperations : 0; this.metrics.averageResponseTime = this.operationTimes.length > 0 ? this.operationTimes.reduce((sum, time) => sum + time, 0) / this.operationTimes.length : 0; this.metrics.cacheSize = this.memoryCache.size; this.metrics.memoryUsage = this.estimateMemoryUsage(); // Calculate operations per second const timeDiff = (Date.now() - this.lastMetricsReset) / 1000; this.metrics.operationsPerSecond = timeDiff > 0 ? this.metrics.totalOperations / timeDiff : 0; } /** * Get current cache metrics */ getMetrics() { this.updateMetrics(); return { ...this.metrics }; } /** * Reset metrics */ resetMetrics() { this.metrics = { totalOperations: 0, hits: 0, misses: 0, hitRatio: 0, averageResponseTime: 0, cacheSize: this.memoryCache.size, memoryUsage: this.estimateMemoryUsage(), operationsPerSecond: 0, }; this.operationTimes = []; this.lastMetricsReset = Date.now(); } /** * Estimate memory usage */ estimateMemoryUsage() { let size = 0; for (const entry of this.memoryCache.values()) { size += JSON.stringify(entry).length * 2; // Rough estimate (UTF-16) } return size; } // ============================================================================ // UTILITY METHODS // ============================================================================ /** * Build full cache key with prefix */ buildKey(key) { return `${this.config.keyPrefix}${key}`; } /** * Debug logging */ log(message, ...args) { if (this.config.debug) { console.log(`[CacheManager] ${message}`, ...args); } } } // ============================================================================ // DEFAULT INSTANCE AND UTILITIES // ============================================================================ /** * Default cache manager instance */ export const defaultCacheManager = new CacheManager({ storage: 'localStorage', fallbackStorage: ['sessionStorage', 'memory'], defaultTTL: 30 * 60 * 1000, // 30 minutes defaultStaleTime: 5 * 60 * 1000, // 5 minutes maxSize: 500, debug: process.env.NODE_ENV === 'development', enableMetrics: true, keyPrefix: 'cms-cache:', }); /** * Create cache manager with custom config */ export function createCacheManager(config) { return new CacheManager(config); } /** * Cache key builders for different content types */ export const CacheKeys = { content: (contentType, slug) => `content:${contentType}:${slug}`, contentById: (id) => `content:id:${id}`, contentList: (contentType, options) => `list:${contentType}:${JSON.stringify(options)}`, route: (path) => `route:${path}`, author: (authorId) => `author:${authorId}`, category: (categoryId) => `category:${categoryId}`, search: (query) => `search:${encodeURIComponent(query)}`, }; /** * Common cache tags for invalidation */ export const CacheTags = { CONTENT: 'content', CONTENT_LIST: 'content-list', AUTHOR: 'author', CATEGORY: 'category', SEARCH: 'search', NAVIGATION: 'navigation', SETTINGS: 'settings', };