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
811 lines
19 KiB
Markdown
811 lines
19 KiB
Markdown
# Guía de Implementación: WebSocket
|
|
|
|
**Versión:** 1.0.0
|
|
**Tiempo estimado:** 1-2 horas
|
|
**Complejidad:** Media
|
|
|
|
---
|
|
|
|
## Pre-requisitos
|
|
|
|
- [ ] Proyecto NestJS existente
|
|
- [ ] Sistema de autenticación JWT funcionando
|
|
- [ ] (Opcional) Redis para escalabilidad horizontal
|
|
|
|
---
|
|
|
|
## Paso 1: Instalar Dependencias
|
|
|
|
```bash
|
|
npm install @nestjs/websockets @nestjs/platform-socket.io socket.io
|
|
|
|
# Ya deberías tener @nestjs/jwt del módulo de auth
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 2: Crear Estructura de Directorios
|
|
|
|
```bash
|
|
mkdir -p src/modules/websocket/gateways
|
|
mkdir -p src/modules/websocket/services
|
|
mkdir -p src/modules/websocket/guards
|
|
mkdir -p src/modules/websocket/types
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 3: Definir Tipos y Eventos
|
|
|
|
```typescript
|
|
// src/modules/websocket/types/websocket.types.ts
|
|
|
|
/**
|
|
* Enum de eventos WebSocket
|
|
* Usar namespace:action para claridad
|
|
*/
|
|
export enum SocketEvent {
|
|
// Conexión
|
|
AUTHENTICATED = 'authenticated',
|
|
ERROR = 'error',
|
|
|
|
// Notificaciones
|
|
NEW_NOTIFICATION = 'notification:new',
|
|
NOTIFICATION_READ = 'notification:read',
|
|
NOTIFICATION_DELETED = 'notification:deleted',
|
|
UNREAD_COUNT_UPDATED = 'notification:unread_count',
|
|
MARK_AS_READ = 'notification:mark_read',
|
|
|
|
// Genéricos
|
|
DATA_UPDATED = 'data:updated',
|
|
USER_ONLINE = 'user:online',
|
|
USER_OFFLINE = 'user:offline',
|
|
}
|
|
|
|
/**
|
|
* Datos del usuario en el socket
|
|
*/
|
|
export interface SocketUserData {
|
|
userId: string;
|
|
email: string;
|
|
role: string;
|
|
tenantId?: string;
|
|
}
|
|
|
|
/**
|
|
* Payload base con timestamp
|
|
*/
|
|
export interface BasePayload {
|
|
timestamp: string;
|
|
}
|
|
|
|
/**
|
|
* Payload de notificación
|
|
*/
|
|
export interface NotificationPayload extends BasePayload {
|
|
notification: {
|
|
id: string;
|
|
title: string;
|
|
message: string;
|
|
type: string;
|
|
data?: Record<string, any>;
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 4: Crear Guard de Autenticación JWT
|
|
|
|
```typescript
|
|
// src/modules/websocket/guards/ws-jwt.guard.ts
|
|
import {
|
|
CanActivate,
|
|
ExecutionContext,
|
|
Injectable,
|
|
Logger,
|
|
} from '@nestjs/common';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { WsException } from '@nestjs/websockets';
|
|
import { Socket } from 'socket.io';
|
|
|
|
/**
|
|
* Socket autenticado con datos del usuario
|
|
*/
|
|
export interface AuthenticatedSocket extends Socket {
|
|
userData?: {
|
|
userId: string;
|
|
email: string;
|
|
role: string;
|
|
tenantId?: string;
|
|
};
|
|
}
|
|
|
|
@Injectable()
|
|
export class WsJwtGuard implements CanActivate {
|
|
private readonly logger = new Logger(WsJwtGuard.name);
|
|
|
|
constructor(private readonly jwtService: JwtService) {}
|
|
|
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
try {
|
|
const client: AuthenticatedSocket = context.switchToWs().getClient();
|
|
|
|
// Extraer token de auth o query params
|
|
const token =
|
|
client.handshake.auth?.token ||
|
|
client.handshake.query?.token;
|
|
|
|
if (!token || typeof token !== 'string') {
|
|
this.logger.warn('WebSocket: conexión sin token');
|
|
throw new WsException('Token de autenticación requerido');
|
|
}
|
|
|
|
// Verificar JWT
|
|
const payload = await this.jwtService.verifyAsync(token);
|
|
|
|
// Adjuntar datos al socket
|
|
client.userData = {
|
|
userId: payload.sub,
|
|
email: payload.email,
|
|
role: payload.role,
|
|
tenantId: payload.tenant_id,
|
|
};
|
|
|
|
this.logger.log(`WebSocket autenticado: ${client.userData.email}`);
|
|
return true;
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : 'Error desconocido';
|
|
this.logger.warn(`WebSocket auth falló: ${msg}`);
|
|
throw new WsException('Autenticación fallida');
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 5: Crear Gateway Principal
|
|
|
|
```typescript
|
|
// src/modules/websocket/gateways/notifications.gateway.ts
|
|
import {
|
|
WebSocketGateway,
|
|
WebSocketServer,
|
|
SubscribeMessage,
|
|
OnGatewayConnection,
|
|
OnGatewayDisconnect,
|
|
OnGatewayInit,
|
|
ConnectedSocket,
|
|
MessageBody,
|
|
} from '@nestjs/websockets';
|
|
import { Logger, UseGuards } from '@nestjs/common';
|
|
import { Server } from 'socket.io';
|
|
import { WsJwtGuard, AuthenticatedSocket } from '../guards/ws-jwt.guard';
|
|
import { SocketEvent } from '../types/websocket.types';
|
|
|
|
@WebSocketGateway({
|
|
cors: {
|
|
origin: process.env.CORS_ORIGIN?.split(',') || [
|
|
'http://localhost:3000',
|
|
'http://localhost:5173',
|
|
],
|
|
credentials: true,
|
|
methods: ['GET', 'POST'],
|
|
},
|
|
path: '/socket.io/',
|
|
transports: ['websocket', 'polling'],
|
|
})
|
|
export class NotificationsGateway
|
|
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
|
|
{
|
|
@WebSocketServer()
|
|
server!: Server;
|
|
|
|
private readonly logger = new Logger(NotificationsGateway.name);
|
|
|
|
// Map: userId -> Set de socketIds
|
|
private userSockets = new Map<string, Set<string>>();
|
|
|
|
/**
|
|
* Lifecycle: Gateway inicializado
|
|
*/
|
|
afterInit(server: Server) {
|
|
this.logger.log('WebSocket Gateway inicializado');
|
|
}
|
|
|
|
/**
|
|
* Lifecycle: Cliente conectado
|
|
*/
|
|
@UseGuards(WsJwtGuard)
|
|
async handleConnection(client: AuthenticatedSocket) {
|
|
const userId = client.userData?.userId;
|
|
const userEmail = client.userData?.email;
|
|
|
|
if (!userId) {
|
|
this.logger.warn('Conexión rechazada: sin datos de usuario');
|
|
client.disconnect();
|
|
return;
|
|
}
|
|
|
|
this.logger.log(`Cliente conectado: ${userEmail} (${client.id})`);
|
|
|
|
// Registrar socket del usuario
|
|
if (!this.userSockets.has(userId)) {
|
|
this.userSockets.set(userId, new Set());
|
|
}
|
|
this.userSockets.get(userId)!.add(client.id);
|
|
|
|
// Unirse a room personal
|
|
await client.join(`user:${userId}`);
|
|
this.logger.debug(`Socket ${client.id} unido a room: user:${userId}`);
|
|
|
|
// Emitir evento de autenticación exitosa
|
|
client.emit(SocketEvent.AUTHENTICATED, {
|
|
success: true,
|
|
userId,
|
|
email: userEmail,
|
|
socketId: client.id,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Lifecycle: Cliente desconectado
|
|
*/
|
|
async handleDisconnect(client: AuthenticatedSocket) {
|
|
const userId = client.userData?.userId;
|
|
const userEmail = client.userData?.email;
|
|
|
|
if (userId) {
|
|
const sockets = this.userSockets.get(userId);
|
|
if (sockets) {
|
|
sockets.delete(client.id);
|
|
if (sockets.size === 0) {
|
|
this.userSockets.delete(userId);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.logger.log(`Cliente desconectado: ${userEmail} (${client.id})`);
|
|
}
|
|
|
|
/**
|
|
* Handler: Marcar notificación como leída
|
|
*/
|
|
@UseGuards(WsJwtGuard)
|
|
@SubscribeMessage(SocketEvent.MARK_AS_READ)
|
|
async handleMarkAsRead(
|
|
@ConnectedSocket() client: AuthenticatedSocket,
|
|
@MessageBody() data: { notificationId: string },
|
|
) {
|
|
try {
|
|
const userId = client.userData!.userId;
|
|
const { notificationId } = data;
|
|
|
|
this.logger.debug(
|
|
`Usuario ${userId} marca notificación ${notificationId} como leída`,
|
|
);
|
|
|
|
// Aquí podrías llamar a NotificationService.markAsRead(notificationId, userId)
|
|
// await this.notificationService.markAsRead(notificationId, userId);
|
|
|
|
// Confirmar al cliente
|
|
client.emit(SocketEvent.NOTIFICATION_READ, {
|
|
notificationId,
|
|
success: true,
|
|
});
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : 'Error desconocido';
|
|
this.logger.error('Error marcando como leída:', error);
|
|
client.emit(SocketEvent.ERROR, {
|
|
message: 'Error al marcar notificación como leída',
|
|
});
|
|
return { success: false, error: errorMessage };
|
|
}
|
|
}
|
|
|
|
// =====================================================
|
|
// Métodos públicos para emitir eventos
|
|
// =====================================================
|
|
|
|
/**
|
|
* Emitir a un usuario específico (todos sus dispositivos)
|
|
*/
|
|
emitToUser(userId: string, event: SocketEvent, data: any) {
|
|
const room = `user:${userId}`;
|
|
this.server.to(room).emit(event, {
|
|
...data,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
this.logger.debug(`Emitido ${event} a usuario ${userId}`);
|
|
}
|
|
|
|
/**
|
|
* Emitir a múltiples usuarios
|
|
*/
|
|
emitToUsers(userIds: string[], event: SocketEvent, data: any) {
|
|
userIds.forEach((userId) => {
|
|
this.emitToUser(userId, event, data);
|
|
});
|
|
this.logger.debug(`Emitido ${event} a ${userIds.length} usuarios`);
|
|
}
|
|
|
|
/**
|
|
* Broadcast a todos los conectados
|
|
*/
|
|
broadcast(event: SocketEvent, data: any) {
|
|
this.server.emit(event, {
|
|
...data,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
this.logger.debug(`Broadcast ${event} a todos los usuarios`);
|
|
}
|
|
|
|
/**
|
|
* Emitir a una room específica
|
|
*/
|
|
emitToRoom(room: string, event: SocketEvent, data: any) {
|
|
this.server.to(room).emit(event, {
|
|
...data,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
this.logger.debug(`Emitido ${event} a room ${room}`);
|
|
}
|
|
|
|
// =====================================================
|
|
// Métodos de utilidad
|
|
// =====================================================
|
|
|
|
/**
|
|
* Obtener cantidad de usuarios conectados
|
|
*/
|
|
getConnectedUsersCount(): number {
|
|
return this.userSockets.size;
|
|
}
|
|
|
|
/**
|
|
* Verificar si usuario está conectado
|
|
*/
|
|
isUserConnected(userId: string): boolean {
|
|
return (
|
|
this.userSockets.has(userId) && this.userSockets.get(userId)!.size > 0
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Obtener cantidad de sockets de un usuario
|
|
*/
|
|
getUserSocketCount(userId: string): number {
|
|
return this.userSockets.get(userId)?.size || 0;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 6: Crear Servicio de WebSocket
|
|
|
|
```typescript
|
|
// src/modules/websocket/services/websocket.service.ts
|
|
import { Injectable, Logger } from '@nestjs/common';
|
|
import { NotificationsGateway } from '../gateways/notifications.gateway';
|
|
import { SocketEvent } from '../types/websocket.types';
|
|
|
|
/**
|
|
* WebSocketService
|
|
*
|
|
* API limpia para que otros módulos emitan eventos en tiempo real
|
|
*/
|
|
@Injectable()
|
|
export class WebSocketService {
|
|
private readonly logger = new Logger(WebSocketService.name);
|
|
|
|
constructor(private readonly gateway: NotificationsGateway) {}
|
|
|
|
/**
|
|
* Emitir notificación a un usuario
|
|
*/
|
|
emitNotificationToUser(userId: string, notification: any) {
|
|
this.gateway.emitToUser(userId, SocketEvent.NEW_NOTIFICATION, {
|
|
notification,
|
|
});
|
|
this.logger.debug(`Notificación enviada a usuario ${userId}`);
|
|
}
|
|
|
|
/**
|
|
* Emitir notificación a múltiples usuarios
|
|
*/
|
|
emitNotificationToUsers(userIds: string[], notification: any) {
|
|
this.gateway.emitToUsers(userIds, SocketEvent.NEW_NOTIFICATION, {
|
|
notification,
|
|
});
|
|
this.logger.debug(`Notificación enviada a ${userIds.length} usuarios`);
|
|
}
|
|
|
|
/**
|
|
* Actualizar contador de no leídas
|
|
*/
|
|
emitUnreadCountUpdate(userId: string, unreadCount: number) {
|
|
this.gateway.emitToUser(userId, SocketEvent.UNREAD_COUNT_UPDATED, {
|
|
unreadCount,
|
|
});
|
|
this.logger.debug(`Contador (${unreadCount}) enviado a usuario ${userId}`);
|
|
}
|
|
|
|
/**
|
|
* Notificación eliminada
|
|
*/
|
|
emitNotificationDeleted(userId: string, notificationId: string) {
|
|
this.gateway.emitToUser(userId, SocketEvent.NOTIFICATION_DELETED, {
|
|
notificationId,
|
|
});
|
|
this.logger.debug(
|
|
`Eliminación ${notificationId} enviada a usuario ${userId}`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Emitir datos actualizados genérico
|
|
*/
|
|
emitDataUpdated(userId: string, dataType: string, data: any) {
|
|
this.gateway.emitToUser(userId, SocketEvent.DATA_UPDATED, {
|
|
type: dataType,
|
|
data,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Broadcast a todos los usuarios
|
|
*/
|
|
broadcast(event: SocketEvent, data: any) {
|
|
this.gateway.broadcast(event, data);
|
|
}
|
|
|
|
/**
|
|
* Emitir a room específica
|
|
*/
|
|
emitToRoom(room: string, event: SocketEvent, data: any) {
|
|
this.gateway.emitToRoom(room, event, data);
|
|
}
|
|
|
|
/**
|
|
* Verificar si usuario está conectado
|
|
*/
|
|
isUserConnected(userId: string): boolean {
|
|
return this.gateway.isUserConnected(userId);
|
|
}
|
|
|
|
/**
|
|
* Obtener usuarios conectados
|
|
*/
|
|
getConnectedUsersCount(): number {
|
|
return this.gateway.getConnectedUsersCount();
|
|
}
|
|
|
|
/**
|
|
* Obtener cantidad de sockets de un usuario
|
|
*/
|
|
getUserSocketCount(userId: string): number {
|
|
return this.gateway.getUserSocketCount(userId);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 7: Crear Módulo
|
|
|
|
```typescript
|
|
// src/modules/websocket/websocket.module.ts
|
|
import { Module } from '@nestjs/common';
|
|
import { JwtModule } from '@nestjs/jwt';
|
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
import { NotificationsGateway } from './gateways/notifications.gateway';
|
|
import { WebSocketService } from './services/websocket.service';
|
|
import { WsJwtGuard } from './guards/ws-jwt.guard';
|
|
|
|
@Module({
|
|
imports: [
|
|
JwtModule.registerAsync({
|
|
imports: [ConfigModule],
|
|
inject: [ConfigService],
|
|
useFactory: (config: ConfigService) => ({
|
|
secret: config.get<string>('JWT_SECRET'),
|
|
signOptions: {
|
|
expiresIn: config.get<string>('JWT_EXPIRES_IN', '7d'),
|
|
},
|
|
}),
|
|
}),
|
|
],
|
|
providers: [NotificationsGateway, WebSocketService, WsJwtGuard],
|
|
exports: [WebSocketService],
|
|
})
|
|
export class WebSocketModule {}
|
|
|
|
// Barrel export
|
|
// src/modules/websocket/index.ts
|
|
export * from './websocket.module';
|
|
export * from './services/websocket.service';
|
|
export * from './types/websocket.types';
|
|
export * from './guards/ws-jwt.guard';
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 8: Registrar en AppModule
|
|
|
|
```typescript
|
|
// src/app.module.ts
|
|
import { Module } from '@nestjs/common';
|
|
import { WebSocketModule } from './modules/websocket';
|
|
|
|
@Module({
|
|
imports: [
|
|
// ... otros módulos
|
|
WebSocketModule,
|
|
],
|
|
})
|
|
export class AppModule {}
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 9: Uso en Otros Servicios
|
|
|
|
```typescript
|
|
// src/modules/notifications/services/notification.service.ts
|
|
import { Injectable } from '@nestjs/common';
|
|
import { WebSocketService } from '@/modules/websocket';
|
|
|
|
@Injectable()
|
|
export class NotificationService {
|
|
constructor(
|
|
private readonly notificationRepo: Repository<Notification>,
|
|
private readonly wsService: WebSocketService,
|
|
) {}
|
|
|
|
async create(userId: string, dto: CreateNotificationDto) {
|
|
// 1. Guardar en BD
|
|
const notification = this.notificationRepo.create({
|
|
...dto,
|
|
user_id: userId,
|
|
});
|
|
await this.notificationRepo.save(notification);
|
|
|
|
// 2. Emitir por WebSocket si está conectado
|
|
if (this.wsService.isUserConnected(userId)) {
|
|
this.wsService.emitNotificationToUser(userId, notification);
|
|
}
|
|
|
|
// 3. Actualizar contador
|
|
const unread = await this.countUnread(userId);
|
|
this.wsService.emitUnreadCountUpdate(userId, unread);
|
|
|
|
return notification;
|
|
}
|
|
|
|
async markAsRead(notificationId: string, userId: string) {
|
|
await this.notificationRepo.update(notificationId, { status: 'read' });
|
|
|
|
// Actualizar contador
|
|
const unread = await this.countUnread(userId);
|
|
this.wsService.emitUnreadCountUpdate(userId, unread);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 10: Cliente Frontend (React)
|
|
|
|
### Instalación
|
|
|
|
```bash
|
|
npm install socket.io-client
|
|
```
|
|
|
|
### Hook de WebSocket
|
|
|
|
```typescript
|
|
// src/hooks/useSocket.ts
|
|
import { useEffect, useRef, useCallback, useState } from 'react';
|
|
import { io, Socket } from 'socket.io-client';
|
|
import { useAuth } from './useAuth';
|
|
|
|
interface UseSocketReturn {
|
|
socket: Socket | null;
|
|
isConnected: boolean;
|
|
on: (event: string, handler: (data: any) => void) => () => void;
|
|
emit: (event: string, data: any) => void;
|
|
}
|
|
|
|
export function useSocket(): UseSocketReturn {
|
|
const { token } = useAuth();
|
|
const socketRef = useRef<Socket | null>(null);
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!token) return;
|
|
|
|
const socket = io(import.meta.env.VITE_API_URL || 'http://localhost:3000', {
|
|
path: '/socket.io/',
|
|
transports: ['websocket', 'polling'],
|
|
auth: { token },
|
|
reconnectionAttempts: 5,
|
|
reconnectionDelay: 1000,
|
|
});
|
|
|
|
socket.on('connect', () => {
|
|
console.log('Socket conectado');
|
|
setIsConnected(true);
|
|
});
|
|
|
|
socket.on('disconnect', (reason) => {
|
|
console.log('Socket desconectado:', reason);
|
|
setIsConnected(false);
|
|
});
|
|
|
|
socket.on('authenticated', (data) => {
|
|
console.log('Autenticado:', data.email);
|
|
});
|
|
|
|
socket.on('error', (error) => {
|
|
console.error('Error de socket:', error);
|
|
});
|
|
|
|
socketRef.current = socket;
|
|
|
|
return () => {
|
|
socket.disconnect();
|
|
socketRef.current = null;
|
|
};
|
|
}, [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,
|
|
isConnected,
|
|
on,
|
|
emit,
|
|
};
|
|
}
|
|
```
|
|
|
|
### Uso en Componente
|
|
|
|
```tsx
|
|
// src/components/NotificationBell.tsx
|
|
import { useEffect, useState } from 'react';
|
|
import { useSocket } from '@/hooks/useSocket';
|
|
import { Badge, IconButton } from '@mui/material';
|
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
|
|
|
export function NotificationBell() {
|
|
const { on, isConnected } = useSocket();
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
|
|
useEffect(() => {
|
|
// Escuchar actualizaciones del contador
|
|
const unsubscribe = on('notification:unread_count', (data) => {
|
|
setUnreadCount(data.unreadCount);
|
|
});
|
|
|
|
return unsubscribe;
|
|
}, [on]);
|
|
|
|
useEffect(() => {
|
|
// Escuchar nuevas notificaciones
|
|
const unsubscribe = on('notification:new', (data) => {
|
|
// Mostrar toast o actualizar lista
|
|
console.log('Nueva notificación:', data.notification);
|
|
setUnreadCount((prev) => prev + 1);
|
|
});
|
|
|
|
return unsubscribe;
|
|
}, [on]);
|
|
|
|
return (
|
|
<IconButton color={isConnected ? 'primary' : 'default'}>
|
|
<Badge badgeContent={unreadCount} color="error">
|
|
<NotificationsIcon />
|
|
</Badge>
|
|
</IconButton>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Variables de Entorno
|
|
|
|
```env
|
|
# Backend
|
|
JWT_SECRET=your-secret-key
|
|
JWT_EXPIRES_IN=7d
|
|
CORS_ORIGIN=http://localhost:3000,http://localhost:5173
|
|
|
|
# Frontend
|
|
VITE_API_URL=http://localhost:3000
|
|
```
|
|
|
|
---
|
|
|
|
## Checklist de Implementación
|
|
|
|
- [ ] Dependencias instaladas (@nestjs/websockets, socket.io)
|
|
- [ ] Tipos y eventos definidos
|
|
- [ ] WsJwtGuard creado
|
|
- [ ] NotificationsGateway implementado
|
|
- [ ] WebSocketService creado
|
|
- [ ] Módulo registrado en AppModule
|
|
- [ ] Variables de entorno configuradas
|
|
- [ ] Build pasa sin errores
|
|
- [ ] Test: conectar desde frontend
|
|
- [ ] Test: recibir evento de notificación
|
|
|
|
---
|
|
|
|
## Verificar Funcionamiento
|
|
|
|
### Backend
|
|
|
|
```bash
|
|
# Ver logs al iniciar
|
|
npm run start:dev
|
|
# Debería mostrar: WebSocket Gateway inicializado
|
|
```
|
|
|
|
### Frontend
|
|
|
|
```javascript
|
|
// En consola del navegador
|
|
const socket = io('http://localhost:3000', {
|
|
path: '/socket.io/',
|
|
auth: { token: 'your-jwt-token' },
|
|
});
|
|
|
|
socket.on('authenticated', (data) => console.log('Auth:', data));
|
|
socket.on('notification:new', (data) => console.log('Notif:', data));
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### "Authentication token required"
|
|
- Verificar que el token se envía en `auth: { token }`
|
|
- Verificar que el token JWT es válido
|
|
|
|
### "CORS error"
|
|
- Verificar que `CORS_ORIGIN` incluya el origen del frontend
|
|
- Verificar que `credentials: true` está configurado
|
|
|
|
### Conexión se desconecta inmediatamente
|
|
- Verificar que el guard no está rechazando la conexión
|
|
- Revisar logs del backend para ver el motivo
|
|
|
|
### Eventos no llegan al cliente
|
|
- Verificar que el cliente está en el room correcto (`user:{userId}`)
|
|
- Verificar que el evento se emite con el nombre correcto
|
|
|
|
---
|
|
|
|
**Versión:** 1.0.0
|
|
**Sistema:** SIMCO Catálogo
|