Skip to content

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:

  1. req: el payload del evento, validado automáticamente contra la clase del primer parámetro.
  2. socket: la instancia de Socket.IO para emitir eventos y gestionar rooms.
  3. 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:

Terminal window
npm install socket.io-client

Conexió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 servidor
socket.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.