/** * Redis Caching Service * Provides centralized caching functionality with TTL and invalidation */ const { createClient } = require('redis'); const { logger } = require('../Middleware/errorHandler'); class CacheService { constructor() { this.client = null; this.isConnected = false; this.defaultTTL = 300; // 5 minutes this.keyPrefix = 'campverse:'; } /** * Initialize Redis connection */ async connect() { try { this.client = createClient({ url: process.env.REDIS_URL || 'redis://127.0.0.1:6379', retry_strategy: (options) => { if (options.error && options.error.code === 'ECONNREFUSED') { logger.error('Redis connection refused'); return new Error('Redis connection refused'); } if (options.total_retry_time > 1000 * 60 * 60) { logger.error('Redis retry time exhausted'); return new Error('Retry time exhausted'); } if (options.attempt > 10) { logger.error('Redis max retry attempts reached'); return undefined; } return Math.min(options.attempt * 100, 3000); } }); this.client.on('error', (err) => { logger.error('Redis Client Error:', err); this.isConnected = false; }); this.client.on('connect', () => { logger.info('Redis connected'); this.isConnected = true; }); this.client.on('disconnect', () => { logger.warn('Redis disconnected'); this.isConnected = false; }); await this.client.connect(); } catch (error) { logger.error('Failed to connect to Redis:', error); this.isConnected = false; } } /** * Generate cache key with prefix */ _generateKey(key) { return `${this.keyPrefix}${key}`; } /** * Get value from cache */ async get(key) { if (!this.isConnected) { logger.warn(`Cache not connected, returning null for key: ${key}`); return null; } try { const cacheKey = this._generateKey(key); const value = await this.client.get(cacheKey); if (value) { logger.debug(`Cache hit for key: ${key}`); return JSON.parse(value); } logger.debug(`Cache miss for key: ${key}`); return null; } catch (error) { logger.error(`Cache get error for key ${key}:`, error); // Don't throw, just return null to allow fallback return null; } } /** * Set value in cache with TTL */ async set(key, value, ttl = this.defaultTTL) { if (!this.isConnected) { logger.warn(`Cache not connected, cannot set key: ${key}`); return false; } try { const cacheKey = this._generateKey(key); const serializedValue = JSON.stringify(value); await this.client.setEx(cacheKey, ttl, serializedValue); logger.debug(`Cache set for key: ${key}, TTL: ${ttl}s`); return true; } catch (error) { logger.error(`Cache set error for key ${key}:`, error); return false; } } /** * Delete value from cache */ async del(key) { if (!this.isConnected) return false; try { const cacheKey = this._generateKey(key); const result = await this.client.del(cacheKey); logger.debug(`Cache delete for key: ${key}`); return result > 0; } catch (error) { logger.error(`Cache delete error for key ${key}:`, error); return false; } } /** * Delete multiple keys by pattern */ async delPattern(pattern) { if (!this.isConnected) return false; try { const searchPattern = this._generateKey(pattern); const keys = await this.client.keys(searchPattern); if (keys.length > 0) { await this.client.del(keys); logger.debug(`Cache pattern delete: ${pattern}, ${keys.length} keys deleted`); } return true; } catch (error) { logger.error(`Cache pattern delete error for pattern ${pattern}:`, error); return false; } } /** * Check if key exists */ async exists(key) { if (!this.isConnected) return false; try { const cacheKey = this._generateKey(key); const result = await this.client.exists(cacheKey); return result === 1; } catch (error) { logger.error(`Cache exists error for key ${key}:`, error); return false; } } /** * Increment counter */ async incr(key, ttl = this.defaultTTL) { if (!this.isConnected) return null; try { const cacheKey = this._generateKey(key); const value = await this.client.incr(cacheKey); if (value === 1) { // Set TTL only for new keys await this.client.expire(cacheKey, ttl); } return value; } catch (error) { logger.error(`Cache incr error for key ${key}:`, error); return null; } } /** * Get or set pattern - fetch from cache or execute function and cache result */ async getOrSet(key, fetchFunction, ttl = this.defaultTTL) { // Try to get from cache first const cachedValue = await this.get(key); if (cachedValue !== null) { return cachedValue; } try { // Execute function to get fresh data const freshValue = await fetchFunction(); // Cache the result await this.set(key, freshValue, ttl); return freshValue; } catch (error) { logger.error(`Cache getOrSet error for key ${key}:`, error); throw error; } } /** * Cache invalidation patterns */ async invalidateUser(userId) { await this.delPattern(`user:${userId}:*`); await this.delPattern(`dashboard:user:${userId}`); } async invalidateInstitution(institutionId) { await this.delPattern(`institution:${institutionId}:*`); await this.delPattern(`dashboard:institution:${institutionId}`); await this.delPattern('institutions:*'); } async invalidateEvent(eventId) { await this.delPattern(`event:${eventId}:*`); await this.delPattern('events:*'); await this.delPattern('recommendations:*'); } async invalidateGlobal() { await this.delPattern('events:*'); await this.delPattern('institutions:*'); await this.delPattern('analytics:*'); } /** * Invalidate verifier-related cache (pending verifications, etc.) */ async invalidateVerifierCache() { await this.delPattern('verifier:*'); await this.delPattern('pending:*'); await this.delPattern('verification:*'); await this.delPattern('institutions:pending:*'); await this.delPattern('certificates:pending:*'); } /** * Invalidate admin-related cache */ async invalidateAdminCache() { await this.delPattern('admin:*'); await this.delPattern('platform:*'); await this.delPattern('users:*'); await this.delPattern('institutions:*'); await this.delPattern('analytics:*'); } /** * Invalidate certificate verification cache */ async invalidateCertificateVerification(eventId) { await this.delPattern(`event:${eventId}:*`); await this.delPattern('certificates:pending:*'); await this.delPattern('verification:certificates:*'); await this.delPattern('verifier:*'); } /** * Invalidate institution verification cache */ async invalidateInstitutionVerification(institutionId) { await this.delPattern(`institution:${institutionId}:*`); await this.delPattern('institutions:pending:*'); await this.delPattern('verification:institutions:*'); await this.delPattern('verifier:*'); await this.delPattern('institutions:*'); } /** * Health check */ async healthCheck() { if (!this.isConnected) return false; try { await this.client.ping(); return true; } catch (error) { logger.error('Redis health check failed:', error); return false; } } /** * Get cache statistics */ async getStats() { if (!this.isConnected) return null; try { const info = await this.client.info('memory'); const keyspace = await this.client.info('keyspace'); return { connected: this.isConnected, memory: info, keyspace, timestamp: new Date().toISOString() }; } catch (error) { logger.error('Failed to get cache stats:', error); return null; } } /** * Flush all cache (use with caution) */ async flushAll() { if (!this.isConnected) return false; try { await this.client.flushAll(); logger.warn('Cache flushed - all keys deleted'); return true; } catch (error) { logger.error('Cache flush error:', error); return false; } } /** * Close connection */ async disconnect() { if (this.client) { await this.client.disconnect(); this.isConnected = false; logger.info('Redis disconnected'); } } } // Cache key generators for consistency const CacheKeys = { user: (userId) => `user:${userId}`, userDashboard: (userId) => `dashboard:user:${userId}`, userEvents: (userId) => `user:${userId}:events`, userCertificates: (userId) => `user:${userId}:certificates`, institution: (institutionId) => `institution:${institutionId}`, institutionDashboard: (institutionId) => `dashboard:institution:${institutionId}`, institutionEvents: (institutionId) => `institution:${institutionId}:events`, institutionUsers: (institutionId) => `institution:${institutionId}:users`, event: (eventId) => `event:${eventId}`, eventParticipants: (eventId) => `event:${eventId}:participants`, eventAnalytics: (eventId) => `event:${eventId}:analytics`, events: (filters = '') => `events:${filters}`, institutions: (filters = '') => `institutions:${filters}`, recommendations: (userId) => `recommendations:${userId}`, analytics: (type, id = '') => `analytics:${type}:${id}`, rateLimit: (ip, endpoint) => `ratelimit:${ip}:${endpoint}`, session: (sessionId) => `session:${sessionId}`, }; // TTL constants (in seconds) const CacheTTL = { SHORT: 60, // 1 minute MEDIUM: 300, // 5 minutes LONG: 1800, // 30 minutes VERY_LONG: 3600, // 1 hour DAY: 86400, // 24 hours }; // Create singleton instance const cacheService = new CacheService(); module.exports = { cacheService, CacheKeys, CacheTTL };