const Redis = require('ioredis'); // Redis is a cache, not a source of truth. If it's unreachable, callers // should fall back to the underlying data store (Supabase). All higher-level // helpers here return null on connection failure rather than throwing — // that keeps every consumer single-branch (`if (cached) return cached`). let client = null; let degraded = false; function getRedisClient() { if (!client) { client = new Redis(process.env.REDIS_URL || 'redis://127.0.0.1:6379', { // Eager connect — but allow commands to QUEUE while the TCP/auth // handshake completes. Without offline queueing, pollers booting // alongside the API throw "Stream isn't writeable" before Redis // is ready. With queueing, ioredis flushes the backlog the moment // the connection enters READY state. lazyConnect: false, enableOfflineQueue: true, maxRetriesPerRequest: 1, retryStrategy(times) { // Exponential backoff up to 30s. ioredis keeps trying forever on its // own; we just slow it down so the logs aren't a hose. return Math.min(times * 1000, 30_000); }, }); client.on('error', (err) => { // Only log the first failure to avoid log spam during outages. if (!degraded) { console.warn('[redis] entering degraded mode:', err.message); degraded = true; } }); client.on('ready', () => { // Surface the ready transition in PM2/container logs so operators // can confirm the connection actually established. Distinct from // reconnect-after-outage which the line below logs at info. if (degraded) console.info('[redis] reconnected, leaving degraded mode'); else console.log('[redis] connected and ready'); degraded = false; }); } return client; } function isDegraded() { return degraded; } async function cacheGet(key) { if (degraded) return null; try { const raw = await getRedisClient().get(key); if (!raw) return null; try { return JSON.parse(raw); } catch { // Non-JSON values are returned as-is (some callers store plain strings). return raw; } } catch (err) { if (!degraded) console.warn('[redis] cacheGet failed:', err.message); return null; } } async function cacheSet(key, value, ttlSeconds) { if (degraded) return false; try { const payload = typeof value === 'string' ? value : JSON.stringify(value); if (ttlSeconds && ttlSeconds > 0) { await getRedisClient().set(key, payload, 'EX', ttlSeconds); } else { await getRedisClient().set(key, payload); } return true; } catch (err) { if (!degraded) console.warn('[redis] cacheSet failed:', err.message); return false; } } async function cacheDel(key) { if (degraded) return false; try { await getRedisClient().del(key); return true; } catch (err) { if (!degraded) console.warn('[redis] cacheDel failed:', err.message); return false; } } module.exports = { getRedisClient, cacheGet, cacheSet, cacheDel, isDegraded };