import Redis from 'ioredis' let pub let sub let client // 'ready' listeners (registered before connect; fire on initial connect AND each // reconnect). Used by services that need to reseed Valkey state from Postgres. const readyListeners = new Set() const attachReadyHandler = (instance) => { instance.on('ready', () => { for (const fn of readyListeners) { // Fire-and-forget; each listener owns its own error handling. Promise.resolve() .then(() => fn()) .catch((err) => console.error('[valkey] ready listener failed:', err)) } }) } export const onValkeyReady = (fn) => { readyListeners.add(fn) return () => readyListeners.delete(fn) } export const getValkeyClient = () => { if (!client) { const url = process.env.VALKEY_URL || 'redis://localhost:6379' client = new Redis(url) attachReadyHandler(client) } return client } export const getValkeyPub = () => { if (!pub) { const url = process.env.VALKEY_URL || 'redis://localhost:6379' pub = new Redis(url) } return pub } export const getValkeySub = () => { if (!sub) { const url = process.env.VALKEY_URL || 'redis://localhost:6379' sub = new Redis(url) } return sub } export const publish = async (channel, data) => { const pubClient = getValkeyPub() const numReceivers = await pubClient.publish(channel, JSON.stringify(data)) console.log(`[valkey] publish to ${channel} → ${numReceivers} receiver(s)`) } export const subscribe = (channel, callback) => { const subClient = getValkeySub() subClient.subscribe(channel) console.log(`[valkey] subscribed to ${channel}`) const handler = (ch, message) => { if (ch === channel) { console.log(`[valkey] received on ${channel}`) callback(JSON.parse(message)) } } subClient.on('message', handler) return () => { subClient.unsubscribe(channel) subClient.removeListener('message', handler) console.log(`[valkey] unsubscribed from ${channel}`) } } // --- Thin wrappers used by the mitra-availability mirror --- // // Each wrapper uses the shared `client` (separate ioredis instance from pub/sub // to keep subscribe state isolated). Callers in services/* wrap these in // try/catch and fall back to Postgres on error — see the plan doc. export const sadd = (key, ...members) => getValkeyClient().sadd(key, ...members) export const srem = (key, ...members) => getValkeyClient().srem(key, ...members) export const sismember = async (key, member) => (await getValkeyClient().sismember(key, member)) === 1 export const smembers = (key) => getValkeyClient().smembers(key) export const sdiff = (...keys) => getValkeyClient().sdiff(...keys) export const scard = (key) => getValkeyClient().scard(key) export const set = (key, value) => getValkeyClient().set(key, value) export const get = (key) => getValkeyClient().get(key) export const del = (...keys) => getValkeyClient().del(...keys) export const incr = (key) => getValkeyClient().incr(key) export const decr = (key) => getValkeyClient().decr(key) export const exists = (key) => getValkeyClient().exists(key) export const pipeline = () => getValkeyClient().pipeline() export const multi = () => getValkeyClient().multi()