import { createClient } from 'redis'; let redisClient: ReturnType | null = null; let connectionFailed = false; // Track if connection has permanently failed // Helper to check if error is connection refused const isConnectionRefused = (err: any): boolean => { if (!err) return false; // Check direct properties if (err.code === 'ECONNREFUSED' || err.message?.includes('ECONNREFUSED')) { return true; } // Check AggregateError if (err.errors && Array.isArray(err.errors)) { return err.errors.some((e: any) => e?.code === 'ECONNREFUSED' || e?.message?.includes('ECONNREFUSED')); } // Check nested error if (err.cause) { return isConnectionRefused(err.cause); } return false; }; export const getRedisClient = async () => { // If Redis URL is not configured, return null instead of trying to connect if (!process.env.REDIS_URL) { return null; } // If connection has already failed, don't try again if (connectionFailed) { return null; } if (!redisClient) { const redisUrl = process.env.REDIS_URL; try { redisClient = createClient({ url: redisUrl, socket: { reconnectStrategy: (retries) => { // Stop trying after 1 attempt to avoid spam if (retries > 1) { connectionFailed = true; return false; } return false; // Don't reconnect automatically } } }); redisClient.on('error', (err: any) => { // Silently handle connection refused errors - Redis is optional if (isConnectionRefused(err)) { connectionFailed = true; return; // Don't log connection refused errors } // Only log non-connection-refused errors console.error('Redis Client Error:', err); }); redisClient.on('connect', () => { console.log('Redis Client Connected'); connectionFailed = false; // Reset on successful connection }); redisClient.on('ready', () => { console.log('Redis Client Ready'); connectionFailed = false; // Reset on ready }); redisClient.on('end', () => { console.log('Redis Client Disconnected'); }); await redisClient.connect().catch((err: any) => { // Connection failed if (isConnectionRefused(err)) { connectionFailed = true; // Silently handle connection refused - Redis is optional } else { // Only log non-connection-refused errors console.error('Redis connection failed:', err); } redisClient = null; }); } catch (error: any) { // If connection fails, set to null if (isConnectionRefused(error)) { connectionFailed = true; } redisClient = null; } } return redisClient; }; export const closeRedisConnection = async () => { if (redisClient) { await redisClient.quit(); redisClient = null; } }; // Cache utilities export const cache = { async get(key: string) { try { const client = await getRedisClient(); if (!client) return null; const value = await client.get(key); return value ? JSON.parse(value) : null; } catch (error) { // Silently fail if Redis is not available return null; } }, async set(key: string, value: unknown, ttlSeconds = 3600) { try { const client = await getRedisClient(); if (!client) return false; await client.setEx(key, ttlSeconds, JSON.stringify(value)); return true; } catch (error) { // Silently fail if Redis is not available return false; } }, async del(key: string) { try { const client = await getRedisClient(); if (!client) return false; await client.del(key); return true; } catch (error) { // Silently fail if Redis is not available return false; } }, async exists(key: string) { try { const client = await getRedisClient(); if (!client) return false; return await client.exists(key); } catch (error) { // Silently fail if Redis is not available return false; } }, async flush() { try { const client = await getRedisClient(); if (!client) return false; await client.flushAll(); return true; } catch (error) { // Silently fail if Redis is not available return false; } } }; // Session management export const session = { async create(userId: string, data: unknown, ttlSeconds = 86400) { const sessionId = `session:${userId}:${Date.now()}`; await cache.set(sessionId, data, ttlSeconds); return sessionId; }, async get(sessionId: string) { return await cache.get(sessionId); }, async update(sessionId: string, data: unknown, ttlSeconds = 86400) { return await cache.set(sessionId, data, ttlSeconds); }, async destroy(sessionId: string) { return await cache.del(sessionId); } }; // Analytics caching export const analyticsCache = { async getProjectStats(projectId: number) { return await cache.get(`analytics:project:${projectId}`); }, async setProjectStats(projectId: number, stats: unknown, ttlSeconds = 300) { return await cache.set(`analytics:project:${projectId}`, stats, ttlSeconds); }, async getOverallStats() { return await cache.get('analytics:overall'); }, async setOverallStats(stats: unknown, ttlSeconds = 600) { return await cache.set('analytics:overall', stats, ttlSeconds); }, async invalidateProject(projectId: number) { await cache.del(`analytics:project:${projectId}`); await cache.del('analytics:overall'); }, async clearAll() { try { const client = await getRedisClient(); if (!client) return; // Clear all analytics-related keys const keys = await client.keys('analytics:*'); if (keys.length > 0) { await client.del(keys); } } catch (error) { // Silently fail if Redis is not available } } };