import { createClient, RedisClientType } from 'redis'; import { logError, logStartup, logWarning } from './Logger'; export interface ActiveChatData { chatId: string; participants: string[]; lastActivity: Date; messageCount: number; chatType: 'direct' | 'group' | 'game'; gameId?: string; name?: string; } export interface ActiveUserData { userId: string; activeChatIds: string[]; lastActivity: Date; isOnline: boolean; } export class RedisService { private static instance: RedisService; private client: RedisClientType; private isConnected: boolean = false; private constructor() { const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; this.client = createClient({ url: redisUrl, socket: { reconnectStrategy: (retries) => Math.min(retries * 50, 500) } }); this.client.on('error', (err) => { logError('Redis connection error', err); this.isConnected = false; }); this.client.on('connect', () => { logStartup('Redis client connected successfully'); this.isConnected = true; }); this.client.on('disconnect', () => { logWarning('Redis client disconnected'); this.isConnected = false; }); } public static getInstance(): RedisService { if (!RedisService.instance) { RedisService.instance = new RedisService(); } return RedisService.instance; } public async connect(): Promise { try { if (!this.isConnected) { await this.client.connect(); } } catch (error) { logError('Failed to connect to Redis', error as Error); throw error; } } public async disconnect(): Promise { try { if (this.isConnected) { await this.client.disconnect(); } } catch (error) { logError('Failed to disconnect from Redis', error as Error); } } public async setActiveChat(chatId: string, chatData: ActiveChatData): Promise { try { const key = `active_chat:${chatId}`; await this.client.hSet(key, { chatId: chatData.chatId, participants: JSON.stringify(chatData.participants), lastActivity: chatData.lastActivity.toISOString(), messageCount: chatData.messageCount.toString(), chatType: chatData.chatType, gameId: chatData.gameId || '', name: chatData.name || '' }); // Set expiration for 1 hour of inactivity await this.client.expire(key, 3600); } catch (error) { logError(`Failed to set active chat ${chatId}`, error as Error); } } public async getActiveChat(chatId: string): Promise { try { const key = `active_chat:${chatId}`; const data = await this.client.hGetAll(key); if (!data.chatId) { return null; } return { chatId: data.chatId, participants: JSON.parse(data.participants), lastActivity: new Date(data.lastActivity), messageCount: parseInt(data.messageCount, 10), chatType: data.chatType as 'direct' | 'group' | 'game', gameId: data.gameId || undefined, name: data.name || undefined }; } catch (error) { logError(`Failed to get active chat ${chatId}`, error as Error); return null; } } public async removeActiveChat(chatId: string): Promise { try { const key = `active_chat:${chatId}`; await this.client.del(key); } catch (error) { logError(`Failed to remove active chat ${chatId}`, error as Error); } } public async getAllActiveChats(): Promise { try { const pattern = 'active_chat:*'; const keys = await this.client.keys(pattern); const chats: ActiveChatData[] = []; for (const key of keys) { const data = await this.client.hGetAll(key); if (data.chatId) { chats.push({ chatId: data.chatId, participants: JSON.parse(data.participants), lastActivity: new Date(data.lastActivity), messageCount: parseInt(data.messageCount, 10), chatType: data.chatType as 'direct' | 'group' | 'game', gameId: data.gameId || undefined, name: data.name || undefined }); } } return chats; } catch (error) { logError('Failed to get all active chats', error as Error); return []; } } public async setActiveUser(userId: string, userData: ActiveUserData): Promise { try { const key = `active_user:${userId}`; await this.client.hSet(key, { userId: userData.userId, activeChatIds: JSON.stringify(userData.activeChatIds), lastActivity: userData.lastActivity.toISOString(), isOnline: userData.isOnline.toString() }); // Set expiration for 2 hours await this.client.expire(key, 7200); } catch (error) { logError(`Failed to set active user ${userId}`, error as Error); } } public async getActiveUser(userId: string): Promise { try { const key = `active_user:${userId}`; const data = await this.client.hGetAll(key); if (!data.userId) { return null; } return { userId: data.userId, activeChatIds: JSON.parse(data.activeChatIds), lastActivity: new Date(data.lastActivity), isOnline: data.isOnline === 'true' }; } catch (error) { logError(`Failed to get active user ${userId}`, error as Error); return null; } } public async removeActiveUser(userId: string): Promise { try { const key = `active_user:${userId}`; await this.client.del(key); } catch (error) { logError(`Failed to remove active user ${userId}`, error as Error); } } public async addUserToChat(userId: string, chatId: string): Promise { try { const userData = await this.getActiveUser(userId) || { userId, activeChatIds: [], lastActivity: new Date(), isOnline: true }; if (!userData.activeChatIds.includes(chatId)) { userData.activeChatIds.push(chatId); userData.lastActivity = new Date(); await this.setActiveUser(userId, userData); } } catch (error) { logError(`Failed to add user ${userId} to chat ${chatId}`, error as Error); } } public async removeUserFromChat(userId: string, chatId: string): Promise { try { const userData = await this.getActiveUser(userId); if (userData) { userData.activeChatIds = userData.activeChatIds.filter(id => id !== chatId); userData.lastActivity = new Date(); await this.setActiveUser(userId, userData); } } catch (error) { logError(`Failed to remove user ${userId} from chat ${chatId}`, error as Error); } } public async getUserActiveChats(userId: string): Promise { try { const userData = await this.getActiveUser(userId); return userData?.activeChatIds || []; } catch (error) { logError(`Failed to get active chats for user ${userId}`, error as Error); return []; } } public async updateChatActivity(chatId: string, messageCount?: number): Promise { try { const chatData = await this.getActiveChat(chatId); if (chatData) { chatData.lastActivity = new Date(); if (messageCount !== undefined) { chatData.messageCount = messageCount; } await this.setActiveChat(chatId, chatData); } } catch (error) { logError(`Failed to update chat activity ${chatId}`, error as Error); } } public async getInactiveChats(inactivityMinutes: number): Promise { try { const cutoffTime = new Date(Date.now() - inactivityMinutes * 60 * 1000); const allChats = await this.getAllActiveChats(); return allChats .filter(chat => chat.lastActivity < cutoffTime) .map(chat => chat.chatId); } catch (error) { logError('Failed to get inactive chats', error as Error); return []; } } public async cleanupInactiveChats(inactivityMinutes: number): Promise { try { const inactiveChats = await this.getInactiveChats(inactivityMinutes); for (const chatId of inactiveChats) { await this.removeActiveChat(chatId); } return inactiveChats; } catch (error) { logError('Failed to cleanup inactive chats', error as Error); return []; } } public async ping(): Promise { try { const result = await this.client.ping(); return result === 'PONG'; } catch (error) { logError('Redis ping failed', error as Error); return false; } } public isRedisConnected(): boolean { return this.isConnected; } // Generic Redis methods for game data public async get(key: string): Promise { try { return await this.client.get(key); } catch (error) { logError(`Failed to get key ${key}`, error as Error); return null; } } public async set(key: string, value: string): Promise { try { await this.client.set(key, value); } catch (error) { logError(`Failed to set key ${key}`, error as Error); } } public async setWithExpiry(key: string, value: string, expirySeconds: number): Promise { try { await this.client.setEx(key, expirySeconds, value); } catch (error) { logError(`Failed to set key ${key} with expiry`, error as Error); } } public async del(key: string): Promise { try { await this.client.del(key); } catch (error) { logError(`Failed to delete key ${key}`, error as Error); } } public async setAdd(key: string, member: string): Promise { try { await this.client.sAdd(key, member); } catch (error) { logError(`Failed to add member to set ${key}`, error as Error); } } public async setRemove(key: string, member: string): Promise { try { await this.client.sRem(key, member); } catch (error) { logError(`Failed to remove member from set ${key}`, error as Error); } } public async setMembers(key: string): Promise { try { return await this.client.sMembers(key); } catch (error) { logError(`Failed to get members of set ${key}`, error as Error); return []; } } public async exists(key: string): Promise { try { const result = await this.client.exists(key); return result === 1; } catch (error) { logError(`Failed to check existence of key ${key}`, error as Error); return false; } } }