/** * WEBSOCKET GATEWAY - REFERENCE IMPLEMENTATION * * @description Gateway WebSocket para comunicación en tiempo real. * Soporta autenticación JWT, rooms y eventos tipados. * * @usage * ```typescript * // En el cliente * const socket = io('http://localhost:3000', { * auth: { token: 'jwt-token' } * }); * socket.emit('join-room', { roomId: 'room-1' }); * socket.on('message', (data) => console.log(data)); * ``` * * @origin gamilit (patrón base) */ import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect, ConnectedSocket, MessageBody, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { Logger, UseGuards } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; // Adaptar imports según proyecto // import { WsAuthGuard } from '../guards'; @WebSocketGateway({ cors: { origin: process.env.CORS_ORIGIN || '*', credentials: true, }, namespace: '/ws', }) export class AppWebSocketGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; private readonly logger = new Logger(AppWebSocketGateway.name); private readonly connectedClients = new Map(); constructor(private readonly jwtService: JwtService) {} /** * Manejar nueva conexión */ async handleConnection(client: Socket) { try { // Extraer y validar token const token = client.handshake.auth?.token || client.handshake.headers?.authorization?.replace('Bearer ', ''); if (!token) { this.disconnect(client, 'No token provided'); return; } const payload = this.jwtService.verify(token); // Almacenar cliente conectado this.connectedClients.set(client.id, { socketId: client.id, userId: payload.sub, role: payload.role, connectedAt: new Date(), }); // Auto-join a room del usuario client.join(`user:${payload.sub}`); this.logger.log(`Client connected: ${client.id} (user: ${payload.sub})`); client.emit('connected', { message: 'Successfully connected' }); } catch (error) { this.disconnect(client, 'Invalid token'); } } /** * Manejar desconexión */ handleDisconnect(client: Socket) { const clientInfo = this.connectedClients.get(client.id); this.connectedClients.delete(client.id); this.logger.log(`Client disconnected: ${client.id} (user: ${clientInfo?.userId || 'unknown'})`); } /** * Unirse a una sala */ @SubscribeMessage('join-room') handleJoinRoom( @ConnectedSocket() client: Socket, @MessageBody() data: { roomId: string }, ) { client.join(data.roomId); this.logger.debug(`Client ${client.id} joined room: ${data.roomId}`); return { event: 'room-joined', data: { roomId: data.roomId } }; } /** * Salir de una sala */ @SubscribeMessage('leave-room') handleLeaveRoom( @ConnectedSocket() client: Socket, @MessageBody() data: { roomId: string }, ) { client.leave(data.roomId); this.logger.debug(`Client ${client.id} left room: ${data.roomId}`); return { event: 'room-left', data: { roomId: data.roomId } }; } /** * Enviar mensaje a una sala */ @SubscribeMessage('message') handleMessage( @ConnectedSocket() client: Socket, @MessageBody() data: { roomId: string; message: string }, ) { const clientInfo = this.connectedClients.get(client.id); // Broadcast a la sala (excepto al sender) client.to(data.roomId).emit('message', { senderId: clientInfo?.userId, message: data.message, timestamp: new Date().toISOString(), }); return { event: 'message-sent', data: { success: true } }; } // ============ MÉTODOS PÚBLICOS PARA SERVICIOS ============ /** * Enviar notificación a un usuario específico */ sendToUser(userId: string, event: string, data: any) { this.server.to(`user:${userId}`).emit(event, data); } /** * Enviar a una sala */ sendToRoom(roomId: string, event: string, data: any) { this.server.to(roomId).emit(event, data); } /** * Broadcast a todos los clientes conectados */ broadcast(event: string, data: any) { this.server.emit(event, data); } /** * Obtener clientes conectados en una sala */ async getClientsInRoom(roomId: string): Promise { const sockets = await this.server.in(roomId).fetchSockets(); return sockets.map(s => s.id); } /** * Verificar si un usuario está conectado */ isUserConnected(userId: string): boolean { for (const client of this.connectedClients.values()) { if (client.userId === userId) return true; } return false; } // ============ HELPERS PRIVADOS ============ private disconnect(client: Socket, reason: string) { client.emit('error', { message: reason }); client.disconnect(true); this.logger.warn(`Client ${client.id} disconnected: ${reason}`); } } // ============ TIPOS ============ interface ConnectedClient { socketId: string; userId: string; role: string; connectedAt: Date; }