sanket3280/code-execution
0
1/**2 * Cache Middleware3 * Provides route-level caching with Redis or in-memory fallback4 */5 6const redisClient = require('../config/redis');7const logger = require('../utils/logger');8 9// In-memory cache fallback10const memoryCache = new Map();11const cacheTimestamps = new Map();12 13/**14 * Cache middleware for routes15 * @param {string} keyPrefix - Prefix for cache key16 * @param {number} ttlSeconds - Time to live in seconds (default: 300 = 5 minutes)17 * @param {function} keyGenerator - Optional function to generate dynamic cache key from req18 */19const cache = (keyPrefix, ttlSeconds = 300, keyGenerator = null) => {20 return async (req, res, next) => {21 try {22 // Generate cache key23 let cacheKey;24 if (keyGenerator && typeof keyGenerator === 'function') {25 cacheKey = keyGenerator(req);26 } else {27 // Default: use query params and user ID if available28 const userId = req.user?.id || 'guest';29 const queryString = JSON.stringify(req.query);30 cacheKey = `${keyPrefix}:${userId}:${queryString}`;31 }32 33 // Try to get from cache (with timeout for Redis)34 let cached = null;35 let cacheSource = 'none';36 37 // Check memory cache first (fastest)38 const memoryCached = memoryCache.get(cacheKey);39 const timestamp = cacheTimestamps.get(cacheKey);40 if (memoryCached && timestamp && (Date.now() - timestamp < ttlSeconds * 1000)) {41 cached = memoryCached;42 cacheSource = 'memory';43 } else if (memoryCached) {44 // Expired - remove from memory45 memoryCache.delete(cacheKey);46 cacheTimestamps.delete(cacheKey);47 }48 49 // If not in memory, try Redis (with very short timeout)50 if (!cached) {51 try {52 const redisPromise = redisClient.get(cacheKey);53 const timeoutPromise = new Promise((_, reject) => 54 setTimeout(() => reject(new Error('Redis timeout')), 30) // 30ms timeout55 );56 const redisResult = await Promise.race([redisPromise, timeoutPromise]);57 if (redisResult) {58 cached = redisResult;59 cacheSource = 'redis';60 // Also store in memory for next time61 memoryCache.set(cacheKey, typeof cached === 'string' ? JSON.parse(cached) : cached);62 cacheTimestamps.set(cacheKey, Date.now());63 }64 } catch (err) {65 // Redis failed or timed out - continue without cache66 }67 }68 69 if (cached) {70 // Cache hit - return immediately with 304 or 20071 const cachedData = typeof cached === 'string' ? JSON.parse(cached) : cached;72 const etag = `"${Buffer.from(JSON.stringify(cachedData)).toString('base64').slice(0, 16)}"`;73 74 // Check if client has matching ETag75 const clientEtag = req.headers['if-none-match'];76 if (clientEtag === etag) {77 res.setHeader('X-Cache', 'HIT');78 res.setHeader('X-Cache-Source', cacheSource);79 return res.status(304).end(); // Not Modified80 }81 82 res.setHeader('X-Cache', 'HIT');83 res.setHeader('X-Cache-Source', cacheSource);84 res.setHeader('ETag', etag);85 res.setHeader('Cache-Control', `public, max-age=${ttlSeconds}`);86 return res.json(cachedData);87 }88 89 // Silent cache miss90 res.setHeader('X-Cache', 'MISS');91 92 // Store original res.json93 const originalJson = res.json.bind(res);94 95 // Override res.json to cache the response96 res.json = (body) => {97 const bodyStr = JSON.stringify(body);98 const etag = `"${Buffer.from(bodyStr).toString('base64').slice(0, 16)}"`;99 100 // Set ETag and Cache-Control headers101 res.setHeader('ETag', etag);102 res.setHeader('Cache-Control', `public, max-age=${ttlSeconds}`);103 104 // Cache in background (non-blocking)105 Promise.race([106 redisClient.set(cacheKey, bodyStr, 'EX', ttlSeconds),107 new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 100))108 ]).catch(err => {109 // Redis failed or timed out, use memory cache as fallback110 memoryCache.set(cacheKey, body);111 cacheTimestamps.set(cacheKey, Date.now());112 });113 114 // Send the response immediately (don't wait for cache)115 return originalJson(body);116 };117 118 next();119 } catch (error) {120 logger.error('Cache middleware error', error);121 // Continue without caching on error122 next();123 }124 };125};126 127/**128 * Invalidate cache by pattern129 * @param {string} pattern - Redis key pattern (e.g., 'problems:*')130 */131const invalidateCache = async (pattern) => {132 try {133 const keys = await redisClient.keys(pattern);134 if (keys.length > 0) {135 await redisClient.del(...keys);136 logger.debug(`Cache invalidated: ${keys.length} keys matching ${pattern}`);137 }138 } catch (error) {139 logger.error('Cache invalidation error', error);140 }141};142 143module.exports = {144 cache,145 invalidateCache146};147 