feat: implement Redis-backed Socket.io adapter and user presence service for cross-instance communication

This commit is contained in:
pradeepkumar
2026-04-02 19:30:08 +05:30
parent 360969ae2c
commit 539fdf70e3
10 changed files with 211 additions and 20 deletions

View File

@@ -16,6 +16,7 @@ import { ConnectionRequestsService } from '../connection-requests/connection-req
import { PrismaService } from '../prisma/prisma.service';
import { CreateMessageDto } from './dto';
import { Logger } from '@nestjs/common';
import { RedisPresenceService } from '../common/services/redis-presence.service';
interface AuthenticatedSocket extends Socket {
userId?: string;
@@ -44,6 +45,7 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
private readonly prisma: PrismaService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly redisPresence: RedisPresenceService,
) {
// Periodic cleanup of userSocketMap and rateLimitMap every 5 minutes
setInterval(() => {
@@ -112,6 +114,9 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
this.logger.log(`Client connected successfully: ${client.id} (User: ${userId}, Role: ${payload.role})`);
// Join per-user room for Redis-backed cross-instance message routing
client.join(`user:${userId}`);
// Everything below is best-effort — don't let DB errors kill the socket connection
try {
// Auto-join admin users to admin_room for support chat notifications
@@ -123,7 +128,11 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
this.logger.warn(`Failed to join admin room for ${userId}: ${err.message}`);
}
// Update user online status in background (don't await)
// Update user online status in Redis + DB (don't await)
this.redisPresence.setOnline(userId).catch((err) => {
this.logger.warn(`Failed to set Redis presence for ${userId}: ${err.message}`);
});
this.messagesService.updateOnlineStatus(userId, true)
.then(() => {
this.broadcastUserStatus(userId, true);
@@ -158,13 +167,14 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
*/
async handleDisconnect(client: AuthenticatedSocket) {
if (client.userId) {
// Remove socket from tracking
// Remove socket from local tracking
const sockets = this.userSocketMap.get(client.userId);
if (sockets) {
sockets.delete(client.id);
// Only mark offline if no more sockets connected
// Only mark offline if no more local sockets connected
if (sockets.size === 0) {
this.userSocketMap.delete(client.userId);
await this.redisPresence.setOffline(client.userId);
await this.messagesService.updateOnlineStatus(client.userId, false);
this.broadcastUserStatus(client.userId, false);
}
@@ -245,7 +255,7 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
this.sendToUser(otherUserId, 'new_message', message);
// Auto-mark as DELIVERED if recipient is online
if (this.isUserOnline(otherUserId)) {
if (await this.isUserOnline(otherUserId)) {
const deliveredAt = new Date().toISOString();
await this.messagesService.markMessageDelivered(message.id);
// Update message object so sender gets correct status in callback
@@ -502,25 +512,17 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
}
/**
* Send message to a specific user (by userId)
* Send message to a specific user via per-user room (Redis-backed cross-instance)
*/
sendToUser(userId: string, event: string, data: any) {
const sockets = this.userSocketMap.get(userId);
if (sockets && sockets.size > 0) {
sockets.forEach((socketId) => {
this.server.to(socketId).emit(event, data);
});
this.logger.debug(`Sent ${event} to user ${userId} via ${sockets.size} socket(s)`);
} else {
this.logger.debug(`User ${userId} has no active sockets for ${event} - message saved in DB`);
}
this.server.to(`user:${userId}`).emit(event, data);
this.logger.debug(`Sent ${event} to user:${userId} room`);
}
/**
* Check if a user is online
* Check if a user is online (Redis-backed)
*/
isUserOnline(userId: string): boolean {
const sockets = this.userSocketMap.get(userId);
return sockets ? sockets.size > 0 : false;
async isUserOnline(userId: string): Promise<boolean> {
return this.redisPresence.isOnline(userId);
}
}