Controladores Socket
Los controladores socket te permiten manejar comunicación WebSocket en tiempo real usando Socket.IO. Cada controlador opera en un namespace y puede definir múltiples handlers de eventos con validación automática.
Conceptos clave
@socketController(namespace): registra una clase como controlador socket en un namespace.@onSocketEvent(event): define un handler para un evento específico.@handshakeMiddlewares([]): aplica middlewares de autenticación durante la conexión.IHandshakeMiddleware: interfaz para middlewares de handshake.
Paso 1: Definir un controlador
import { socketController, onSocketEvent } from '@wabot-dev/framework'import type { Socket } from 'socket.io'
@socketController('/notifications')export class NotificationController { constructor(private notificationService: NotificationService) {}
@onSocketEvent('subscribe') async onSubscribe(req: SubscribeRequest, socket: Socket, callback: Function) { await this.notificationService.subscribe(req.channel, socket.id) callback({ success: true, channel: req.channel }) }
@onSocketEvent('unsubscribe') async onUnsubscribe(req: UnsubscribeRequest, socket: Socket, callback: Function) { await this.notificationService.unsubscribe(req.channel, socket.id) callback({ success: true }) }}Los handlers de eventos reciben tres parámetros:
req: el payload del evento, validado automáticamente contra la clase del primer parámetro.socket: la instancia de Socket.IO para emitir eventos y gestionar rooms.callback: función de acknowledgment para responder al cliente.
Paso 2: Definir clases de request
import { isString, isNotEmpty } from '@wabot-dev/framework'
export class SubscribeRequest { @isString() @isNotEmpty() channel?: string}
export class UnsubscribeRequest { @isString() @isNotEmpty() channel?: string}
export class SendMessageRequest { @isString() @isNotEmpty() room?: string
@isString() @isNotEmpty() content?: string}Paso 3: Middlewares de handshake
Los middlewares de handshake se ejecutan cuando un cliente intenta conectarse. Son ideales para autenticación:
import { injectable, IHandshakeMiddleware } from '@wabot-dev/framework'import type { Socket } from 'socket.io'import type { DependencyContainer } from 'tsyringe'
@injectable()export class AuthHandshakeMiddleware implements IHandshakeMiddleware { async handle(socket: Socket, container: DependencyContainer): Promise<void> { const token = socket.handshake.auth?.token if (!token) { throw new Error('Token de autenticación requerido') } // Validar token y registrar usuario en el contenedor }}Aplicar middlewares al controlador:
import { socketController, handshakeMiddlewares, onSocketEvent } from '@wabot-dev/framework'
@socketController('/chat')@handshakeMiddlewares([AuthHandshakeMiddleware])export class ChatController { @onSocketEvent('message') async onMessage(req: SendMessageRequest, socket: Socket, callback: Function) { // Solo se ejecuta si el handshake middleware pasó socket.to(req.room!).emit('new-message', { content: req.content }) callback({ sent: true }) }}Paso 4: Namespaces y rooms
Los namespaces separan lógicamente los controladores. Los rooms permiten agrupar sockets para broadcasting:
@socketController('/chat')export class ChatController { @onSocketEvent('join-room') async onJoinRoom(req: JoinRoomRequest, socket: Socket, callback: Function) { await socket.join(req.room!) callback({ joined: req.room }) }
@onSocketEvent('leave-room') async onLeaveRoom(req: LeaveRoomRequest, socket: Socket, callback: Function) { await socket.leave(req.room!) callback({ left: req.room }) }
@onSocketEvent('message') async onMessage(req: SendMessageRequest, socket: Socket) { // Enviar a todos en el room excepto al emisor socket.to(req.room!).emit('new-message', { from: socket.id, content: req.content, }) }
@onSocketEvent('broadcast') async onBroadcast(req: SendMessageRequest, socket: Socket) { // Enviar a TODOS en el namespace (incluido el emisor) socket.nsp.emit('announcement', { content: req.content }) }}Paso 5: Registrar controladores
import { runSocketControllers } from '@wabot-dev/framework'import { ChatController } from '@/controllers/ChatController'import { NotificationController } from '@/controllers/NotificationController'
runSocketControllers([ChatController, NotificationController])Cliente: conectar desde JavaScript/TypeScript
Para conectarte a un controlador socket desde el cliente, usa socket.io-client:
npm install socket.io-clientConexión básica
import { io } from 'socket.io-client'
const socket = io('http://localhost:3000/chat', { auth: { token: 'tu-jwt-token-aqui', },})
socket.on('connect', () => { console.log('Conectado:', socket.id)})
socket.on('connect_error', (error) => { console.error('Error de conexión:', error.message)})Emitir eventos con acknowledgment
// Emitir y esperar respuesta del servidorsocket.emit('join-room', { room: 'ventas' }, (response) => { console.log('Respuesta:', response) // { joined: 'ventas' }})
socket.emit('message', { room: 'ventas', content: 'Hola equipo' }, (response) => { console.log('Mensaje enviado:', response) // { sent: true }})Escuchar eventos del servidor
socket.on('new-message', (data) => { console.log(`Mensaje de ${data.from}: ${data.content}`)})
socket.on('announcement', (data) => { console.log('Anuncio:', data.content)})Reconexión
Socket.IO maneja la reconexión automáticamente. Puedes configurar el comportamiento:
const socket = io('http://localhost:3000/chat', { auth: { token: 'tu-token' }, reconnection: true, reconnectionAttempts: 5, reconnectionDelay: 1000,})
socket.on('reconnect', (attemptNumber) => { console.log('Reconectado después de', attemptNumber, 'intentos')})
socket.on('reconnect_error', (error) => { console.error('Error de reconexión:', error)})Ejemplo completo
Un controlador de chat en tiempo real con rooms y autenticación:
import { socketController, handshakeMiddlewares, onSocketEvent, isString, isNotEmpty, isOptional, injectable, IHandshakeMiddleware, runSocketControllers,} from '@wabot-dev/framework'import type { Socket } from 'socket.io'import type { DependencyContainer } from 'tsyringe'
// --- Requests ---
export class JoinRoomRequest { @isString() @isNotEmpty() room?: string}
export class ChatMessageRequest { @isString() @isNotEmpty() room?: string
@isString() @isNotEmpty() content?: string
@isString() @isOptional() replyTo?: string}
// --- Middleware ---
@injectable()export class TokenHandshakeMiddleware implements IHandshakeMiddleware { async handle(socket: Socket, container: DependencyContainer): Promise<void> { const token = socket.handshake.auth?.token if (!token) { throw new Error('Token requerido') } // Aquí validarías el token JWT (ver guía de Autenticación JWT) }}
// --- Controlador ---
@socketController('/chat')@handshakeMiddlewares([TokenHandshakeMiddleware])export class ChatController { @onSocketEvent('join') async onJoin(req: JoinRoomRequest, socket: Socket, callback: Function) { await socket.join(req.room!)
// Notificar al room que alguien se unió socket.to(req.room!).emit('user-joined', { userId: socket.id })
callback({ joined: req.room, members: (await socket.in(req.room!).fetchSockets()).length }) }
@onSocketEvent('message') async onMessage(req: ChatMessageRequest, socket: Socket, callback: Function) { const message = { id: crypto.randomUUID(), from: socket.id, content: req.content, replyTo: req.replyTo, timestamp: new Date().toISOString(), }
// Enviar a todos en el room socket.to(req.room!).emit('new-message', message)
callback({ sent: true, messageId: message.id }) }
@onSocketEvent('leave') async onLeave(req: JoinRoomRequest, socket: Socket, callback: Function) { await socket.leave(req.room!) socket.to(req.room!).emit('user-left', { userId: socket.id }) callback({ left: req.room }) }}
// --- Registro ---runSocketControllers([ChatController])Siguiente paso
Protege tus endpoints REST y conexiones Socket con autenticación JWT: Autenticación JWT.