import { Injectable, OnModuleDestroy } from '@nestjs/common'; import Redis from 'ioredis'; const ONLINE_KEY = 'genex:presence:online'; const DAU_KEY_PREFIX = 'genex:dau:'; const ONLINE_WINDOW = 180; // 3 minutes @Injectable() export class PresenceRedisService implements OnModuleDestroy { private readonly redis: Redis; constructor() { this.redis = new Redis({ host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379', 10), password: process.env.REDIS_PASSWORD || undefined, db: parseInt(process.env.REDIS_DB || '0', 10), }); } async onModuleDestroy() { await this.redis.quit(); } /** Update user heartbeat timestamp */ async updatePresence(userId: string): Promise { const now = Math.floor(Date.now() / 1000); await this.redis.zadd(ONLINE_KEY, now, userId); } /** Count users online within window */ async countOnline(): Promise { const threshold = Math.floor(Date.now() / 1000) - ONLINE_WINDOW; return this.redis.zcount(ONLINE_KEY, threshold, '+inf'); } /** Add user/installId to HyperLogLog DAU */ async addDauIdentifier(date: string, identifier: string): Promise { const key = `${DAU_KEY_PREFIX}${date}`; await this.redis.pfadd(key, identifier); // Auto-expire after 48 hours await this.redis.expire(key, 48 * 3600); } /** Get approximate DAU from HyperLogLog */ async getApproxDau(date: string): Promise { return this.redis.pfcount(`${DAU_KEY_PREFIX}${date}`); } /** Clean up expired presence data (>24h old) */ async cleanupExpired(): Promise { const cutoff = Math.floor(Date.now() / 1000) - 24 * 3600; return this.redis.zremrangebyscore(ONLINE_KEY, '-inf', cutoff); } getWindowSeconds(): number { return ONLINE_WINDOW; } }