Files
SerpentRace/SerpentRace_Backend/src/Application/Services/RedisService.ts
T
2025-09-15 19:00:35 +02:00

376 lines
12 KiB
TypeScript

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<void> {
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<void> {
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<void> {
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<ActiveChatData | null> {
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<void> {
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<ActiveChatData[]> {
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<void> {
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<ActiveUserData | null> {
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<void> {
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<void> {
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<void> {
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<string[]> {
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<void> {
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<string[]> {
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<string[]> {
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<boolean> {
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<string | null> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<string[]> {
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<boolean> {
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;
}
}
}