Saltearse al contenido

Guía de WebSockets

Construye tiempo real con el plugin WebSocket oficial y los decoradores. Esta guía está alineada con las versiones actuales de veloce-ts (ver Registro de cambios).

  • @WebSocket(path) — marca la clase como manejador para esa ruta.
  • @OnConnect / @OnMessage / @OnDisconnect — ciclo de vida.
  • WebSocketPlugin — registra rutas y hace el upgrade según el runtime (Bun / Deno / Node).
  • WebSocketConnection — send, broadcast, join, leave, close, y acceso native al WebSocket.

Un solo @OnMessage por clase: los metadatos guardan un único manejador de mensajes (si declaras varios @OnMessage, gana el último). Modela distintos payloads del cliente con un esquema Zod (por ejemplo unión discriminada por un campo type).

import 'reflect-metadata';
import {
VeloceTS,
WebSocketPlugin,
WebSocket,
OnConnect,
OnMessage,
OnDisconnect,
WebSocketConnection,
} from 'veloce-ts';
import { z } from 'zod';
const ChatMessage = z.object({
type: z.literal('chat'),
text: z.string().min(1),
});
@WebSocket('/ws/chat')
class ChatSocket {
@OnConnect()
onOpen(client: WebSocketConnection) {
client.join('lobby');
client.send({ type: 'welcome', message: 'Conectado' });
}
@OnMessage(ChatMessage)
onData(client: WebSocketConnection, msg: z.infer<typeof ChatMessage>) {
client.broadcast({ type: 'chat', text: msg.text }, 'lobby');
}
@OnDisconnect()
onClose(client: WebSocketConnection) {
console.log('desconectado', client.id);
}
}
const app = new VeloceTS({ title: 'Chat', version: '1.0.0' });
app.usePlugin(new WebSocketPlugin());
app.include(ChatSocket);
await app.compile();
app.listen(3000);
  • Importa WebSocketPlugin desde veloce-ts (también en veloce-ts/plugins).
  • El constructor del plugin no acepta opciones; la ruta es siempre el argumento de @WebSocket('/ruta').
  • Llama a await app.compile() después de los include() para compilar metadatos.
RuntimeRuta de upgradeInstalación extra
Bunupgrade() nativo de Bun.serve()—
DenoDeno.upgradeWebSocket()—
Node 20+@hono/node-ws (envuelve ws)npm install @hono/node-ws

En Node el plugin necesita el http.Server real, que solo existe después de app.listen() — por eso inyecta el manejador WebSocket en el hook onStart() del plugin. Dos consecuencias:

  • Sirve con app.listen(). Si le pasas app.fetch a un servidor que creaste tú, el plugin se queda sin servidor al que engancharse y el upgrade no se completa.
  • Node también necesita @hono/node-server, que listen() ya requiere de todas formas.

Si falta @hono/node-ws, el arranque falla con el comando de instalación en vez de un upgrade roto en runtime:

WebSocket support on Node requires the @hono/node-ws package.
Install it with: npm install @hono/node-ws

El heartbeat varía un poco según el runtime — solo Bun expone un ws.ping() a nivel de protocolo; ver la nota en Endurecimiento de conexiones.

Las cuatro opciones son opcionales y se configuran en WebSocketPlugin:

app.usePlugin(new WebSocketPlugin({
heartbeatIntervalMs: 30_000, // por defecto 30 s; 0 lo desactiva
heartbeatTimeoutMs: 60_000, // por defecto 2 × heartbeatIntervalMs
idleTimeoutMs: 0, // por defecto 0 (desactivado)
maxMessageSizeBytes: 1_048_576, // por defecto 1 MiB
}));
OpciónComportamiento
heartbeatIntervalMsCada cuánto hace ping el servidor. Cierra la conexión con código 1001 si no llega tráfico entrante dentro de heartbeatTimeoutMs.
heartbeatTimeoutMsLímite de vitalidad. Por defecto el doble del intervalo.
idleTimeoutMsCorte duro por inactividad (código 1001) independientemente de las respuestas de heartbeat. Ponlo mayor que heartbeatIntervalMs si usas ambos, ya que los pong cuentan como actividad.
maxMessageSizeBytesLos mensajes entrantes demasiado grandes reciben un frame de error y código de cierre 1009 (Message Too Big).

Las conexiones cerradas se eliminan del registro de conexiones y de todas las salas a las que se habían unido.

Protocolo de heartbeat. En cada tick el servidor envía un ping a nivel de protocolo donde el runtime lo expone (el ws.ping() de Bun) más un frame JSON portable a nivel de aplicación {"type":"ping"}. La vitalidad se mide por cualquier tráfico entrante, porque los eventos pong a nivel de protocolo no son observables desde el manager — Bun los entrega a un handler pong en Bun.serve() que el adaptador no conecta, y los sockets actualizados de Deno no exponen ping() en absoluto. Los clientes deberían responder con {"type":"pong"} o simplemente enviar cualquier mensaje dentro del timeout.

Mientras el heartbeat está activo, los frames entrantes {"type":"ping"} y {"type":"pong"} son mensajes de control reservados y no se reenvían a tus handlers @OnMessage.

APIUso
idIdentificador estable de la conexión
send(data)Envía datos serializables a JSON a este cliente
broadcast(data, room?)Envía a todas las conexiones de una sala, o a todas si omites room
join(room) / leave(room)Entrar / salir de salas
close(code?, reason?)Cierra el socket
nativeInstancia WebSocket subyacente

Un esquema Zod opcional en @OnMessage(Schema) valida cada mensaje JSON entrante. Si falla, el manager envía un objeto de error al cliente sin cerrar la conexión.

El proyecto examples/chat-api (Bun) muestra REST + JWT + SQLite junto al flujo WebSocket, con notas sobre 426/501 cuando no hay upgrade (p. ej. pruebas con fetch).