@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
JavaScript
/**
* @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',
};