Typed messages and routing
A raw websocket is a pipe for bytes. To make it useful, you need a tiny message protocol on top. The convention is simple: every message is a JSON object with a type field and a payload. The server switches on the type and calls the right handler. Clients do the same on their end.
import { WebSocket } from 'ws';
import { joinRoom, leaveRoom, broadcastToRoom } from './rooms';
type ClientMessage =
| { type: 'subscribe'; channel: string }
| { type: 'unsubscribe'; channel: string }
| { type: 'ping' };
export function handleMessage(ws: WebSocket, raw: string) {
let msg: ClientMessage;
try {
msg = JSON.parse(raw);
} catch {
ws.send(JSON.stringify({ type: 'error', message: 'invalid JSON' }));
return;
}
switch (msg.type) {
case 'subscribe':
joinRoom(ws, msg.channel);
ws.send(JSON.stringify({ type: 'subscribed', channel: msg.channel }));
return;
case 'unsubscribe':
leaveRoom(ws, msg.channel);
ws.send(JSON.stringify({ type: 'unsubscribed', channel: msg.channel }));
return;
case 'ping':
ws.send(JSON.stringify({ type: 'pong' }));
return;
}
}A discriminated union for messages and a router that dispatches them.
import { broadcastToRoom } from './rooms';
export function announceBookAdded(book: { id: number; title: string }) {
broadcastToRoom('catalog', {
type: 'book_added',
data: book,
});
}When the REST side creates a new book, it broadcasts a book_added event to everyone listening on the catalog channel.
Now read the full flow. A client subscribes to the catalog channel with a subscribe message. An admin creates a new book with a REST call. The controller calls announceBookAdded, which broadcasts a book_added message to every client in the catalog room. Every live catalog page updates instantly with no refresh.
Quiz: Quiz
Loading practiceโฆ