UNPKG

@ideal-photography/shared

Version:

Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.

485 lines (424 loc) 11.4 kB
/** * Caching utilities for improved performance * Provides in-memory caching and Redis-compatible interface */ /** * In-memory cache implementation */ class MemoryCache { constructor(options = {}) { this.cache = new Map(); this.timers = new Map(); this.defaultTTL = options.defaultTTL || 300000; // 5 minutes this.maxSize = options.maxSize || 1000; this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0 }; } /** * Set a value in cache with optional TTL */ set(key, value, ttl = this.defaultTTL) { // Remove oldest entries if cache is full if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; this.delete(firstKey); } // Clear existing timer for this key if (this.timers.has(key)) { clearTimeout(this.timers.get(key)); } // Set the value this.cache.set(key, { value, createdAt: Date.now(), ttl }); // Set expiration timer if (ttl > 0) { const timer = setTimeout(() => { this.delete(key); }, ttl); this.timers.set(key, timer); } this.stats.sets++; return true; } /** * Get a value from cache */ get(key) { const entry = this.cache.get(key); if (!entry) { this.stats.misses++; return null; } // Check if expired if (entry.ttl > 0 && Date.now() - entry.createdAt > entry.ttl) { this.delete(key); this.stats.misses++; return null; } this.stats.hits++; return entry.value; } /** * Delete a value from cache */ delete(key) { const deleted = this.cache.delete(key); if (this.timers.has(key)) { clearTimeout(this.timers.get(key)); this.timers.delete(key); } if (deleted) { this.stats.deletes++; } return deleted; } /** * Check if key exists */ has(key) { return this.cache.has(key) && this.get(key) !== null; } /** * Clear all cache entries */ clear() { // Clear all timers for (const timer of this.timers.values()) { clearTimeout(timer); } this.cache.clear(); this.timers.clear(); // Reset stats this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0 }; } /** * Get cache statistics */ getStats() { const total = this.stats.hits + this.stats.misses; return { ...this.stats, size: this.cache.size, hitRate: total > 0 ? (this.stats.hits / total * 100).toFixed(2) + '%' : '0%', memoryUsage: this.getMemoryUsage() }; } /** * Get approximate memory usage */ getMemoryUsage() { let size = 0; for (const [key, entry] of this.cache.entries()) { size += key.length * 2; // Approximate string size size += JSON.stringify(entry.value).length * 2; } return `${(size / 1024).toFixed(2)} KB`; } /** * Get or set pattern - useful for resolver caching */ async getOrSet(key, fetchFunction, ttl = this.defaultTTL) { const cached = this.get(key); if (cached !== null) { return cached; } try { const value = await fetchFunction(); this.set(key, value, ttl); return value; } catch (error) { console.error(`Cache fetch error for key ${key}:`, error); throw error; } } } /** * Cache key generators for consistent naming */ export const CacheKeys = { // User-related keys user: (id) => `user:${id}`, userByEmail: (email) => `user:email:${email}`, userList: (filters) => `users:list:${JSON.stringify(filters)}`, userStats: () => 'users:stats', // Booking-related keys booking: (id) => `booking:${id}`, bookingList: (filters) => `bookings:list:${JSON.stringify(filters)}`, bookingStats: () => 'bookings:stats', userBookings: (userId) => `user:${userId}:bookings`, // Service-related keys service: (id) => `service:${id}`, serviceList: (filters) => `services:list:${JSON.stringify(filters)}`, serviceStats: () => 'services:stats', activeServices: () => 'services:active', // Order-related keys order: (id) => `order:${id}`, orderList: (filters) => `orders:list:${JSON.stringify(filters)}`, userOrders: (userId) => `user:${userId}:orders`, // Analytics keys dashboardStats: () => 'analytics:dashboard', revenueAnalytics: (period) => `analytics:revenue:${period}`, userAnalytics: (period) => `analytics:users:${period}`, bookingAnalytics: (period) => `analytics:bookings:${period}`, // Settings keys settings: () => 'settings:all', settingsByCategory: (category) => `settings:category:${category}`, // Media keys mediaList: (filters) => `media:list:${JSON.stringify(filters)}`, mediaStats: () => 'media:stats' }; /** * Cache TTL constants (in milliseconds) */ export const CacheTTL = { SHORT: 60 * 1000, // 1 minute MEDIUM: 300 * 1000, // 5 minutes LONG: 900 * 1000, // 15 minutes HOUR: 3600 * 1000, // 1 hour DAY: 86400 * 1000, // 24 hours WEEK: 604800 * 1000 // 7 days }; /** * Global cache instance */ export const cache = new MemoryCache({ defaultTTL: CacheTTL.MEDIUM, maxSize: 1000 }); /** * Cache invalidation utilities */ export const CacheInvalidation = { /** * Invalidate user-related caches */ user: (userId) => { const patterns = [ CacheKeys.user(userId), CacheKeys.userBookings(userId), CacheKeys.userOrders(userId), CacheKeys.userStats(), 'users:list:' // Partial match for user lists ]; patterns.forEach(pattern => { if (pattern.includes('users:list:')) { // Clear all user list caches for (const key of cache.cache.keys()) { if (key.startsWith(pattern)) { cache.delete(key); } } } else { cache.delete(pattern); } }); }, /** * Invalidate booking-related caches */ booking: (bookingId, userId) => { const patterns = [ CacheKeys.booking(bookingId), CacheKeys.bookingStats(), CacheKeys.dashboardStats(), 'bookings:list:' // Partial match ]; if (userId) { patterns.push(CacheKeys.userBookings(userId)); } patterns.forEach(pattern => { if (pattern.includes('bookings:list:')) { for (const key of cache.cache.keys()) { if (key.startsWith(pattern)) { cache.delete(key); } } } else { cache.delete(pattern); } }); }, /** * Invalidate service-related caches */ service: (serviceId) => { const patterns = [ CacheKeys.service(serviceId), CacheKeys.serviceStats(), CacheKeys.activeServices(), 'services:list:' // Partial match ]; patterns.forEach(pattern => { if (pattern.includes('services:list:')) { for (const key of cache.cache.keys()) { if (key.startsWith(pattern)) { cache.delete(key); } } } else { cache.delete(pattern); } }); }, /** * Invalidate analytics caches */ analytics: () => { const patterns = [ 'analytics:' ]; patterns.forEach(pattern => { for (const key of cache.cache.keys()) { if (key.startsWith(pattern)) { cache.delete(key); } } }); }, /** * Clear all caches */ all: () => { cache.clear(); } }; /** * Cache middleware for GraphQL resolvers */ export const withCache = (keyGenerator, ttl = CacheTTL.MEDIUM) => { return (resolver) => { return async (parent, args, context, info) => { const cacheKey = typeof keyGenerator === 'function' ? keyGenerator(args, context) : keyGenerator; // Try to get from cache first const cached = cache.get(cacheKey); if (cached !== null) { return cached; } // Execute resolver and cache result try { const result = await resolver(parent, args, context, info); cache.set(cacheKey, result, ttl); return result; } catch (error) { // Don't cache errors throw error; } }; }; }; /** * Batch cache operations */ export const BatchCache = { /** * Get multiple keys at once */ mget: (keys) => { return keys.map(key => ({ key, value: cache.get(key) })); }, /** * Set multiple keys at once */ mset: (entries, ttl) => { entries.forEach(({ key, value }) => { cache.set(key, value, ttl); }); }, /** * Delete multiple keys at once */ mdel: (keys) => { return keys.map(key => cache.delete(key)); } }; /** * Cache warming utilities */ export const CacheWarming = { /** * Warm up dashboard stats */ warmDashboard: async (resolvers) => { try { console.log('Warming dashboard cache...'); const stats = await resolvers.dashboardStats(); cache.set(CacheKeys.dashboardStats(), stats, CacheTTL.MEDIUM); console.log('✓ Dashboard cache warmed'); } catch (error) { console.error('Failed to warm dashboard cache:', error); } }, /** * Warm up frequently accessed data */ warmFrequentData: async (resolvers) => { try { console.log('Warming frequently accessed cache...'); // Active services const services = await resolvers.services({ isActive: true }); cache.set(CacheKeys.activeServices(), services, CacheTTL.LONG); // User stats const userStats = await resolvers.userStats(); cache.set(CacheKeys.userStats(), userStats, CacheTTL.MEDIUM); // Booking stats const bookingStats = await resolvers.bookingStats(); cache.set(CacheKeys.bookingStats(), bookingStats, CacheTTL.MEDIUM); console.log('✓ Frequent data cache warmed'); } catch (error) { console.error('Failed to warm frequent data cache:', error); } } }; /** * Cache monitoring and cleanup */ export const CacheMonitoring = { /** * Start periodic cache cleanup */ startCleanup: (interval = 300000) => { // 5 minutes setInterval(() => { const stats = cache.getStats(); console.log('Cache stats:', stats); // Clear cache if hit rate is too low if (parseFloat(stats.hitRate) < 20 && stats.size > 100) { console.log('Low cache hit rate detected, clearing cache'); cache.clear(); } }, interval); }, /** * Get detailed cache information */ getInfo: () => { const stats = cache.getStats(); const keys = Array.from(cache.cache.keys()); const keysByPattern = {}; keys.forEach(key => { const pattern = key.split(':')[0]; keysByPattern[pattern] = (keysByPattern[pattern] || 0) + 1; }); return { ...stats, keysByPattern, totalKeys: keys.length }; } };