# Comunicación WebSocket **Versión:** 1.0.0 **Origen:** projects/gamilit, projects/trading-platform **Estado:** Producción **Última actualización:** 2025-12-08 --- ## Descripción Sistema de comunicación en tiempo real via Socket.IO: - Conexiones WebSocket autenticadas con JWT - Rooms por usuario para mensajes privados - Broadcasting para eventos globales - Tracking de usuarios conectados - Multi-dispositivo (un usuario, múltiples sockets) - Integración con sistema de notificaciones --- ## Características | Característica | Descripción | |----------------|-------------| | Autenticación | JWT en handshake | | Rooms | Por usuario (`user:{userId}`) | | Multi-socket | Un usuario puede tener múltiples conexiones | | Broadcast | Eventos a todos los conectados | | Typed events | Enum de eventos tipados | | CORS | Configuración flexible | | Transports | WebSocket + polling fallback | --- ## Stack Tecnológico ```yaml backend: framework: NestJS library: Socket.IO auth: JWT frontend: library: socket.io-client packages: - "@nestjs/websockets" - "@nestjs/platform-socket.io" - "socket.io" - "socket.io-client" ``` --- ## Dependencias NPM ```json { "@nestjs/websockets": "^10.x", "@nestjs/platform-socket.io": "^10.x", "socket.io": "^4.x", "@nestjs/jwt": "^10.x" } ``` Frontend: ```json { "socket.io-client": "^4.x" } ``` --- ## Tablas Requeridas No requiere tablas adicionales. Usa autenticación existente (JWT). --- ## Estructura del Módulo ``` websocket/ ├── websocket.module.ts ├── gateways/ │ └── notifications.gateway.ts # Gateway principal ├── services/ │ └── websocket.service.ts # API para otros módulos ├── guards/ │ └── ws-jwt.guard.ts # Autenticación JWT └── types/ └── websocket.types.ts # Eventos y tipos ``` --- ## Arquitectura ``` ┌─────────────────┐ ┌─────────────────┐ │ Frontend │ │ Frontend │ │ (User A) │ │ (User A) │ │ Device 1 │ │ Device 2 │ └────────┬────────┘ └────────┬────────┘ │ │ │ WebSocket │ WebSocket │ │ ▼ ▼ ┌─────────────────────────────────────────────┐ │ Socket.IO Server │ │ ┌───────────────────────────────────────┐ │ │ │ Room: user:user-a-uuid │ │ │ │ - socket-id-1 │ │ │ │ - socket-id-2 │ │ │ └───────────────────────────────────────┘ │ │ │ │ userSockets Map: │ │ user-a-uuid → Set(socket-id-1, socket-id-2)│ └─────────────────────────────────────────────┘ ``` --- ## Eventos Disponibles ### Eventos del Servidor (Server → Client) | Evento | Descripción | Payload | |--------|-------------|---------| | `authenticated` | Conexión autenticada | `{ success, userId, email }` | | `error` | Error en operación | `{ message }` | | `notification:new` | Nueva notificación | `{ notification, timestamp }` | | `notification:read` | Notificación leída | `{ notificationId, success }` | | `notification:deleted` | Notificación eliminada | `{ notificationId }` | | `notification:unread_count` | Contador actualizado | `{ unreadCount }` | | `achievement:unlocked` | Logro desbloqueado | `{ achievementId, title, ... }` | | `rank:updated` | Cambio de rango | `{ newRank, oldRank }` | | `xp:gained` | XP ganado | `{ amount, source, totalXp }` | | `leaderboard:updated` | Leaderboard actualizado | `{ leaderboard[] }` | ### Eventos del Cliente (Client → Server) | Evento | Descripción | Payload | |--------|-------------|---------| | `notification:mark_read` | Marcar como leída | `{ notificationId }` | --- ## Uso Rápido ### 1. Emitir desde otro servicio ```typescript import { WebSocketService } from '@/modules/websocket'; @Injectable() export class NotificationService { constructor(private readonly wsService: WebSocketService) {} async sendNotification(userId: string, notification: any) { // Guardar en DB... // Emitir en tiempo real this.wsService.emitNotificationToUser(userId, notification); } } ``` ### 2. Frontend - Conectar ```typescript import { io, Socket } from 'socket.io-client'; const socket: Socket = io('http://localhost:3000', { path: '/socket.io/', transports: ['websocket', 'polling'], auth: { token: localStorage.getItem('accessToken'), }, }); // Conexión establecida socket.on('authenticated', (data) => { console.log('Conectado:', data.email); }); // Escuchar notificaciones socket.on('notification:new', (data) => { showToast(data.notification.title); updateNotificationBadge(); }); // Error de autenticación socket.on('error', (data) => { console.error('Error:', data.message); }); // Desconexión socket.on('disconnect', (reason) => { console.log('Desconectado:', reason); }); ``` ### 3. Frontend - Enviar eventos ```typescript // Marcar notificación como leída socket.emit('notification:mark_read', { notificationId: 'uuid' }); ``` ### 4. Verificar si usuario está conectado ```typescript // En cualquier servicio const isOnline = this.wsService.isUserConnected(userId); if (isOnline) { // Enviar por WebSocket (instantáneo) this.wsService.emitNotificationToUser(userId, notification); } else { // Enviar por email o push (asíncrono) await this.emailService.send(userId, notification); } ``` --- ## Flujo de Autenticación ``` 1. Cliente conecta con token JWT │ ▼ 2. WsJwtGuard verifica token │ ├─► Token inválido → disconnect() │ └─► Token válido │ ▼ 3. Extraer userId, email, role del payload │ ▼ 4. Adjuntar userData al socket │ ▼ 5. Join room: user:{userId} │ ▼ 6. Registrar en userSockets Map │ ▼ 7. Emitir 'authenticated' al cliente ``` --- ## Patrones de Uso ### Notificación a un usuario ```typescript // El usuario recibe en todos sus dispositivos conectados this.wsService.emitNotificationToUser(userId, { id: notification.id, title: notification.title, message: notification.message, }); ``` ### Notificación a múltiples usuarios ```typescript // Ejemplo: notificar a todos los miembros de un grupo const memberIds = ['uuid1', 'uuid2', 'uuid3']; this.wsService.emitNotificationToUsers(memberIds, { type: 'group_message', groupId: group.id, message: 'Nuevo mensaje en el grupo', }); ``` ### Broadcast global ```typescript // Ejemplo: actualización del leaderboard this.wsService.broadcastLeaderboardUpdate(newLeaderboard); ``` ### Eventos de gamificación ```typescript // Logro desbloqueado this.wsService.emitAchievementUnlocked(userId, { achievementId: 'ach-001', title: 'Primer Login', description: 'Has iniciado sesión por primera vez', icon: '/icons/first-login.png', }); // XP ganado this.wsService.emitXpGained(userId, { amount: 100, source: 'daily_mission', totalXp: 1500, }); // Subida de rango this.wsService.emitRankUpdated(userId, { oldRank: 'Novato', newRank: 'Aprendiz', xpRequired: 2000, }); ``` --- ## Variables de Entorno ```env # WebSocket WS_PORT=3000 # Puerto (mismo que HTTP) WS_PATH=/socket.io/ # Path del endpoint # CORS CORS_ORIGIN=http://localhost:3000,http://localhost:5173 # JWT (compartido con auth) JWT_SECRET=your-secret-key ``` --- ## Frontend React Hook ```typescript // hooks/useSocket.ts import { useEffect, useRef, useCallback } from 'react'; import { io, Socket } from 'socket.io-client'; import { useAuth } from './useAuth'; export function useSocket() { const { token } = useAuth(); const socketRef = useRef(null); useEffect(() => { if (!token) return; socketRef.current = io(import.meta.env.VITE_API_URL, { path: '/socket.io/', transports: ['websocket', 'polling'], auth: { token }, }); socketRef.current.on('connect', () => { console.log('Socket connected'); }); socketRef.current.on('disconnect', (reason) => { console.log('Socket disconnected:', reason); }); return () => { socketRef.current?.disconnect(); }; }, [token]); const on = useCallback((event: string, handler: (data: any) => void) => { socketRef.current?.on(event, handler); return () => socketRef.current?.off(event, handler); }, []); const emit = useCallback((event: string, data: any) => { socketRef.current?.emit(event, data); }, []); return { socket: socketRef.current, on, emit }; } // Uso en componente function NotificationBell() { const { on } = useSocket(); const [unread, setUnread] = useState(0); useEffect(() => { return on('notification:unread_count', (data) => { setUnread(data.unreadCount); }); }, [on]); return ; } ``` --- ## Consideraciones de Escalabilidad ### Múltiples instancias (Horizontal Scaling) Para escalar horizontalmente con múltiples instancias del servidor: ```typescript // Usar Redis adapter para Socket.IO import { createAdapter } from '@socket.io/redis-adapter'; import { createClient } from 'redis'; const pubClient = createClient({ url: process.env.REDIS_URL }); const subClient = pubClient.duplicate(); io.adapter(createAdapter(pubClient, subClient)); ``` ### Sticky Sessions Con load balancer, configurar sticky sessions para que un cliente siempre llegue a la misma instancia: ```nginx upstream websocket { ip_hash; # Sticky sessions por IP server backend1:3000; server backend2:3000; } ``` --- ## Adaptaciones Necesarias 1. **Eventos**: Definir eventos específicos de tu aplicación 2. **Rooms**: Agregar rooms adicionales (grupos, chats, etc.) 3. **Auth**: Ajustar extracción de datos del JWT 4. **Scaling**: Configurar Redis adapter si múltiples instancias 5. **CORS**: Ajustar orígenes permitidos --- ## Referencias - [Socket.IO Documentation](https://socket.io/docs/v4/) - [NestJS WebSockets](https://docs.nestjs.com/websockets/gateways) - [Socket.IO Redis Adapter](https://socket.io/docs/v4/redis-adapter/) --- **Mantenido por:** Sistema NEXUS **Proyectos origen:** Gamilit Platform, Trading Platform