import Redis from 'ioredis'; import { CONFIG } from '../config'; import { logger } from '../utils/logger'; /** * Redis Bucket 1 — Presence * * Key schema: * presence:uid:{uid} → socket_id (TTL = PRESENCE_TTL_S) * status:uid:{uid} → "online" (TTL = PRESENCE_TTL_S) * * Separate client instance so Bucket 1 errors never affect Bucket 2. */ const presenceClient = new Redis(CONFIG.REDIS_PRESENCE_URL, { maxRetriesPerRequest: 2, connectTimeout: 5000, lazyConnect: false, enableReadyCheck: true, // Keep-alive so the HF container connection stays warm keepAlive: 10000, }); presenceClient.on('connect', () => logger.info('Presence Redis connected')); presenceClient.on('error', (err: Error) => logger.error('Presence Redis error', { error: err.message }) ); presenceClient.on('reconnecting', () => logger.warn('Presence Redis reconnecting')); export { presenceClient }; // ── Key builders ───────────────────────────────────────────────────────────── const presenceKey = (uid: string) => `presence:uid:${uid}`; const statusKey = (uid: string) => `status:uid:${uid}`; // ── Operations ─────────────────────────────────────────────────────────────── /** Map uid → socketId, mark user online. Called on WS connect. */ export async function setOnline(uid: string, socketId: string): Promise { const ttl = CONFIG.PRESENCE_TTL_S; // MULTI/EXEC keeps it atomic await presenceClient .multi() .set(presenceKey(uid), socketId, 'EX', ttl) .set(statusKey(uid), 'online', 'EX', ttl) .exec(); } /** Refresh TTL on heartbeat. Does not overwrite the socket id. */ export async function refreshPresence(uid: string): Promise { const ttl = CONFIG.PRESENCE_TTL_S; await presenceClient .multi() .expire(presenceKey(uid), ttl) .expire(statusKey(uid), ttl) .exec(); } /** Remove user from presence. Called on WS close. */ export async function setOffline(uid: string): Promise { await presenceClient .multi() .del(presenceKey(uid)) .del(statusKey(uid)) .exec(); } /** Returns the socket id for a uid, or null if offline. */ export async function getSocketId(uid: string): Promise { return presenceClient.get(presenceKey(uid)); } /** Returns true if the user is online. */ export async function isOnline(uid: string): Promise { const status = await presenceClient.get(statusKey(uid)); return status === 'online'; }