Guía de WebSockets
Guía de WebSockets
Sección titulada «Guía de WebSockets»Aprende a construir aplicaciones en tiempo real con soporte WebSocket en Veloce-TS v0.2.6.
Tabla de Contenidos
Sección titulada «Tabla de Contenidos»- Resumen
- Configuración
- Manejadores WebSocket
- Decoradores
- Ejemplos en Tiempo Real
- Autenticación
- Mejores Prácticas
Resumen
Sección titulada «Resumen»Veloce-TS proporciona soporte completo de WebSocket con:
- Manejadores basados en Decoradores para código WebSocket limpio
- Integración de Autenticación con tokens JWT
- Gestión de Salas para comunicaciones grupales
- Difusión de Mensajes a múltiples clientes
- Eventos con Seguridad de Tipos con validación Zod
- Manejo Automático de Reconexión
Configuración
Sección titulada «Configuración»1. Instalar Plugin WebSocket
Sección titulada «1. Instalar Plugin WebSocket»import { VeloceTS } from 'veloce-ts';
const app = new VeloceTS();
app.install('websocket', { path: '/ws'});2. Opciones de Configuración WebSocket
Sección titulada «2. Opciones de Configuración WebSocket»app.install('websocket', { path: '/ws', // Ruta del endpoint WebSocket cors: true, // Habilitar CORS authentication: true, // Habilitar autenticación JWT heartbeatInterval: 30000, // Intervalo de heartbeat en ms maxConnections: 1000 // Máximo de conexiones concurrentes});Manejadores WebSocket
Sección titulada «Manejadores WebSocket»Manejador WebSocket Básico
Sección titulada «Manejador WebSocket Básico»import { WebSocket, OnConnect, OnMessage, OnDisconnect } from 'veloce-ts';
@WebSocket('/chat')export class ChatHandler {
@OnConnect() async onConnect(socket: any) { console.log('Cliente conectado:', socket.id);
// Enviar mensaje de bienvenida socket.emit('welcome', { message: '¡Bienvenido al chat!', timestamp: new Date().toISOString() }); }
@OnMessage('message') async onMessage(socket: any, data: any) { console.log('Mensaje recibido:', data);
// Difundir mensaje a todos los clientes conectados socket.broadcast.emit('new-message', { id: generateId(), content: data.content, sender: data.sender, timestamp: new Date().toISOString() }); }
@OnDisconnect() async onDisconnect(socket: any) { console.log('Cliente desconectado:', socket.id); }}Comunicación Basada en Salas
Sección titulada «Comunicación Basada en Salas»import { WebSocket, OnConnect, OnMessage, OnDisconnect } from 'veloce-ts';import { z } from 'zod';
const JoinRoomSchema = z.object({ roomId: z.string(), userId: z.string()});
const ChatMessageSchema = z.object({ roomId: z.string(), content: z.string().min(1), senderId: z.string()});
@WebSocket('/rooms')export class RoomHandler {
@OnConnect() async onConnect(socket: any) { console.log('Cliente conectado a salas:', socket.id); }
@OnMessage('join-room') async onJoinRoom(socket: any, data: z.infer<typeof JoinRoomSchema>) { const { roomId, userId } = JoinRoomSchema.parse(data);
// Unirse a la sala socket.join(roomId);
// Notificar a miembros de la sala socket.to(roomId).emit('user-joined', { userId, roomId, timestamp: new Date().toISOString() });
// Enviar información actual de la sala socket.emit('room-joined', { roomId, memberCount: await this.getRoomMemberCount(roomId) }); }
@OnMessage('leave-room') async onLeaveRoom(socket: any, data: { roomId: string; userId: string }) { const { roomId, userId } = data;
socket.leave(roomId);
// Notificar a miembros de la sala socket.to(roomId).emit('user-left', { userId, roomId, timestamp: new Date().toISOString() }); }
@OnMessage('room-message') async onRoomMessage(socket: any, data: z.infer<typeof ChatMessageSchema>) { const { roomId, content, senderId } = ChatMessageSchema.parse(data);
// Difundir mensaje a miembros de la sala socket.to(roomId).emit('room-message', { id: generateId(), roomId, content, senderId, timestamp: new Date().toISOString() });
// Almacenar mensaje en base de datos await this.messageService.create({ roomId, content, senderId, timestamp: new Date() }); }
@OnDisconnect() async onDisconnect(socket: any) { console.log('Cliente desconectado de salas:', socket.id);
// Manejar limpieza si es necesario await this.handleUserDisconnect(socket.id); }
private async getRoomMemberCount(roomId: string): Promise<number> { // La implementación depende de tu librería WebSocket // Esto es un placeholder return 1; }
private async handleUserDisconnect(socketId: string) { // Limpiar sesiones de usuario, notificar salas, etc. }}Decoradores
Sección titulada «Decoradores»Decoradores WebSocket
Sección titulada «Decoradores WebSocket»| Decorador | Propósito | Ejemplo |
|---|---|---|
@WebSocket(path) | Marca una clase como manejador WebSocket | @WebSocket('/chat') |
@OnConnect() | Maneja conexiones de cliente | @OnConnect() |
@OnMessage(event) | Maneja eventos de mensaje específicos | @OnMessage('message') |
@OnDisconnect() | Maneja desconexiones de cliente | @OnDisconnect() |
Manejo Avanzado de Mensajes
Sección titulada «Manejo Avanzado de Mensajes»import { WebSocket, OnMessage } from 'veloce-ts';import { z } from 'zod';
const TypingSchema = z.object({ roomId: z.string(), userId: z.string(), isTyping: z.boolean()});
const FileUploadSchema = z.object({ roomId: z.string(), fileName: z.string(), fileSize: z.number(), fileType: z.string()});
@WebSocket('/chat')export class AdvancedChatHandler {
@OnMessage('typing') async onTyping(socket: any, data: z.infer<typeof TypingSchema>) { const { roomId, userId, isTyping } = TypingSchema.parse(data);
// Difundir estado de escritura a miembros de la sala (excepto remitente) socket.to(roomId).emit('user-typing', { userId, isTyping, timestamp: new Date().toISOString() }); }
@OnMessage('file-upload') async onFileUpload(socket: any, data: z.infer<typeof FileUploadSchema>) { const { roomId, fileName, fileSize, fileType } = FileUploadSchema.parse(data);
// Validar archivo if (fileSize > 10 * 1024 * 1024) { // Límite de 10MB socket.emit('upload-error', { message: 'Archivo muy grande. El tamaño máximo es 10MB.' }); return; }
// Procesar carga de archivo const fileUrl = await this.fileService.uploadFile(data);
// Difundir información del archivo a la sala socket.to(roomId).emit('file-shared', { id: generateId(), roomId, fileName, fileSize, fileType, fileUrl, timestamp: new Date().toISOString() }); }
@OnMessage('get-online-users') async onGetOnlineUsers(socket: any, data: { roomId: string }) { const { roomId } = data;
const onlineUsers = await this.getOnlineUsers(roomId);
socket.emit('online-users', { roomId, users: onlineUsers, timestamp: new Date().toISOString() }); }}Ejemplos en Tiempo Real
Sección titulada «Ejemplos en Tiempo Real»1. Aplicación de Chat en Vivo
Sección titulada «1. Aplicación de Chat en Vivo»@WebSocket('/chat')export class LiveChatHandler { private activeUsers = new Map<string, any>();
@OnConnect() async onConnect(socket: any) { console.log('Usuario conectado:', socket.id); }
@OnMessage('join-chat') async onJoinChat(socket: any, data: { username: string }) { const { username } = data;
// Almacenar información del usuario this.activeUsers.set(socket.id, { username, joinedAt: new Date() });
// Difundir que el usuario se unió socket.broadcast.emit('user-joined', { username, timestamp: new Date().toISOString() });
// Enviar lista de usuarios actuales const users = Array.from(this.activeUsers.values()).map(u => u.username); socket.emit('users-list', { users }); }
@OnMessage('chat-message') async onChatMessage(socket: any, data: { message: string }) { const user = this.activeUsers.get(socket.id); if (!user) return;
const messageData = { id: generateId(), username: user.username, message: data.message, timestamp: new Date().toISOString() };
// Difundir a todos los usuarios socket.broadcast.emit('chat-message', messageData);
// Almacenar en base de datos await this.messageService.create(messageData); }
@OnDisconnect() async onDisconnect(socket: any) { const user = this.activeUsers.get(socket.id); if (user) { // Difundir que el usuario se fue socket.broadcast.emit('user-left', { username: user.username, timestamp: new Date().toISOString() });
this.activeUsers.delete(socket.id); } }}2. Notificaciones en Tiempo Real
Sección titulada «2. Notificaciones en Tiempo Real»@WebSocket('/notifications')export class NotificationHandler {
@OnConnect() async onConnect(socket: any) { console.log('Cliente de notificaciones conectado:', socket.id); }
@OnMessage('subscribe-notifications') async onSubscribeNotifications(socket: any, data: { userId: string }) { const { userId } = data;
// Unirse a sala específica del usuario socket.join(`user-${userId}`);
// Enviar notificaciones pendientes const pendingNotifications = await this.notificationService.getPending(userId); socket.emit('pending-notifications', { notifications: pendingNotifications }); }
@OnMessage('mark-as-read') async onMarkAsRead(socket: any, data: { notificationId: string; userId: string }) { const { notificationId, userId } = data;
await this.notificationService.markAsRead(notificationId);
socket.emit('notification-read', { notificationId, timestamp: new Date().toISOString() }); }
// Método para enviar notificación a usuario específico async sendNotificationToUser(userId: string, notification: any) { const io = this.getSocketIO(); io.to(`user-${userId}`).emit('new-notification', { ...notification, timestamp: new Date().toISOString() }); }
// Método para difundir notificación a todos los usuarios async broadcastNotification(notification: any) { const io = this.getSocketIO(); io.emit('broadcast-notification', { ...notification, timestamp: new Date().toISOString() }); }}3. Colaboración en Vivo
Sección titulada «3. Colaboración en Vivo»@WebSocket('/collaboration')export class CollaborationHandler { private documentStates = new Map<string, any>();
@OnMessage('join-document') async onJoinDocument(socket: any, data: { documentId: string; userId: string }) { const { documentId, userId } = data;
// Unirse a sala del documento socket.join(`document-${documentId}`);
// Enviar estado actual del documento const currentState = this.documentStates.get(documentId) || { content: '', version: 0 }; socket.emit('document-state', currentState);
// Notificar a otros colaboradores socket.to(`document-${documentId}`).emit('collaborator-joined', { userId, documentId, timestamp: new Date().toISOString() }); }
@OnMessage('document-change') async onDocumentChange(socket: any, data: { documentId: string; userId: string; changes: any; version: number }) { const { documentId, userId, changes, version } = data;
// Validar versión const currentVersion = this.documentStates.get(documentId)?.version || 0; if (version !== currentVersion + 1) { socket.emit('version-conflict', { currentVersion, receivedVersion: version }); return; }
// Actualizar estado del documento const newState = { ...this.documentStates.get(documentId), content: this.applyChanges(this.documentStates.get(documentId)?.content || '', changes), version: version, lastModifiedBy: userId, lastModifiedAt: new Date() };
this.documentStates.set(documentId, newState);
// Difundir cambios a otros colaboradores socket.to(`document-${documentId}`).emit('document-updated', { documentId, changes, version, modifiedBy: userId, timestamp: new Date().toISOString() }); }
@OnMessage('cursor-position') async onCursorPosition(socket: any, data: { documentId: string; userId: string; position: number }) { const { documentId, userId, position } = data;
// Difundir posición del cursor a otros colaboradores socket.to(`document-${documentId}`).emit('cursor-moved', { documentId, userId, position, timestamp: new Date().toISOString() }); }
private applyChanges(content: string, changes: any[]): string { // Aplicar transformaciones operacionales // Este es un ejemplo simplificado let result = content; for (const change of changes) { if (change.type === 'insert') { result = result.slice(0, change.position) + change.text + result.slice(change.position); } else if (change.type === 'delete') { result = result.slice(0, change.position) + result.slice(change.position + change.length); } } return result; }}Autenticación
Sección titulada «Autenticación»Autenticación JWT para WebSockets
Sección titulada «Autenticación JWT para WebSockets»import { WebSocket, OnConnect, Auth } from 'veloce-ts';
@WebSocket('/secure')export class SecureHandler {
@OnConnect() @Auth() // Requerir autenticación async onConnect(socket: any, user: any) { console.log('Usuario autenticado conectado:', user.username);
// Unirse a sala específica del usuario socket.join(`user-${user.id}`);
socket.emit('authenticated', { message: 'Autenticación exitosa', user: { id: user.id, username: user.username, role: user.role } }); }
@OnMessage('private-message') @Auth() async onPrivateMessage(socket: any, data: { targetUserId: string; message: string }, user: any) { const { targetUserId, message } = data;
// Enviar mensaje privado a usuario específico const io = this.getSocketIO(); io.to(`user-${targetUserId}`).emit('private-message', { from: { id: user.id, username: user.username }, message, timestamp: new Date().toISOString() }); }}Mejores Prácticas
Sección titulada «Mejores Prácticas»1. Manejo de Errores
Sección titulada «1. Manejo de Errores»@WebSocket('/chat')export class ChatHandler {
@OnMessage('message') async onMessage(socket: any, data: any) { try { // Validar datos const { content, roomId } = this.validateMessage(data);
// Procesar mensaje await this.processMessage(socket, { content, roomId });
} catch (error) { // Enviar error al cliente socket.emit('error', { message: error.message, code: error.code || 'UNKNOWN_ERROR' });
// Registrar error para depuración console.error('Error WebSocket:', error); } }
private validateMessage(data: any) { if (!data.content || typeof data.content !== 'string') { throw new Error('Contenido de mensaje inválido'); }
if (!data.roomId || typeof data.roomId !== 'string') { throw new Error('ID de sala inválido'); }
return data; }}2. Limitación de Velocidad
Sección titulada «2. Limitación de Velocidad»@WebSocket('/chat')export class ChatHandler { private messageCounts = new Map<string, number>();
@OnMessage('message') async onMessage(socket: any, data: any) { const userId = socket.userId; const now = Date.now(); const userKey = `${userId}-${Math.floor(now / 60000)}`; // Por minuto
// Verificar límite de velocidad const messageCount = this.messageCounts.get(userKey) || 0; if (messageCount > 60) { // 60 mensajes por minuto socket.emit('rate-limit-exceeded', { message: 'Demasiados mensajes. Por favor, reduce la velocidad.' }); return; }
// Actualizar contador this.messageCounts.set(userKey, messageCount + 1);
// Procesar mensaje await this.processMessage(socket, data); }}3. Gestión de Conexiones
Sección titulada «3. Gestión de Conexiones»@WebSocket('/chat')export class ChatHandler { private connections = new Map<string, any>();
@OnConnect() async onConnect(socket: any) { // Almacenar información de conexión this.connections.set(socket.id, { connectedAt: new Date(), userId: socket.userId, rooms: new Set() });
// Configurar heartbeat socket.heartbeatInterval = setInterval(() => { if (!socket.connected) { this.handleDisconnection(socket.id); return; } socket.emit('ping'); }, 30000); }
@OnDisconnect() async onDisconnect(socket: any) { this.handleDisconnection(socket.id); }
private handleDisconnection(socketId: string) { const connection = this.connections.get(socketId); if (connection) { // Limpiar if (connection.heartbeatInterval) { clearInterval(connection.heartbeatInterval); }
this.connections.delete(socketId);
// Notificar salas this.notifyUserDisconnected(connection.userId, connection.rooms); } }}Próximos Pasos
Sección titulada «Próximos Pasos»- Ejemplo TaskMaster - Ver WebSockets en acción
- Guía de Autenticación - Asegurar tus conexiones WebSocket
- Guía de GraphQL - Combinar suscripciones GraphQL con WebSockets
- Guía de Pruebas - Probar tus manejadores WebSocket