96 lines
3.0 KiB
JavaScript
96 lines
3.0 KiB
JavaScript
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 };
|