Sistema NEXUS v3.4 migrado con: Estructura principal: - core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles) - core/catalog: Catalogo de funcionalidades reutilizables - shared/knowledge-base: Base de conocimiento compartida - devtools/scripts: Herramientas de desarrollo - control-plane/registries: Control de servicios y CI/CD - orchestration/: Configuracion de orquestacion de agentes Proyectos incluidos (11): - gamilit (submodule -> GitHub) - trading-platform (OrbiquanTIA) - erp-suite con 5 verticales: - erp-core, construccion, vidrio-templado - mecanicas-diesel, retail, clinicas - betting-analytics - inmobiliaria-analytics - platform_marketing_content - pos-micro, erp-basico Configuracion: - .gitignore completo para Node.js/Python/Docker - gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git) - Sistema de puertos estandarizado (3005-3199) Generated with NEXUS v3.4 Migration System EPIC-010: Configuracion Git y Repositorios
450 lines
11 KiB
Markdown
450 lines
11 KiB
Markdown
# 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<Socket | null>(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 <Badge count={unread}><BellIcon /></Badge>;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 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
|