import { createClient } from "redis"; let redisClient: ReturnType | null = null; let connectionFailed = false; // Track if connection has permanently failed interface RedisError { code?: string; message?: string; errors?: RedisError[]; cause?: unknown; } // Helper to check if error is connection refused const isConnectionRefused = (err: unknown): boolean => { if (!err) return false; const error = err as RedisError; // Check direct properties if ( error.code === "ECONNREFUSED" || error.message?.includes("ECONNREFUSED") ) { return true; } // Check AggregateError if (error.errors && Array.isArray(error.errors)) { return error.errors.some( (e: RedisError) => e?.code === "ECONNREFUSED" || e?.message?.includes("ECONNREFUSED"), ); } // Check nested error if (error.cause) { return isConnectionRefused(error.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: unknown) => { // 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: unknown) => { // 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: unknown) { // 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 } }, };