/** * In-Memory Cache Middleware * Caches API responses to reduce database load */ const logger = require("../config/logger"); class CacheManager { constructor(defaultTTL = 300000) { // 5 minutes default this.cache = new Map(); this.defaultTTL = defaultTTL; } set(key, value, ttl = this.defaultTTL) { const expiresAt = Date.now() + ttl; this.cache.set(key, { value, expiresAt }); logger.debug(`Cache set: ${key} (TTL: ${ttl}ms)`); } get(key) { const cached = this.cache.get(key); if (!cached) return null; if (Date.now() > cached.expiresAt) { this.cache.delete(key); logger.debug(`Cache expired: ${key}`); return null; } logger.debug(`Cache hit: ${key}`); return cached.value; } delete(key) { const deleted = this.cache.delete(key); if (deleted) logger.debug(`Cache invalidated: ${key}`); return deleted; } deletePattern(pattern) { let count = 0; for (const key of this.cache.keys()) { if (key.includes(pattern)) { this.cache.delete(key); count++; } } if (count > 0) logger.debug(`Cache pattern invalidated: ${pattern} (${count} keys)`); return count; } clear() { const size = this.cache.size; this.cache.clear(); logger.info(`Cache cleared (${size} keys)`); } size() { return this.cache.size; } // Clean up expired entries cleanup() { const now = Date.now(); let cleaned = 0; for (const [key, { expiresAt }] of this.cache.entries()) { if (now > expiresAt) { this.cache.delete(key); cleaned++; } } if (cleaned > 0) logger.debug(`Cache cleanup: removed ${cleaned} expired entries`); return cleaned; } } // Global cache instance const cache = new CacheManager(); // Cleanup interval reference (for graceful shutdown) let cleanupInterval = null; // Start automatic cleanup (optional, call from server startup) const startCleanup = () => { if (!cleanupInterval) { cleanupInterval = setInterval(() => cache.cleanup(), 300000); // 5 minutes logger.debug("Cache cleanup scheduler started"); } }; // Stop automatic cleanup (for graceful shutdown) const stopCleanup = () => { if (cleanupInterval) { clearInterval(cleanupInterval); cleanupInterval = null; logger.debug("Cache cleanup scheduler stopped"); } }; /** * Cache middleware factory * @param {number} ttl - Time to live in milliseconds * @param {function} keyGenerator - Function to generate cache key from req */ const cacheMiddleware = (ttl = 300000, keyGenerator = null) => { return (req, res, next) => { // Skip cache for authenticated requests if (req.session && req.session.userId) { return next(); } const key = keyGenerator ? keyGenerator(req) : `${req.method}:${req.originalUrl}`; const cachedResponse = cache.get(key); if (cachedResponse) { res.setHeader("X-Cache", "HIT"); return res.json(cachedResponse); } // Store original json method const originalJson = res.json.bind(res); // Override json method to cache response res.json = function (data) { cache.set(key, data, ttl); res.setHeader("X-Cache", "MISS"); return originalJson(data); }; next(); }; }; module.exports = { cache, cacheMiddleware, startCleanup, stopCleanup, };