Saltearse al contenido

Guía de WebSockets

Aprende a construir aplicaciones en tiempo real con soporte WebSocket en Veloce-TS v0.2.6.

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
import { VeloceTS } from 'veloce-ts';
const app = new VeloceTS();
app.install('websocket', {
path: '/ws'
});
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
});
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);
}
}
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.
}
}
DecoradorPropósitoEjemplo
@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()
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()
});
}
}
@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);
}
}
}
@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()
});
}
}
@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;
}
}
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()
});
}
}
@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;
}
}
@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);
}
}
@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);
}
}
}