feat: implement Redis-backed Socket.io adapter and user presence service for cross-instance communication
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user