Prerequisites
- Understanding of async/await and Promise patterns π
- Basic knowledge of browser and Node.js APIs β‘
- Experience with event-driven programming π»
What you'll learn
- Master WebSocket connections and real-time messaging π―
- Build type-safe WebSocket communication patterns ποΈ
- Implement connection resilience and error recovery π
- Create production-ready real-time applications β¨
π― Introduction
Welcome to the real-time world of WebSockets! π If HTTP is like sending letters through the mail, then WebSockets are like having a direct phone line - instant, bidirectional communication that makes real-time features like chat, live updates, and collaborative editing possible!
Think of WebSockets as persistent tunnels π between your client and server. Once established, both sides can send messages instantly without the overhead of HTTP headers, request/response cycles, or polling. Itβs the backbone of modern real-time web applications!
By the end of this tutorial, youβll be a master of real-time communication, able to build chat applications, live dashboards, collaborative tools, and any other real-time feature your imagination can conjure. Letβs connect to the future! π
π Understanding WebSockets
π€ What Are WebSockets?
WebSockets provide full-duplex communication channels over a single TCP connection. They start with an HTTP handshake but then upgrade to a persistent connection that allows both client and server to send data at any time!
// π Basic WebSocket connection
// client.ts - Browser WebSocket client
class WebSocketClient {
private socket: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
constructor(private url: string) {}
connect(): Promise<void> {
return new Promise((resolve, reject) => {
console.log('π Connecting to WebSocket server...');
this.socket = new WebSocket(this.url);
this.socket.onopen = (event) => {
console.log('β
WebSocket connection established!');
this.reconnectAttempts = 0;
resolve();
};
this.socket.onmessage = (event) => {
console.log('π¨ Message received:', event.data);
this.handleMessage(JSON.parse(event.data));
};
this.socket.onclose = (event) => {
console.log('π WebSocket connection closed:', event.code, event.reason);
this.handleReconnect();
};
this.socket.onerror = (error) => {
console.error('β WebSocket error:', error);
reject(error);
};
});
}
send(message: any): void {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(message));
} else {
console.warn('β οΈ WebSocket not connected. Message not sent:', message);
}
}
private handleMessage(message: any): void {
// Override in subclasses or provide callback
console.log('π₯ Handling message:', message);
}
private handleReconnect(): void {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`π Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => {
this.connect().catch(console.error);
}, this.reconnectDelay * this.reconnectAttempts);
} else {
console.error('π₯ Max reconnection attempts reached');
}
}
disconnect(): void {
if (this.socket) {
this.socket.close();
this.socket = null;
}
}
}
π‘ Key Characteristics
- π Bidirectional: Both client and server can send messages
- β‘ Low Latency: No HTTP overhead for each message
- π‘ Real-Time: Instant message delivery
- π Persistent: Connection stays open until explicitly closed
// π¨ Type-safe WebSocket message system
interface BaseMessage {
type: string;
timestamp: number;
id: string;
}
interface ChatMessage extends BaseMessage {
type: 'chat_message';
content: string;
userId: string;
username: string;
}
interface UserJoinedMessage extends BaseMessage {
type: 'user_joined';
userId: string;
username: string;
}
interface UserLeftMessage extends BaseMessage {
type: 'user_left';
userId: string;
username: string;
}
interface TypingMessage extends BaseMessage {
type: 'typing';
userId: string;
username: string;
isTyping: boolean;
}
// π― Union type for all possible messages
type WebSocketMessage = ChatMessage | UserJoinedMessage | UserLeftMessage | TypingMessage;
// π¦ Message factory for type safety
class MessageFactory {
static createChatMessage(content: string, userId: string, username: string): ChatMessage {
return {
type: 'chat_message',
content,
userId,
username,
timestamp: Date.now(),
id: crypto.randomUUID()
};
}
static createUserJoinedMessage(userId: string, username: string): UserJoinedMessage {
return {
type: 'user_joined',
userId,
username,
timestamp: Date.now(),
id: crypto.randomUUID()
};
}
static createTypingMessage(userId: string, username: string, isTyping: boolean): TypingMessage {
return {
type: 'typing',
userId,
username,
isTyping,
timestamp: Date.now(),
id: crypto.randomUUID()
};
}
}
π WebSockets vs Other Real-Time Technologies
// π HTTP Polling - inefficient, delayed updates
class HTTPPollingClient {
private intervalId: number | null = null;
startPolling(url: string, interval = 1000): void {
// β Wasteful - constantly hitting server even when no updates
this.intervalId = window.setInterval(async () => {
try {
const response = await fetch(url);
const data = await response.json();
this.handleUpdate(data);
} catch (error) {
console.error('Polling error:', error);
}
}, interval);
}
private handleUpdate(data: any): void {
console.log('π Polled update:', data);
}
stopPolling(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
}
// π Server-Sent Events - one-way real-time
class SSEClient {
private eventSource: EventSource | null = null;
connect(url: string): void {
// β
Real-time but only server β client
this.eventSource = new EventSource(url);
this.eventSource.onmessage = (event) => {
console.log('π‘ SSE message:', JSON.parse(event.data));
};
this.eventSource.onerror = (error) => {
console.error('β SSE error:', error);
};
}
disconnect(): void {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
}
// π WebSockets - full duplex real-time (BEST!)
class RealTimeClient {
private socket: WebSocket | null = null;
connect(url: string): void {
// β
Bidirectional real-time with low overhead
this.socket = new WebSocket(url);
this.socket.onmessage = (event) => {
console.log('β‘ Real-time message:', JSON.parse(event.data));
};
// Can send anytime!
setTimeout(() => {
this.send({ type: 'ping', message: 'Hello server!' });
}, 1000);
}
send(data: any): void {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
}
}
}
ποΈ Building a Type-Safe WebSocket Client
π― Advanced WebSocket Client Implementation
Letβs build a production-ready WebSocket client with TypeScript that handles all the edge cases and provides a clean API:
// ποΈ Advanced WebSocket client with full TypeScript support
interface WebSocketClientConfig {
autoReconnect?: boolean;
maxReconnectAttempts?: number;
reconnectDelay?: number;
heartbeatInterval?: number;
connectionTimeout?: number;
}
interface WebSocketClientEvents {
connected: () => void;
disconnected: (reason: string) => void;
message: (message: WebSocketMessage) => void;
error: (error: Error) => void;
reconnecting: (attempt: number) => void;
}
class TypeSafeWebSocketClient {
private socket: WebSocket | null = null;
private reconnectAttempts = 0;
private heartbeatTimer: number | null = null;
private connectionTimeoutTimer: number | null = null;
private eventListeners: Partial<WebSocketClientEvents> = {};
constructor(
private url: string,
private config: WebSocketClientConfig = {}
) {
// ποΈ Default configuration
this.config = {
autoReconnect: true,
maxReconnectAttempts: 5,
reconnectDelay: 1000,
heartbeatInterval: 30000,
connectionTimeout: 10000,
...config
};
}
// π§ Event listener management
on<K extends keyof WebSocketClientEvents>(
event: K,
listener: WebSocketClientEvents[K]
): void {
this.eventListeners[event] = listener;
}
off<K extends keyof WebSocketClientEvents>(event: K): void {
delete this.eventListeners[event];
}
private emit<K extends keyof WebSocketClientEvents>(
event: K,
...args: Parameters<NonNullable<WebSocketClientEvents[K]>>
): void {
const listener = this.eventListeners[event];
if (listener) {
(listener as any)(...args);
}
}
// π Connection management
async connect(): Promise<void> {
return new Promise((resolve, reject) => {
console.log('π Initiating WebSocket connection...');
// β° Connection timeout
this.connectionTimeoutTimer = window.setTimeout(() => {
reject(new Error('Connection timeout'));
}, this.config.connectionTimeout);
try {
this.socket = new WebSocket(this.url);
this.setupEventHandlers(resolve, reject);
} catch (error) {
this.clearConnectionTimeout();
reject(error);
}
});
}
private setupEventHandlers(
resolve: () => void,
reject: (error: Error) => void
): void {
if (!this.socket) return;
this.socket.onopen = () => {
console.log('β
WebSocket connected successfully!');
this.clearConnectionTimeout();
this.reconnectAttempts = 0;
this.startHeartbeat();
this.emit('connected');
resolve();
};
this.socket.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data);
// π Handle heartbeat responses
if (message.type === 'pong') {
console.log('π Heartbeat response received');
return;
}
console.log('π¨ Message received:', message.type);
this.emit('message', message);
} catch (error) {
console.error('β Failed to parse message:', error);
this.emit('error', new Error('Failed to parse message'));
}
};
this.socket.onclose = (event) => {
console.log(`π WebSocket closed: ${event.code} - ${event.reason}`);
this.cleanup();
this.emit('disconnected', event.reason);
if (this.config.autoReconnect && event.code !== 1000) {
this.attemptReconnect();
}
};
this.socket.onerror = (error) => {
console.error('β WebSocket error:', error);
this.clearConnectionTimeout();
this.emit('error', new Error('WebSocket connection error'));
reject(new Error('WebSocket connection error'));
};
}
// π Heartbeat mechanism
private startHeartbeat(): void {
if (this.config.heartbeatInterval && this.config.heartbeatInterval > 0) {
this.heartbeatTimer = window.setInterval(() => {
if (this.isConnected()) {
console.log('π Sending heartbeat...');
this.send({
type: 'ping',
timestamp: Date.now(),
id: crypto.randomUUID()
} as any);
}
}, this.config.heartbeatInterval);
}
}
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
// π Reconnection logic
private attemptReconnect(): void {
if (this.reconnectAttempts >= (this.config.maxReconnectAttempts || 5)) {
console.error('π₯ Max reconnection attempts exceeded');
return;
}
this.reconnectAttempts++;
const delay = (this.config.reconnectDelay || 1000) * this.reconnectAttempts;
console.log(`π Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})...`);
this.emit('reconnecting', this.reconnectAttempts);
setTimeout(() => {
this.connect().catch((error) => {
console.error('β Reconnection failed:', error);
});
}, delay);
}
// π€ Message sending with type safety
send<T extends WebSocketMessage>(message: T): boolean {
if (!this.isConnected()) {
console.warn('β οΈ Cannot send message: WebSocket not connected');
return false;
}
try {
this.socket!.send(JSON.stringify(message));
console.log('π€ Message sent:', message.type);
return true;
} catch (error) {
console.error('β Failed to send message:', error);
this.emit('error', new Error('Failed to send message'));
return false;
}
}
// π Connection state
isConnected(): boolean {
return this.socket?.readyState === WebSocket.OPEN;
}
getReadyState(): string {
if (!this.socket) return 'DISCONNECTED';
switch (this.socket.readyState) {
case WebSocket.CONNECTING: return 'CONNECTING';
case WebSocket.OPEN: return 'OPEN';
case WebSocket.CLOSING: return 'CLOSING';
case WebSocket.CLOSED: return 'CLOSED';
default: return 'UNKNOWN';
}
}
// π§Ή Cleanup
private clearConnectionTimeout(): void {
if (this.connectionTimeoutTimer) {
clearTimeout(this.connectionTimeoutTimer);
this.connectionTimeoutTimer = null;
}
}
private cleanup(): void {
this.stopHeartbeat();
this.clearConnectionTimeout();
}
// π Disconnect
disconnect(code = 1000, reason = 'Client disconnect'): void {
if (this.socket) {
console.log('π Disconnecting WebSocket...');
this.config.autoReconnect = false; // Prevent auto-reconnect
this.socket.close(code, reason);
}
this.cleanup();
}
}
π οΈ Real-World Chat Application
Letβs build a complete chat application that demonstrates real-world WebSocket usage:
// π¬ Chat application using our WebSocket client
interface User {
id: string;
username: string;
avatar?: string;
isOnline: boolean;
}
interface ChatRoom {
id: string;
name: string;
users: User[];
messageCount: number;
}
class ChatApplication {
private client: TypeSafeWebSocketClient;
private currentUser: User | null = null;
private currentRoom: ChatRoom | null = null;
private users = new Map<string, User>();
private messageHistory: ChatMessage[] = [];
private typingUsers = new Set<string>();
private typingTimer: number | null = null;
constructor(private websocketUrl: string) {
this.client = new TypeSafeWebSocketClient(websocketUrl, {
autoReconnect: true,
maxReconnectAttempts: 10,
heartbeatInterval: 30000
});
this.setupEventHandlers();
}
private setupEventHandlers(): void {
// π§ WebSocket event handlers
this.client.on('connected', () => {
console.log('π Connected to chat server!');
this.authenticateUser();
});
this.client.on('disconnected', (reason) => {
console.log('π Disconnected from chat server:', reason);
this.handleDisconnection();
});
this.client.on('message', (message) => {
this.handleIncomingMessage(message);
});
this.client.on('error', (error) => {
console.error('π₯ Chat client error:', error);
this.handleError(error);
});
this.client.on('reconnecting', (attempt) => {
console.log(`π Reconnecting to chat server (attempt ${attempt})...`);
});
}
// π Initialize chat application
async initialize(username: string): Promise<void> {
this.currentUser = {
id: crypto.randomUUID(),
username,
isOnline: true
};
try {
await this.client.connect();
console.log('β
Chat application initialized!');
} catch (error) {
console.error('β Failed to initialize chat:', error);
throw error;
}
}
private authenticateUser(): void {
if (!this.currentUser) return;
const authMessage = MessageFactory.createAuthMessage(
this.currentUser.id,
this.currentUser.username
);
this.client.send(authMessage);
}
// π¬ Send chat message
sendMessage(content: string): void {
if (!this.currentUser || !this.currentRoom) {
console.warn('β οΈ Cannot send message: User not authenticated or not in room');
return;
}
const message = MessageFactory.createChatMessage(
content,
this.currentUser.id,
this.currentUser.username
);
const success = this.client.send(message);
if (success) {
// Add to local history immediately for responsiveness
this.messageHistory.push(message);
this.notifyMessageSent(message);
}
}
// π Handle typing indicators
startTyping(): void {
if (!this.currentUser) return;
const typingMessage = MessageFactory.createTypingMessage(
this.currentUser.id,
this.currentUser.username,
true
);
this.client.send(typingMessage);
// π Auto-stop typing after 3 seconds
if (this.typingTimer) {
clearTimeout(this.typingTimer);
}
this.typingTimer = window.setTimeout(() => {
this.stopTyping();
}, 3000);
}
stopTyping(): void {
if (!this.currentUser) return;
if (this.typingTimer) {
clearTimeout(this.typingTimer);
this.typingTimer = null;
}
const typingMessage = MessageFactory.createTypingMessage(
this.currentUser.id,
this.currentUser.username,
false
);
this.client.send(typingMessage);
}
// π Room management
joinRoom(roomId: string): void {
if (!this.currentUser) return;
const joinMessage: JoinRoomMessage = {
type: 'join_room',
roomId,
userId: this.currentUser.id,
timestamp: Date.now(),
id: crypto.randomUUID()
};
this.client.send(joinMessage);
}
leaveRoom(): void {
if (!this.currentUser || !this.currentRoom) return;
const leaveMessage: LeaveRoomMessage = {
type: 'leave_room',
roomId: this.currentRoom.id,
userId: this.currentUser.id,
timestamp: Date.now(),
id: crypto.randomUUID()
};
this.client.send(leaveMessage);
this.currentRoom = null;
this.messageHistory = [];
this.users.clear();
}
// π¨ Handle incoming messages
private handleIncomingMessage(message: WebSocketMessage): void {
switch (message.type) {
case 'chat_message':
this.handleChatMessage(message);
break;
case 'user_joined':
this.handleUserJoined(message);
break;
case 'user_left':
this.handleUserLeft(message);
break;
case 'typing':
this.handleTypingIndicator(message);
break;
case 'room_joined':
this.handleRoomJoined(message as RoomJoinedMessage);
break;
case 'user_list':
this.handleUserList(message as UserListMessage);
break;
default:
console.warn('π€· Unknown message type:', (message as any).type);
}
}
private handleChatMessage(message: ChatMessage): void {
console.log(`π¬ ${message.username}: ${message.content}`);
// Avoid duplicates (message might already be in history if sent by us)
const exists = this.messageHistory.some(m => m.id === message.id);
if (!exists) {
this.messageHistory.push(message);
}
this.notifyNewMessage(message);
}
private handleUserJoined(message: UserJoinedMessage): void {
console.log(`π ${message.username} joined the chat`);
const user: User = {
id: message.userId,
username: message.username,
isOnline: true
};
this.users.set(user.id, user);
this.notifyUserJoined(user);
}
private handleUserLeft(message: UserLeftMessage): void {
console.log(`π ${message.username} left the chat`);
const user = this.users.get(message.userId);
if (user) {
user.isOnline = false;
this.notifyUserLeft(user);
}
}
private handleTypingIndicator(message: TypingMessage): void {
if (message.userId === this.currentUser?.id) return; // Ignore our own typing
if (message.isTyping) {
this.typingUsers.add(message.username);
console.log(`βοΈ ${message.username} is typing...`);
} else {
this.typingUsers.delete(message.username);
}
this.notifyTypingUpdate();
}
private handleRoomJoined(message: RoomJoinedMessage): void {
this.currentRoom = {
id: message.roomId,
name: message.roomName,
users: [],
messageCount: 0
};
console.log(`π Joined room: ${message.roomName}`);
this.notifyRoomJoined(this.currentRoom);
}
private handleUserList(message: UserListMessage): void {
this.users.clear();
message.users.forEach(userData => {
const user: User = {
id: userData.id,
username: userData.username,
isOnline: true
};
this.users.set(user.id, user);
});
if (this.currentRoom) {
this.currentRoom.users = Array.from(this.users.values());
}
this.notifyUserListUpdate();
}
private handleDisconnection(): void {
// Mark all users as offline
this.users.forEach(user => {
user.isOnline = false;
});
this.typingUsers.clear();
this.notifyDisconnection();
}
private handleError(error: Error): void {
this.notifyError(error);
}
// π Event notifications (override these in your UI layer)
protected notifyNewMessage(message: ChatMessage): void {
console.log('π New message notification');
}
protected notifyMessageSent(message: ChatMessage): void {
console.log('β
Message sent confirmation');
}
protected notifyUserJoined(user: User): void {
console.log('π User joined notification');
}
protected notifyUserLeft(user: User): void {
console.log('π User left notification');
}
protected notifyTypingUpdate(): void {
const typingList = Array.from(this.typingUsers);
console.log('βοΈ Typing users:', typingList);
}
protected notifyRoomJoined(room: ChatRoom): void {
console.log('π Room joined notification');
}
protected notifyUserListUpdate(): void {
console.log('π₯ User list updated');
}
protected notifyDisconnection(): void {
console.log('π Disconnection notification');
}
protected notifyError(error: Error): void {
console.error('β Error notification:', error);
}
// π Getters for current state
get onlineUsers(): User[] {
return Array.from(this.users.values()).filter(u => u.isOnline);
}
get messages(): ChatMessage[] {
return [...this.messageHistory];
}
get currentTypingUsers(): string[] {
return Array.from(this.typingUsers);
}
get isConnected(): boolean {
return this.client.isConnected();
}
get connectionState(): string {
return this.client.getReadyState();
}
// π Cleanup
disconnect(): void {
this.stopTyping();
this.client.disconnect();
}
}
// π Additional message types for complete chat functionality
interface AuthMessage extends BaseMessage {
type: 'auth';
userId: string;
username: string;
}
interface JoinRoomMessage extends BaseMessage {
type: 'join_room';
roomId: string;
userId: string;
}
interface LeaveRoomMessage extends BaseMessage {
type: 'leave_room';
roomId: string;
userId: string;
}
interface RoomJoinedMessage extends BaseMessage {
type: 'room_joined';
roomId: string;
roomName: string;
}
interface UserListMessage extends BaseMessage {
type: 'user_list';
users: Array<{
id: string;
username: string;
}>;
}
// π Extended message factory
class ExtendedMessageFactory extends MessageFactory {
static createAuthMessage(userId: string, username: string): AuthMessage {
return {
type: 'auth',
userId,
username,
timestamp: Date.now(),
id: crypto.randomUUID()
};
}
}
π οΈ Server-Side WebSocket Implementation
ποΈ Node.js WebSocket Server with TypeScript
Letβs create a robust WebSocket server using Node.js and the ws
library:
// π₯οΈ Server-side WebSocket implementation
import WebSocket from 'ws';
import { createServer } from 'http';
import { v4 as uuidv4 } from 'uuid';
interface ConnectedClient {
id: string;
socket: WebSocket;
user?: {
id: string;
username: string;
};
room?: string;
isAlive: boolean;
lastPong: number;
}
interface ChatRoom {
id: string;
name: string;
clients: Set<string>;
messageHistory: ChatMessage[];
createdAt: number;
}
class WebSocketChatServer {
private wss: WebSocket.Server;
private clients = new Map<string, ConnectedClient>();
private rooms = new Map<string, ChatRoom>();
private heartbeatInterval: NodeJS.Timeout;
constructor(port = 8080) {
// ποΈ Create HTTP server and WebSocket server
const server = createServer();
this.wss = new WebSocket.Server({ server });
// π§ Setup WebSocket event handlers
this.wss.on('connection', (socket, request) => {
this.handleConnection(socket, request);
});
// π Setup heartbeat mechanism
this.heartbeatInterval = setInterval(() => {
this.performHeartbeat();
}, 30000);
// π Start server
server.listen(port, () => {
console.log(`π WebSocket server running on port ${port}`);
});
// π§Ή Cleanup on shutdown
process.on('SIGTERM', () => this.shutdown());
process.on('SIGINT', () => this.shutdown());
}
private handleConnection(socket: WebSocket, request: any): void {
const clientId = uuidv4();
const client: ConnectedClient = {
id: clientId,
socket,
isAlive: true,
lastPong: Date.now()
};
this.clients.set(clientId, client);
console.log(`π New client connected: ${clientId}`);
console.log(`π₯ Total clients: ${this.clients.size}`);
// π§ Setup client event handlers
socket.on('message', (data) => {
this.handleMessage(clientId, data);
});
socket.on('close', (code, reason) => {
this.handleDisconnection(clientId, code, reason.toString());
});
socket.on('error', (error) => {
console.error(`β Client ${clientId} error:`, error);
});
socket.on('pong', () => {
const client = this.clients.get(clientId);
if (client) {
client.isAlive = true;
client.lastPong = Date.now();
}
});
// π Send welcome message
this.sendToClient(clientId, {
type: 'server_message',
content: 'Welcome to the chat server!',
timestamp: Date.now(),
id: uuidv4()
} as any);
}
private handleMessage(clientId: string, data: WebSocket.Data): void {
const client = this.clients.get(clientId);
if (!client) return;
try {
const message: WebSocketMessage = JSON.parse(data.toString());
console.log(`π¨ Message from ${clientId}:`, message.type);
switch (message.type) {
case 'auth':
this.handleAuth(clientId, message as AuthMessage);
break;
case 'join_room':
this.handleJoinRoom(clientId, message as JoinRoomMessage);
break;
case 'leave_room':
this.handleLeaveRoom(clientId, message as LeaveRoomMessage);
break;
case 'chat_message':
this.handleChatMessage(clientId, message as ChatMessage);
break;
case 'typing':
this.handleTyping(clientId, message as TypingMessage);
break;
case 'ping':
this.handlePing(clientId);
break;
default:
console.warn(`π€· Unknown message type: ${(message as any).type}`);
}
} catch (error) {
console.error(`β Failed to parse message from ${clientId}:`, error);
this.sendToClient(clientId, {
type: 'error',
message: 'Invalid message format',
timestamp: Date.now(),
id: uuidv4()
} as any);
}
}
private handleAuth(clientId: string, message: AuthMessage): void {
const client = this.clients.get(clientId);
if (!client) return;
// π Set user information
client.user = {
id: message.userId,
username: message.username
};
console.log(`β
Client ${clientId} authenticated as ${message.username}`);
// π Send authentication confirmation
this.sendToClient(clientId, {
type: 'auth_success',
userId: message.userId,
username: message.username,
timestamp: Date.now(),
id: uuidv4()
} as any);
// π Send available rooms
this.sendRoomList(clientId);
}
private handleJoinRoom(clientId: string, message: JoinRoomMessage): void {
const client = this.clients.get(clientId);
if (!client || !client.user) return;
const roomId = message.roomId;
// π Create room if it doesn't exist
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, {
id: roomId,
name: `Room ${roomId}`,
clients: new Set(),
messageHistory: [],
createdAt: Date.now()
});
console.log(`π Created new room: ${roomId}`);
}
const room = this.rooms.get(roomId)!;
// πͺ Leave current room if in one
if (client.room) {
this.leaveClientFromRoom(clientId);
}
// π Join new room
client.room = roomId;
room.clients.add(clientId);
console.log(`π ${client.user.username} joined room ${roomId}`);
// π Send room joined confirmation
this.sendToClient(clientId, {
type: 'room_joined',
roomId,
roomName: room.name,
timestamp: Date.now(),
id: uuidv4()
} as any);
// π Send message history
room.messageHistory.forEach(msg => {
this.sendToClient(clientId, msg);
});
// π₯ Send user list
this.sendUserListToRoom(roomId);
// π’ Announce user joined to room
const joinMessage: UserJoinedMessage = {
type: 'user_joined',
userId: client.user.id,
username: client.user.username,
timestamp: Date.now(),
id: uuidv4()
};
this.broadcastToRoom(roomId, joinMessage, clientId);
}
private handleLeaveRoom(clientId: string, message: LeaveRoomMessage): void {
this.leaveClientFromRoom(clientId);
}
private leaveClientFromRoom(clientId: string): void {
const client = this.clients.get(clientId);
if (!client || !client.room || !client.user) return;
const room = this.rooms.get(client.room);
if (!room) return;
// πͺ Remove from room
room.clients.delete(clientId);
console.log(`πͺ ${client.user.username} left room ${client.room}`);
// π’ Announce user left to room
const leftMessage: UserLeftMessage = {
type: 'user_left',
userId: client.user.id,
username: client.user.username,
timestamp: Date.now(),
id: uuidv4()
};
this.broadcastToRoom(client.room, leftMessage, clientId);
// π₯ Update user list for remaining clients
this.sendUserListToRoom(client.room);
// π§Ή Clean up empty rooms
if (room.clients.size === 0) {
this.rooms.delete(client.room);
console.log(`ποΈ Removed empty room: ${client.room}`);
}
client.room = undefined;
}
private handleChatMessage(clientId: string, message: ChatMessage): void {
const client = this.clients.get(clientId);
if (!client || !client.room || !client.user) {
console.warn(`β οΈ Unauthorized chat message from ${clientId}`);
return;
}
const room = this.rooms.get(client.room);
if (!room) return;
// πΎ Store message in room history
room.messageHistory.push(message);
// π Limit history size (keep last 100 messages)
if (room.messageHistory.length > 100) {
room.messageHistory = room.messageHistory.slice(-100);
}
console.log(`π¬ ${client.user.username} in ${client.room}: ${message.content}`);
// π’ Broadcast message to all room members
this.broadcastToRoom(client.room, message);
}
private handleTyping(clientId: string, message: TypingMessage): void {
const client = this.clients.get(clientId);
if (!client || !client.room) return;
// π’ Broadcast typing indicator to room (except sender)
this.broadcastToRoom(client.room, message, clientId);
}
private handlePing(clientId: string): void {
// π Respond with pong
this.sendToClient(clientId, {
type: 'pong',
timestamp: Date.now(),
id: uuidv4()
} as any);
}
private handleDisconnection(clientId: string, code: number, reason: string): void {
const client = this.clients.get(clientId);
console.log(`π Client ${clientId} disconnected: ${code} - ${reason}`);
if (client && client.room) {
this.leaveClientFromRoom(clientId);
}
this.clients.delete(clientId);
console.log(`π₯ Total clients: ${this.clients.size}`);
}
// π Heartbeat mechanism
private performHeartbeat(): void {
const now = Date.now();
this.clients.forEach((client, clientId) => {
if (!client.isAlive || (now - client.lastPong) > 60000) {
// π Client is dead, terminate connection
console.log(`π Terminating dead client: ${clientId}`);
client.socket.terminate();
return;
}
client.isAlive = false;
client.socket.ping();
});
}
// π€ Send message to specific client
private sendToClient(clientId: string, message: any): void {
const client = this.clients.get(clientId);
if (!client || client.socket.readyState !== WebSocket.OPEN) return;
try {
client.socket.send(JSON.stringify(message));
} catch (error) {
console.error(`β Failed to send message to ${clientId}:`, error);
}
}
// π’ Broadcast message to all clients in a room
private broadcastToRoom(roomId: string, message: any, excludeClientId?: string): void {
const room = this.rooms.get(roomId);
if (!room) return;
room.clients.forEach(clientId => {
if (clientId !== excludeClientId) {
this.sendToClient(clientId, message);
}
});
}
// π Send room list to client
private sendRoomList(clientId: string): void {
const roomList = Array.from(this.rooms.values()).map(room => ({
id: room.id,
name: room.name,
userCount: room.clients.size
}));
this.sendToClient(clientId, {
type: 'room_list',
rooms: roomList,
timestamp: Date.now(),
id: uuidv4()
});
}
// π₯ Send user list to all clients in room
private sendUserListToRoom(roomId: string): void {
const room = this.rooms.get(roomId);
if (!room) return;
const users = Array.from(room.clients)
.map(clientId => {
const client = this.clients.get(clientId);
return client?.user ? {
id: client.user.id,
username: client.user.username
} : null;
})
.filter(user => user !== null);
const userListMessage = {
type: 'user_list',
users,
timestamp: Date.now(),
id: uuidv4()
};
this.broadcastToRoom(roomId, userListMessage);
}
// π Server statistics
getStats(): any {
return {
totalClients: this.clients.size,
totalRooms: this.rooms.size,
authenticatedClients: Array.from(this.clients.values()).filter(c => c.user).length,
roomsWithClients: Array.from(this.rooms.values()).filter(r => r.clients.size > 0).length
};
}
// π Shutdown server gracefully
private shutdown(): void {
console.log('π Shutting down WebSocket server...');
clearInterval(this.heartbeatInterval);
// π Send goodbye message to all clients
this.clients.forEach((client, clientId) => {
this.sendToClient(clientId, {
type: 'server_shutdown',
message: 'Server is shutting down',
timestamp: Date.now(),
id: uuidv4()
});
setTimeout(() => {
client.socket.close(1001, 'Server shutdown');
}, 1000);
});
// π Close WebSocket server
setTimeout(() => {
this.wss.close(() => {
console.log('β
WebSocket server closed');
process.exit(0);
});
}, 2000);
}
}
// π Start the server
const chatServer = new WebSocketChatServer(8080);
// π Log stats every 30 seconds
setInterval(() => {
const stats = chatServer.getStats();
console.log('π Server stats:', stats);
}, 30000);
π¨ Advanced WebSocket Patterns
π Connection Pool Management
For applications that need to manage multiple WebSocket connections:
// π WebSocket connection pool for managing multiple connections
interface ConnectionConfig {
url: string;
protocols?: string[];
autoReconnect?: boolean;
maxReconnectAttempts?: number;
}
class WebSocketConnectionPool {
private connections = new Map<string, TypeSafeWebSocketClient>();
private connectionConfigs = new Map<string, ConnectionConfig>();
async addConnection(
name: string,
config: ConnectionConfig
): Promise<TypeSafeWebSocketClient> {
if (this.connections.has(name)) {
throw new Error(`Connection '${name}' already exists`);
}
console.log(`π Adding connection: ${name}`);
const client = new TypeSafeWebSocketClient(config.url, {
autoReconnect: config.autoReconnect ?? true,
maxReconnectAttempts: config.maxReconnectAttempts ?? 5
});
// π§ Setup common event handlers
client.on('connected', () => {
console.log(`β
Connection '${name}' established`);
});
client.on('disconnected', (reason) => {
console.log(`π Connection '${name}' disconnected: ${reason}`);
});
client.on('error', (error) => {
console.error(`β Connection '${name}' error:`, error);
});
try {
await client.connect();
this.connections.set(name, client);
this.connectionConfigs.set(name, config);
return client;
} catch (error) {
console.error(`β Failed to add connection '${name}':`, error);
throw error;
}
}
getConnection(name: string): TypeSafeWebSocketClient | undefined {
return this.connections.get(name);
}
async removeConnection(name: string): Promise<void> {
const connection = this.connections.get(name);
if (!connection) {
console.warn(`β οΈ Connection '${name}' not found`);
return;
}
console.log(`ποΈ Removing connection: ${name}`);
connection.disconnect();
this.connections.delete(name);
this.connectionConfigs.delete(name);
}
// π’ Broadcast message to all connections
broadcastToAll(message: any): void {
this.connections.forEach((connection, name) => {
if (connection.isConnected()) {
connection.send(message);
} else {
console.warn(`β οΈ Connection '${name}' not connected, skipping broadcast`);
}
});
}
// π’ Broadcast to specific connections
broadcastToConnections(connectionNames: string[], message: any): void {
connectionNames.forEach(name => {
const connection = this.connections.get(name);
if (connection?.isConnected()) {
connection.send(message);
} else {
console.warn(`β οΈ Connection '${name}' not found or not connected`);
}
});
}
// π Get pool status
getStatus(): Array<{ name: string; connected: boolean; state: string }> {
return Array.from(this.connections.entries()).map(([name, connection]) => ({
name,
connected: connection.isConnected(),
state: connection.getReadyState()
}));
}
// π Reconnect all disconnected connections
async reconnectAll(): Promise<void> {
const reconnectPromises = Array.from(this.connections.entries())
.filter(([, connection]) => !connection.isConnected())
.map(async ([name, connection]) => {
try {
console.log(`π Reconnecting '${name}'...`);
await connection.connect();
console.log(`β
'${name}' reconnected`);
} catch (error) {
console.error(`β Failed to reconnect '${name}':`, error);
}
});
await Promise.allSettled(reconnectPromises);
}
// π§Ή Cleanup all connections
async cleanup(): Promise<void> {
console.log('π§Ή Cleaning up WebSocket connection pool...');
const cleanupPromises = Array.from(this.connections.keys()).map(name =>
this.removeConnection(name)
);
await Promise.allSettled(cleanupPromises);
console.log('β
Connection pool cleanup complete');
}
}
π WebSocket Authentication and Security
Implementing secure WebSocket connections with authentication:
// π Secure WebSocket client with authentication
interface AuthConfig {
token?: string;
apiKey?: string;
username?: string;
password?: string;
}
class SecureWebSocketClient extends TypeSafeWebSocketClient {
private authConfig: AuthConfig;
private isAuthenticated = false;
constructor(url: string, authConfig: AuthConfig, config?: WebSocketClientConfig) {
super(url, config);
this.authConfig = authConfig;
}
async connect(): Promise<void> {
// π Connect to WebSocket
await super.connect();
// π Authenticate after connection
await this.authenticate();
}
private async authenticate(): Promise<void> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Authentication timeout'));
}, 10000);
// π§ Listen for auth response
const originalMessageHandler = this.handleMessage;
this.handleMessage = (message: WebSocketMessage) => {
if ((message as any).type === 'auth_success') {
console.log('β
Authentication successful!');
clearTimeout(timeout);
this.isAuthenticated = true;
this.handleMessage = originalMessageHandler;
resolve();
} else if ((message as any).type === 'auth_error') {
console.error('β Authentication failed:', (message as any).error);
clearTimeout(timeout);
reject(new Error((message as any).error || 'Authentication failed'));
} else {
// Pass other messages to original handler
originalMessageHandler.call(this, message);
}
};
// π€ Send authentication message
const authMessage = this.createAuthMessage();
super.send(authMessage);
});
}
private createAuthMessage(): any {
const baseMessage = {
type: 'authenticate',
timestamp: Date.now(),
id: crypto.randomUUID()
};
if (this.authConfig.token) {
return {
...baseMessage,
method: 'token',
token: this.authConfig.token
};
}
if (this.authConfig.apiKey) {
return {
...baseMessage,
method: 'api_key',
apiKey: this.authConfig.apiKey
};
}
if (this.authConfig.username && this.authConfig.password) {
return {
...baseMessage,
method: 'credentials',
username: this.authConfig.username,
password: this.authConfig.password
};
}
throw new Error('No valid authentication method provided');
}
// π Override send to ensure authentication
send<T extends WebSocketMessage>(message: T): boolean {
if (!this.isAuthenticated) {
console.warn('β οΈ Cannot send message: Not authenticated');
return false;
}
return super.send(message);
}
// π Update authentication credentials
updateAuth(newAuthConfig: AuthConfig): void {
this.authConfig = { ...this.authConfig, ...newAuthConfig };
this.isAuthenticated = false;
if (this.isConnected()) {
this.authenticate().catch(error => {
console.error('β Re-authentication failed:', error);
this.emit('error', error);
});
}
}
get authenticated(): boolean {
return this.isAuthenticated;
}
}
// π‘οΈ Message encryption for sensitive data
class EncryptedWebSocketClient extends SecureWebSocketClient {
private encryptionKey: string;
constructor(
url: string,
authConfig: AuthConfig,
encryptionKey: string,
config?: WebSocketClientConfig
) {
super(url, authConfig, config);
this.encryptionKey = encryptionKey;
}
// π Encrypt message before sending
send<T extends WebSocketMessage>(message: T): boolean {
try {
const encryptedMessage = {
type: 'encrypted',
data: this.encrypt(JSON.stringify(message)),
timestamp: Date.now(),
id: crypto.randomUUID()
};
return super.send(encryptedMessage as any);
} catch (error) {
console.error('β Failed to encrypt message:', error);
return false;
}
}
// π Decrypt incoming messages
protected handleMessage(message: WebSocketMessage): void {
if ((message as any).type === 'encrypted') {
try {
const decryptedData = this.decrypt((message as any).data);
const originalMessage = JSON.parse(decryptedData);
super.handleMessage(originalMessage);
} catch (error) {
console.error('β Failed to decrypt message:', error);
}
} else {
super.handleMessage(message);
}
}
// π Simple encryption (use a proper crypto library in production!)
private encrypt(data: string): string {
// β οΈ This is a simple example - use proper encryption in production!
return btoa(data + this.encryptionKey);
}
// π Simple decryption
private decrypt(encryptedData: string): string {
// β οΈ This is a simple example - use proper decryption in production!
const decoded = atob(encryptedData);
return decoded.substring(0, decoded.length - this.encryptionKey.length);
}
}
π Performance Optimization and Best Practices
β‘ Message Batching and Throttling
For high-frequency applications, implement message batching:
// β‘ High-performance WebSocket client with message batching
interface BatchConfig {
maxBatchSize: number;
maxBatchDelay: number;
enableCompression: boolean;
}
class BatchedWebSocketClient extends TypeSafeWebSocketClient {
private messageBatch: WebSocketMessage[] = [];
private batchTimer: number | null = null;
private batchConfig: BatchConfig;
constructor(
url: string,
batchConfig: BatchConfig = {
maxBatchSize: 10,
maxBatchDelay: 100,
enableCompression: false
},
config?: WebSocketClientConfig
) {
super(url, config);
this.batchConfig = batchConfig;
}
// π¦ Batched message sending
send<T extends WebSocketMessage>(message: T): boolean {
if (!this.isConnected()) {
console.warn('β οΈ Cannot batch message: WebSocket not connected');
return false;
}
// π₯ Add message to batch
this.messageBatch.push(message);
console.log(`π¦ Added message to batch (${this.messageBatch.length}/${this.batchConfig.maxBatchSize})`);
// π Send immediately if batch is full
if (this.messageBatch.length >= this.batchConfig.maxBatchSize) {
this.flushBatch();
return true;
}
// β° Schedule batch send if not already scheduled
if (!this.batchTimer) {
this.batchTimer = window.setTimeout(() => {
this.flushBatch();
}, this.batchConfig.maxBatchDelay);
}
return true;
}
// π Send all batched messages
private flushBatch(): void {
if (this.messageBatch.length === 0) return;
console.log(`π Flushing batch of ${this.messageBatch.length} messages`);
const batchMessage = {
type: 'batch',
messages: this.messageBatch,
compressed: this.batchConfig.enableCompression,
timestamp: Date.now(),
id: crypto.randomUUID()
};
// ποΈ Compress if enabled
if (this.batchConfig.enableCompression) {
batchMessage.messages = this.compressMessages(this.messageBatch);
}
// π€ Send the batch
const success = super.send(batchMessage as any);
if (success) {
// π§Ή Clear batch
this.messageBatch = [];
// β° Clear timer
if (this.batchTimer) {
clearTimeout(this.batchTimer);
this.batchTimer = null;
}
} else {
console.error('β Failed to send batch, keeping messages for retry');
}
}
// ποΈ Simple message compression
private compressMessages(messages: WebSocketMessage[]): any {
// Simple compression: group messages by type
const compressed: { [type: string]: WebSocketMessage[] } = {};
messages.forEach(message => {
if (!compressed[message.type]) {
compressed[message.type] = [];
}
compressed[message.type].push(message);
});
return compressed;
}
// π¨ Force flush on disconnect
disconnect(code?: number, reason?: string): void {
this.flushBatch(); // Send any pending messages
super.disconnect(code, reason);
}
}
// ποΈ Message throttling for rate limiting
class ThrottledWebSocketClient extends TypeSafeWebSocketClient {
private messageQueue: Array<{ message: WebSocketMessage; timestamp: number }> = [];
private rateLimitWindow = 1000; // 1 second
private maxMessagesPerWindow = 10;
send<T extends WebSocketMessage>(message: T): boolean {
const now = Date.now();
// π§Ή Clean old messages from queue
this.messageQueue = this.messageQueue.filter(
item => now - item.timestamp < this.rateLimitWindow
);
// π« Check rate limit
if (this.messageQueue.length >= this.maxMessagesPerWindow) {
console.warn('β οΈ Rate limit exceeded, dropping message');
return false;
}
// π₯ Add to queue and send
this.messageQueue.push({ message, timestamp: now });
return super.send(message);
}
// π Get current rate limit status
getRateLimitStatus(): { used: number; limit: number; resetTime: number } {
const now = Date.now();
const validMessages = this.messageQueue.filter(
item => now - item.timestamp < this.rateLimitWindow
);
const oldestMessage = validMessages[0];
const resetTime = oldestMessage
? oldestMessage.timestamp + this.rateLimitWindow
: now;
return {
used: validMessages.length,
limit: this.maxMessagesPerWindow,
resetTime
};
}
}
π§ͺ Testing WebSocket Applications
π¬ Unit Testing WebSocket Clients
// π§ͺ Testing utilities for WebSocket applications
import { jest } from '@jest/globals';
// π Mock WebSocket for testing
class MockWebSocket {
static CONNECTING = 0;
static OPEN = 1;
static CLOSING = 2;
static CLOSED = 3;
readyState = MockWebSocket.CONNECTING;
onopen: ((event: Event) => void) | null = null;
onclose: ((event: CloseEvent) => void) | null = null;
onmessage: ((event: MessageEvent) => void) | null = null;
onerror: ((event: Event) => void) | null = null;
private listeners: { [key: string]: Function[] } = {};
constructor(public url: string, public protocols?: string | string[]) {
// π Simulate async connection
setTimeout(() => {
this.readyState = MockWebSocket.OPEN;
if (this.onopen) {
this.onopen(new Event('open'));
}
}, 10);
}
send(data: string | ArrayBuffer | Blob): void {
if (this.readyState !== MockWebSocket.OPEN) {
throw new Error('WebSocket is not open');
}
// Echo messages back for testing
setTimeout(() => {
if (this.onmessage) {
this.onmessage({
data: `echo: ${data}`,
type: 'message'
} as MessageEvent);
}
}, 1);
}
close(code?: number, reason?: string): void {
this.readyState = MockWebSocket.CLOSING;
setTimeout(() => {
this.readyState = MockWebSocket.CLOSED;
if (this.onclose) {
this.onclose({
code: code || 1000,
reason: reason || '',
wasClean: true
} as CloseEvent);
}
}, 1);
}
// Test helpers
simulateMessage(data: any): void {
if (this.onmessage) {
this.onmessage({
data: JSON.stringify(data),
type: 'message'
} as MessageEvent);
}
}
simulateError(): void {
if (this.onerror) {
this.onerror(new Event('error'));
}
}
simulateClose(code = 1000, reason = ''): void {
this.readyState = MockWebSocket.CLOSED;
if (this.onclose) {
this.onclose({
code,
reason,
wasClean: code === 1000
} as CloseEvent);
}
}
}
// π§ͺ Test suite for WebSocket client
describe('WebSocket Client Tests', () => {
let mockWebSocket: MockWebSocket;
let client: TypeSafeWebSocketClient;
beforeEach(() => {
// π Replace global WebSocket with mock
(global as any).WebSocket = MockWebSocket;
client = new TypeSafeWebSocketClient('ws://localhost:8080');
});
afterEach(() => {
client.disconnect();
});
test('π should connect successfully', async () => {
const connectPromise = client.connect();
// β
Should resolve when connection opens
await expect(connectPromise).resolves.toBeUndefined();
expect(client.isConnected()).toBe(true);
});
test('π€ should send messages when connected', async () => {
await client.connect();
const testMessage = {
type: 'test',
content: 'Hello WebSocket!',
timestamp: Date.now(),
id: 'test-123'
};
const success = client.send(testMessage as any);
expect(success).toBe(true);
});
test('π« should not send messages when disconnected', () => {
const testMessage = {
type: 'test',
content: 'Hello WebSocket!',
timestamp: Date.now(),
id: 'test-123'
};
const success = client.send(testMessage as any);
expect(success).toBe(false);
});
test('π¨ should handle incoming messages', async () => {
await client.connect();
const messageHandler = jest.fn();
client.on('message', messageHandler);
// π¨ Simulate incoming message
const testMessage = {
type: 'chat_message',
content: 'Test message',
userId: 'user-123',
username: 'testuser',
timestamp: Date.now(),
id: 'msg-123'
};
// Access the underlying mock WebSocket
const ws = (client as any).socket as MockWebSocket;
ws.simulateMessage(testMessage);
// β° Wait for message processing
await new Promise(resolve => setTimeout(resolve, 10));
expect(messageHandler).toHaveBeenCalledWith(testMessage);
});
test('π should attempt reconnection on unexpected disconnect', async () => {
const client = new TypeSafeWebSocketClient('ws://localhost:8080', {
autoReconnect: true,
maxReconnectAttempts: 2,
reconnectDelay: 100
});
await client.connect();
const reconnectingHandler = jest.fn();
client.on('reconnecting', reconnectingHandler);
// π₯ Simulate unexpected disconnect
const ws = (client as any).socket as MockWebSocket;
ws.simulateClose(1006, 'Connection lost');
// β° Wait for reconnection attempt
await new Promise(resolve => setTimeout(resolve, 150));
expect(reconnectingHandler).toHaveBeenCalledWith(1);
});
test('β should handle connection errors', async () => {
const errorHandler = jest.fn();
client.on('error', errorHandler);
// Start connection
const connectPromise = client.connect();
// π₯ Simulate connection error
setTimeout(() => {
const ws = (client as any).socket as MockWebSocket;
ws.simulateError();
}, 5);
// β Should reject the connection promise
await expect(connectPromise).rejects.toThrow();
expect(errorHandler).toHaveBeenCalled();
});
test('π should handle heartbeat', async () => {
const client = new TypeSafeWebSocketClient('ws://localhost:8080', {
heartbeatInterval: 100
});
await client.connect();
// β° Wait for heartbeat
await new Promise(resolve => setTimeout(resolve, 150));
// π Verify heartbeat was sent (this would need access to sent messages)
// In a real implementation, you'd check that a ping message was sent
expect(client.isConnected()).toBe(true);
});
});
// π Integration test with mock server
describe('WebSocket Integration Tests', () => {
let mockServer: MockWebSocketServer;
beforeEach(() => {
mockServer = new MockWebSocketServer();
});
afterEach(() => {
mockServer.stop();
});
test('π¬ should handle chat flow', async () => {
const client1 = new ChatApplication('ws://localhost:8080');
const client2 = new ChatApplication('ws://localhost:8080');
// π Connect both clients
await client1.initialize('Alice');
await client2.initialize('Bob');
// π Join the same room
client1.joinRoom('test-room');
client2.joinRoom('test-room');
// β° Wait for room join
await new Promise(resolve => setTimeout(resolve, 50));
// π¬ Alice sends a message
client1.sendMessage('Hello Bob!');
// β° Wait for message delivery
await new Promise(resolve => setTimeout(resolve, 50));
// β
Bob should receive the message
const bobMessages = client2.messages;
expect(bobMessages).toHaveLength(1);
expect(bobMessages[0].content).toBe('Hello Bob!');
expect(bobMessages[0].username).toBe('Alice');
});
});
// π Mock WebSocket server for integration tests
class MockWebSocketServer {
private clients: MockWebSocket[] = [];
constructor() {
// Override WebSocket constructor to track connections
const originalWebSocket = (global as any).WebSocket;
(global as any).WebSocket = class extends MockWebSocket {
constructor(url: string, protocols?: string | string[]) {
super(url, protocols);
this.registerClient(this);
}
};
}
private registerClient(client: MockWebSocket): void {
this.clients.push(client);
// π§ Handle client messages
client.onmessage = (event) => {
this.handleClientMessage(client, JSON.parse(event.data));
};
}
private handleClientMessage(sender: MockWebSocket, message: any): void {
// π’ Broadcast to all other clients
this.clients.forEach(client => {
if (client !== sender && client.readyState === MockWebSocket.OPEN) {
client.simulateMessage(message);
}
});
}
stop(): void {
this.clients.forEach(client => {
client.simulateClose();
});
this.clients = [];
}
}
π― Common Pitfalls and Best Practices
β Common Mistakes to Avoid
// β Common WebSocket pitfalls and their solutions
class WebSocketPitfalls {
// β WRONG: Not handling connection states properly
static wrongConnectionHandling(): void {
const socket = new WebSocket('ws://localhost:8080');
// π₯ This will fail - socket might not be open yet!
socket.send(JSON.stringify({ type: 'immediate' }));
}
// β
CORRECT: Wait for connection before sending
static correctConnectionHandling(): void {
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = () => {
// β
Now it's safe to send
socket.send(JSON.stringify({ type: 'safe' }));
};
}
// β WRONG: Not handling reconnections
static wrongReconnectionHandling(): void {
const socket = new WebSocket('ws://localhost:8080');
socket.onclose = () => {
console.log('Connection closed'); // π₯ That's it? No reconnection!
};
}
// β
CORRECT: Implement proper reconnection logic
static correctReconnectionHandling(): void {
let reconnectAttempts = 0;
const maxAttempts = 5;
function connect(): void {
const socket = new WebSocket('ws://localhost:8080');
socket.onclose = (event) => {
if (event.code !== 1000 && reconnectAttempts < maxAttempts) {
reconnectAttempts++;
setTimeout(() => connect(), 1000 * reconnectAttempts);
}
};
}
connect();
}
// β WRONG: Not validating incoming messages
static wrongMessageHandling(): void {
const socket = new WebSocket('ws://localhost:8080');
socket.onmessage = (event) => {
// π₯ This could crash if data is invalid JSON!
const message = JSON.parse(event.data);
console.log(message.content.toUpperCase()); // π₯ What if content is undefined?
};
}
// β
CORRECT: Always validate incoming data
static correctMessageHandling(): void {
const socket = new WebSocket('ws://localhost:8080');
socket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
// β
Validate message structure
if (typeof message === 'object' && message.type) {
switch (message.type) {
case 'chat_message':
if (typeof message.content === 'string') {
console.log(message.content.toUpperCase());
}
break;
// Handle other message types...
}
}
} catch (error) {
console.error('Invalid message received:', error);
}
};
}
// β WRONG: Memory leaks with event listeners
static wrongEventListenerCleanup(): void {
const socket = new WebSocket('ws://localhost:8080');
// π₯ These listeners are never cleaned up!
socket.onmessage = (event) => { /* handle */ };
socket.onerror = (error) => { /* handle */ };
// Later in the app...
// socket = null; // π₯ Memory leak! Listeners still exist
}
// β
CORRECT: Properly cleanup event listeners
static correctEventListenerCleanup(): void {
const socket = new WebSocket('ws://localhost:8080');
const messageHandler = (event: MessageEvent) => { /* handle */ };
const errorHandler = (event: Event) => { /* handle */ };
socket.addEventListener('message', messageHandler);
socket.addEventListener('error', errorHandler);
// β
Cleanup function
function cleanup(): void {
socket.removeEventListener('message', messageHandler);
socket.removeEventListener('error', errorHandler);
socket.close();
}
// Call cleanup when component unmounts or app closes
window.addEventListener('beforeunload', cleanup);
}
// β WRONG: Not handling large message payloads
static wrongLargeMessageHandling(): void {
const socket = new WebSocket('ws://localhost:8080');
// π₯ This could overwhelm the connection!
const hugeData = new Array(1000000).fill('data');
socket.send(JSON.stringify(hugeData));
}
// β
CORRECT: Implement message chunking for large data
static correctLargeMessageHandling(): void {
const socket = new WebSocket('ws://localhost:8080');
function sendLargeData(data: any[], chunkSize = 1000): void {
const chunks = [];
for (let i = 0; i < data.length; i += chunkSize) {
chunks.push(data.slice(i, i + chunkSize));
}
const messageId = crypto.randomUUID();
chunks.forEach((chunk, index) => {
socket.send(JSON.stringify({
type: 'data_chunk',
messageId,
chunkIndex: index,
totalChunks: chunks.length,
data: chunk
}));
});
}
}
// β WRONG: Blocking the main thread with heavy processing
static wrongHeavyProcessing(): void {
const socket = new WebSocket('ws://localhost:8080');
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
// π₯ This blocks the UI thread!
for (let i = 0; i < 1000000; i++) {
// Heavy computation
Math.sqrt(data.numbers[i % data.numbers.length]);
}
};
}
// β
CORRECT: Use Web Workers for heavy processing
static correctHeavyProcessing(): void {
const socket = new WebSocket('ws://localhost:8080');
const worker = new Worker('/heavy-processor.js');
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
// β
Offload to worker thread
worker.postMessage(data);
};
worker.onmessage = (event) => {
// β
Process results without blocking UI
console.log('Heavy processing complete:', event.data);
};
}
}
π Best Practices Summary
// π WebSocket best practices checklist
class WebSocketBestPractices {
// β
1. Always implement proper error handling
static implementErrorHandling(): void {
const client = new TypeSafeWebSocketClient('ws://localhost:8080');
client.on('error', (error) => {
console.error('WebSocket error:', error);
// Implement user notification
// Log to error tracking service
// Attempt recovery if possible
});
}
// β
2. Use heartbeat/ping-pong to detect dead connections
static implementHeartbeat(): void {
const client = new TypeSafeWebSocketClient('ws://localhost:8080', {
heartbeatInterval: 30000 // Ping every 30 seconds
});
}
// β
3. Implement exponential backoff for reconnections
static implementExponentialBackoff(): void {
const client = new TypeSafeWebSocketClient('ws://localhost:8080', {
autoReconnect: true,
maxReconnectAttempts: 10,
reconnectDelay: 1000 // Will be multiplied by attempt number
});
}
// β
4. Use proper message validation and typing
static implementMessageValidation(): void {
interface ValidMessage {
type: string;
timestamp: number;
id: string;
}
function isValidMessage(data: any): data is ValidMessage {
return (
typeof data === 'object' &&
typeof data.type === 'string' &&
typeof data.timestamp === 'number' &&
typeof data.id === 'string'
);
}
const client = new TypeSafeWebSocketClient('ws://localhost:8080');
client.on('message', (message) => {
if (isValidMessage(message)) {
// β
Type-safe processing
console.log(`Valid message: ${message.type}`);
}
});
}
// β
5. Implement proper cleanup
static implementCleanup(): void {
class ComponentWithWebSocket {
private client: TypeSafeWebSocketClient;
constructor() {
this.client = new TypeSafeWebSocketClient('ws://localhost:8080');
}
// β
Always cleanup on component unmount
destroy(): void {
this.client.disconnect();
// Clear any timers
// Remove event listeners
// Cancel pending operations
}
}
}
// β
6. Use connection pooling for multiple connections
static useConnectionPooling(): void {
const pool = new WebSocketConnectionPool();
// Add connections for different services
pool.addConnection('chat', { url: 'ws://chat.example.com' });
pool.addConnection('notifications', { url: 'ws://notifications.example.com' });
pool.addConnection('live-data', { url: 'ws://data.example.com' });
}
// β
7. Implement proper security measures
static implementSecurity(): void {
// Use secure WebSocket (WSS) in production
const client = new SecureWebSocketClient(
'wss://secure.example.com',
{ token: 'your-auth-token' }
);
// Validate all incoming data
// Implement rate limiting
// Use encryption for sensitive data
}
// β
8. Monitor connection quality
static monitorConnectionQuality(): void {
const client = new TypeSafeWebSocketClient('ws://localhost:8080');
let messagesSent = 0;
let messagesReceived = 0;
let latencySum = 0;
let latencyCount = 0;
// Track sent messages
const originalSend = client.send.bind(client);
client.send = (message: any) => {
messagesSent++;
(message as any).sentAt = Date.now();
return originalSend(message);
};
// Track received messages and latency
client.on('message', (message) => {
messagesReceived++;
if ((message as any).sentAt) {
const latency = Date.now() - (message as any).sentAt;
latencySum += latency;
latencyCount++;
}
});
// Log metrics periodically
setInterval(() => {
const avgLatency = latencyCount > 0 ? latencySum / latencyCount : 0;
console.log('WebSocket Metrics:', {
messagesSent,
messagesReceived,
averageLatency: avgLatency,
connectionState: client.getReadyState()
});
}, 60000); // Every minute
}
}
π Hands-On Exercises
π» Exercise 1: Build a Simple Chat Room
Create a basic chat room application:
// π» Exercise 1: Simple Chat Room
// Your task: Implement the missing methods
class SimpleChatRoom {
private client: TypeSafeWebSocketClient;
private username: string;
private messageContainer: HTMLElement;
private messageInput: HTMLInputElement;
constructor(username: string) {
this.username = username;
this.client = new TypeSafeWebSocketClient('ws://localhost:8080');
// Get DOM elements
this.messageContainer = document.getElementById('messages')!;
this.messageInput = document.getElementById('messageInput') as HTMLInputElement;
this.setupEventListeners();
}
async connect(): Promise<void> {
// TODO: Connect to WebSocket and handle connection events
// Hint: Use this.client.connect() and setup event handlers
}
private setupEventListeners(): void {
// TODO: Setup WebSocket event listeners
// Handle: connected, disconnected, message, error events
}
sendMessage(): void {
// TODO: Send a chat message
// Hint: Get text from this.messageInput, create message object, send via this.client
}
private displayMessage(message: ChatMessage): void {
// TODO: Display message in the UI
// Hint: Create div element, set innerHTML, append to this.messageContainer
}
private showStatus(status: string): void {
// TODO: Show connection status to user
console.log(status);
}
}
// π§ͺ Test your implementation:
// const chatRoom = new SimpleChatRoom('YourName');
// chatRoom.connect();
π» Exercise 2: Real-Time Collaborative Editor
Create a collaborative text editor with conflict resolution:
// π» Exercise 2: Collaborative Editor
// Your task: Implement operational transforms for conflict-free editing
interface EditOperation {
type: 'insert' | 'delete';
position: number;
content?: string;
length?: number;
userId: string;
timestamp: number;
}
class CollaborativeEditor {
private client: TypeSafeWebSocketClient;
private textArea: HTMLTextAreaElement;
private operations: EditOperation[] = [];
private userId: string;
constructor(userId: string) {
this.userId = userId;
this.client = new TypeSafeWebSocketClient('ws://localhost:8080');
this.textArea = document.getElementById('editor') as HTMLTextAreaElement;
this.setupEventListeners();
}
private setupEventListeners(): void {
// TODO: Setup text area change listeners
// TODO: Setup WebSocket message handlers
// Hint: Listen for 'input' events on textArea
// Hint: Handle incoming edit operations from other users
}
private handleTextChange(event: Event): void {
// TODO: Detect what changed and create edit operation
// Hint: Compare current text with previous state
// Hint: Send operation to other users via WebSocket
}
private applyOperation(operation: EditOperation): void {
// TODO: Apply incoming operation to local text
// Hint: Transform operation based on local operations
// Hint: Update textArea.value
}
private transformOperation(
operation: EditOperation,
localOperations: EditOperation[]
): EditOperation {
// TODO: Implement operational transform
// This is the tricky part - handle concurrent edits!
// Hint: Adjust position based on previous operations
return operation;
}
}
π» Exercise 3: Real-Time Dashboard
Build a live data dashboard with WebSocket updates:
// π» Exercise 3: Real-Time Dashboard
// Your task: Create a dashboard that updates with live data
interface MetricUpdate {
type: 'metric_update';
metricName: string;
value: number;
timestamp: number;
}
interface ChartDataPoint {
timestamp: number;
value: number;
}
class RealTimeDashboard {
private client: TypeSafeWebSocketClient;
private metrics = new Map<string, ChartDataPoint[]>();
private charts = new Map<string, any>(); // Chart.js instances
constructor() {
this.client = new TypeSafeWebSocketClient('ws://localhost:8080');
this.setupWebSocket();
}
private setupWebSocket(): void {
// TODO: Setup WebSocket connection and message handling
// Hint: Handle 'metric_update' messages
}
private handleMetricUpdate(update: MetricUpdate): void {
// TODO: Update metric data and refresh chart
// Hint: Add new data point to this.metrics
// Hint: Keep only last 50 data points for performance
// Hint: Update corresponding chart
}
private createChart(metricName: string, canvasId: string): void {
// TODO: Create Chart.js chart for the metric
// Hint: Use Chart.js library to create line chart
}
private updateChart(metricName: string): void {
// TODO: Update chart with new data
// Hint: Get chart from this.charts, update data, call chart.update()
}
// Bonus: Add data filtering by time range
filterDataByTimeRange(hours: number): void {
// TODO: Filter metrics to show only last N hours
}
}
π― Final Project: Complete Real-Time Application
Combine everything youβve learned to build a comprehensive real-time application:
// π Final Project: Multi-Feature Real-Time App
// Combine chat, notifications, live updates, and collaboration!
class RealTimeApp {
private chatClient: ChatApplication;
private dashboardClient: RealTimeDashboard;
private notificationClient: TypeSafeWebSocketClient;
private collaborativeEditor: CollaborativeEditor;
constructor(private userId: string, private username: string) {
this.initializeClients();
this.setupGlobalEventHandlers();
}
private initializeClients(): void {
// TODO: Initialize all WebSocket clients
// TODO: Setup cross-client communication
// TODO: Handle global connection state
}
private setupGlobalEventHandlers(): void {
// TODO: Handle app-wide events
// TODO: Coordinate between different features
// TODO: Implement global error handling
}
// TODO: Add methods for:
// - User presence tracking
// - Cross-feature notifications
// - Data synchronization
// - Offline support with queued operations
// - Performance monitoring
}
π Conclusion
Congratulations! π Youβve mastered the art of real-time communication with WebSockets and TypeScript! Youβve learned how to:
- π Create robust WebSocket connections with automatic reconnection
- π¬ Build complete chat applications with typing indicators
- ποΈ Implement type-safe message passing systems
- π‘οΈ Add authentication and security measures
- β‘ Optimize performance with batching and throttling
- π§ͺ Test WebSocket applications thoroughly
- π― Avoid common pitfalls and follow best practices
Youβre now equipped to build any real-time feature your applications need - from simple notifications to complex collaborative tools. WebSockets are your gateway to creating engaging, interactive user experiences that work seamlessly across all network conditions.
Keep practicing, keep building, and remember: the web is real-time, and now you have the power to make it happen! πβ¨
Next Steps: Try building a real-time game, a collaborative whiteboard, or a live streaming dashboard. The possibilities are endless with your new WebSocket superpowers! π