/** * Redis Client for Session Persistence * * Provides persistent storage for user credentials across server restarts. * Falls back to in-memory Map if Redis is unavailable. */ import { createClient } from 'redis'; const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; const USER_CACHE_TTL = 30 * 60; // 30 minutes in seconds let redisClient = null; let redisAvailable = false; // Fallback in-memory cache (used if Redis is unavailable) const memoryCache = new Map(); /** * Initialize Redis client */ export async function initRedis() { try { console.error('[Redis] Connecting to Redis at', REDIS_URL); redisClient = createClient({ url: REDIS_URL, socket: { connectTimeout: 5000, reconnectStrategy: (retries) => { if (retries > 3) { console.error('[Redis] Max reconnection attempts reached, falling back to memory cache'); redisAvailable = false; return false; // Stop reconnecting } return Math.min(retries * 100, 3000); } } }); redisClient.on('error', (err) => { console.error('[Redis] Error:', err.message); redisAvailable = false; }); redisClient.on('connect', () => { console.error('[Redis] Connected successfully'); redisAvailable = true; }); redisClient.on('reconnecting', () => { console.error('[Redis] Reconnecting...'); }); await redisClient.connect(); redisAvailable = true; console.error('[Redis] Ready to use'); } catch (error) { console.error('[Redis] Failed to initialize:', error.message); console.error('[Redis] Falling back to in-memory cache'); redisAvailable = false; } } /** * Set user credentials in cache (Redis or memory) */ export async function setUserCredentials(userIdentifier, credentials) { if (!userIdentifier) { console.error('[Redis] Cannot set credentials: no userIdentifier'); return false; } const key = `user:creds:${userIdentifier}`; const value = JSON.stringify({ ...credentials, lastUsed: Date.now() }); if (redisAvailable && redisClient) { try { await redisClient.setEx(key, USER_CACHE_TTL, value); console.error(`[Redis] Saved credentials for user ${userIdentifier} (TTL: ${USER_CACHE_TTL}s)`); return true; } catch (error) { console.error('[Redis] Error saving to Redis:', error.message); console.error('[Redis] Falling back to memory cache for this operation'); redisAvailable = false; } } // Fallback to memory cache memoryCache.set(key, { value, expiresAt: Date.now() + (USER_CACHE_TTL * 1000) }); console.error(`[Redis] Saved credentials for user ${userIdentifier} to memory cache`); return true; } /** * Get user credentials from cache (Redis or memory) */ export async function getUserCredentials(userIdentifier) { if (!userIdentifier) { return null; } const key = `user:creds:${userIdentifier}`; if (redisAvailable && redisClient) { try { const value = await redisClient.get(key); if (value) { console.error(`[Redis] Retrieved credentials for user ${userIdentifier} from Redis`); const creds = JSON.parse(value); // Update lastUsed timestamp await setUserCredentials(userIdentifier, { website: creds.website, token: creds.token, tokenHash: creds.tokenHash, profileName: creds.profileName }); return creds; } } catch (error) { console.error('[Redis] Error reading from Redis:', error.message); console.error('[Redis] Falling back to memory cache'); redisAvailable = false; } } // Fallback to memory cache const cached = memoryCache.get(key); if (cached) { if (Date.now() < cached.expiresAt) { console.error(`[Redis] Retrieved credentials for user ${userIdentifier} from memory cache`); const creds = JSON.parse(cached.value); // Update expiration memoryCache.set(key, { value: cached.value, expiresAt: Date.now() + (USER_CACHE_TTL * 1000) }); return creds; } else { console.error(`[Redis] Memory cache expired for user ${userIdentifier}`); memoryCache.delete(key); } } return null; } /** * Delete user credentials from cache */ export async function deleteUserCredentials(userIdentifier) { if (!userIdentifier) { return; } const key = `user:creds:${userIdentifier}`; if (redisAvailable && redisClient) { try { await redisClient.del(key); console.error(`[Redis] Deleted credentials for user ${userIdentifier} from Redis`); } catch (error) { console.error('[Redis] Error deleting from Redis:', error.message); } } // Also delete from memory cache memoryCache.delete(key); console.error(`[Redis] Deleted credentials for user ${userIdentifier} from memory cache`); } /** * Get Redis health status */ export function getRedisStatus() { return { available: redisAvailable, connected: redisClient?.isOpen || false, url: REDIS_URL, fallbackCacheSize: memoryCache.size }; } /** * Close Redis connection (for graceful shutdown) */ export async function closeRedis() { if (redisClient) { try { await redisClient.quit(); console.error('[Redis] Connection closed'); } catch (error) { console.error('[Redis] Error closing connection:', error.message); } } }