workspace-v1/shared/catalog/websocket/_reference/websocket.gateway.reference.ts
rckrdmrd cb4c0681d3 feat(workspace): Add new projects and update architecture
New projects created:
- michangarrito (marketplace mobile)
- template-saas (SaaS template)
- clinica-dental (dental ERP)
- clinica-veterinaria (veterinary ERP)

Architecture updates:
- Move catalog from core/ to shared/
- Add MCP servers structure and templates
- Add git management scripts
- Update SUBREPOSITORIOS.md with 15 new repos
- Update .gitignore for new projects

Repository infrastructure:
- 4 main repositories
- 11 subrepositorios
- Gitea remotes configured

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 04:43:28 -06:00

199 lines
5.2 KiB
TypeScript

/**
* 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<string, ConnectedClient>();
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<string[]> {
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;
}