import { createClient } from 'redis'; import { config } from './config.js'; class InMemoryRedisClient { constructor() { this.mode = 'memory'; this._strings = new Map(); this._sortedSets = new Map(); } _now() { return Date.now(); } _purgeExpiredStrings() { const now = this._now(); for (const [key, entry] of this._strings.entries()) { if (entry.expiresAt !== null && entry.expiresAt <= now) { this._strings.delete(key); } } } _getStringEntry(key) { this._purgeExpiredStrings(); return this._strings.get(key) ?? null; } _getSortedSet(key) { let set = this._sortedSets.get(key); if (!set) { set = new Map(); this._sortedSets.set(key, set); } return set; } async connect() { return this; } on() { return this; } async zAdd(key, entries) { const set = this._getSortedSet(key); for (const entry of entries) { set.set(String(entry.value), Number(entry.score)); } } async zCard(key) { return this._getSortedSet(key).size; } async zRem(key, ...values) { const set = this._getSortedSet(key); let removed = 0; for (const value of values) { if (set.delete(String(value))) { removed += 1; } } return removed; } async zRange(key, start, stop) { const items = Array.from(this._getSortedSet(key).entries()) .sort((left, right) => left[1] - right[1] || left[0].localeCompare(right[0])) .map(([value]) => value); const normalizedStop = stop < 0 ? items.length + stop : stop; return items.slice(start, normalizedStop + 1); } async set(key, value, options = {}) { const existing = this._getStringEntry(key); if (options.NX && existing) { return null; } let expiresAt = null; if (typeof options.PX === 'number') { expiresAt = this._now() + options.PX; } else if (typeof options.EX === 'number') { expiresAt = this._now() + (options.EX * 1000); } this._strings.set(key, { value: String(value), expiresAt }); return 'OK'; } async get(key) { const entry = this._getStringEntry(key); return entry ? entry.value : null; } async del(key) { this._purgeExpiredStrings(); const existed = this._strings.delete(key); return existed ? 1 : 0; } } export async function createRedis() { const client = createClient({ url: config.redisUrl, socket: { connectTimeout: 3000, reconnectStrategy: false, }, }); client.on('error', (err) => { console.error('[redis] error', err); }); try { await client.connect(); client.mode = 'redis'; console.log('[redis] connected'); return client; } catch (error) { try { if (typeof client.disconnect === 'function') { await client.disconnect(); } else if (typeof client.quit === 'function') { await client.quit(); } } catch { // Ignore cleanup errors and continue with in-memory fallback. } console.warn('[redis] unavailable, using in-memory fallback:', error instanceof Error ? error.message : String(error)); return new InMemoryRedisClient(); } } export function key(...parts) { return config.redisKeyPrefix + parts.join(''); }