37 KiB
description, shortcut
| description | shortcut |
|---|---|
| Build WebSocket servers for real-time bidirectional communication | ws |
Build WebSocket Server
Automatically generate production-ready WebSocket servers with Socket.IO or native WebSocket implementations featuring room management, authentication, broadcasting, presence tracking, and resilient connection handling for real-time applications.
When to Use This Command
Use /build-websocket-server when you need to:
- Build real-time chat applications or messaging systems
- Implement live collaboration features (Google Docs-style)
- Create real-time dashboards with live data updates
- Build multiplayer game servers
- Stream live data feeds (stock prices, sports scores)
- Implement real-time notifications and alerts
DON'T use this when:
- Simple request-response patterns suffice (use REST)
- Unidirectional server-to-client updates only (consider SSE)
- Very high throughput binary streaming (consider WebRTC)
- Message ordering is critical (consider message queues)
Design Decisions
This command implements Socket.IO as the primary approach because:
- Automatic fallback to long-polling for compatibility
- Built-in room and namespace management
- Automatic reconnection with exponential backoff
- Binary data support with automatic serialization
- Event acknowledgments and timeouts
- Extensive middleware ecosystem
Alternative considered: Native WebSocket (ws)
- Lower overhead and better performance
- No automatic fallbacks
- Manual implementation of features
- Recommended for simple, high-performance needs
Alternative considered: Server-Sent Events (SSE)
- Simpler for unidirectional communication
- Works over HTTP/2
- No bidirectional support
- Recommended for news feeds, notifications
Prerequisites
Before running this command:
- Choose Socket.IO vs native WebSocket
- Design event/message protocol
- Plan authentication strategy
- Define room/channel structure
- Determine scaling approach (Redis adapter for multi-server)
Implementation Process
Step 1: Initialize WebSocket Server
Set up Socket.IO or ws server with proper configuration.
Step 2: Implement Authentication
Add middleware for connection authentication and authorization.
Step 3: Define Event Handlers
Create handlers for all client events and server broadcasts.
Step 4: Add Room Management
Implement room joining, leaving, and broadcasting logic.
Step 5: Configure Resilience
Set up reconnection, heartbeat, and error handling.
Output Format
The command generates:
websocket/server.js- Main WebSocket server setupwebsocket/handlers/- Event handler moduleswebsocket/middleware/- Auth and validation middlewarewebsocket/rooms/- Room management logicwebsocket/presence/- User presence trackingwebsocket/client.js- Client SDK/libraryconfig/websocket.json- Server configurationtests/websocket/- Integration tests
Code Examples
Example 1: Full-Featured Chat Server with Socket.IO
// websocket/server.js - Socket.IO server with all features
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { instrument } = require('@socket.io/admin-ui');
const jwt = require('jsonwebtoken');
const Redis = require('ioredis');
const crypto = require('crypto');
class WebSocketServer {
constructor(httpServer, config = {}) {
this.config = {
cors: {
origin: config.corsOrigin || ['http://localhost:3000'],
credentials: true
},
pingTimeout: config.pingTimeout || 60000,
pingInterval: config.pingInterval || 25000,
transports: config.transports || ['websocket', 'polling'],
maxHttpBufferSize: config.maxHttpBufferSize || 1e6, // 1MB
...config
};
this.io = new Server(httpServer, this.config);
this.redis = new Redis(config.redisUrl);
this.pubClient = this.redis;
this.subClient = this.redis.duplicate();
this.rooms = new Map();
this.users = new Map();
this.presence = new Map();
this.setupRedisAdapter();
this.setupMiddleware();
this.setupEventHandlers();
this.setupAdminUI();
this.startPresenceTracking();
}
setupRedisAdapter() {
// Enable horizontal scaling with Redis
this.io.adapter(createAdapter(this.pubClient, this.subClient));
}
setupMiddleware() {
// Authentication middleware
this.io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication required'));
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await this.validateUser(decoded.userId);
if (!user) {
return next(new Error('Invalid user'));
}
// Attach user data to socket
socket.userId = user.id;
socket.user = user;
socket.sessionId = crypto.randomBytes(8).toString('hex');
// Rate limiting per user
const rateLimitKey = `ratelimit:${user.id}`;
const requests = await this.redis.incr(rateLimitKey);
if (requests === 1) {
await this.redis.expire(rateLimitKey, 60); // 1 minute window
}
if (requests > 100) { // 100 messages per minute
return next(new Error('Rate limit exceeded'));
}
next();
} catch (error) {
next(new Error('Authentication failed: ' + error.message));
}
});
// Connection logging middleware
this.io.use((socket, next) => {
console.log(`New connection from ${socket.handshake.address}`);
socket.on('error', (error) => {
console.error(`Socket error for ${socket.userId}:`, error);
});
next();
});
}
setupEventHandlers() {
this.io.on('connection', (socket) => {
this.handleConnection(socket);
// Room management
socket.on('join-room', (roomId, callback) =>
this.handleJoinRoom(socket, roomId, callback)
);
socket.on('leave-room', (roomId, callback) =>
this.handleLeaveRoom(socket, roomId, callback)
);
// Messaging
socket.on('message', (data, callback) =>
this.handleMessage(socket, data, callback)
);
socket.on('typing', (data) =>
this.handleTyping(socket, data)
);
socket.on('stop-typing', (data) =>
this.handleStopTyping(socket, data)
);
// Presence
socket.on('update-status', (status) =>
this.handleStatusUpdate(socket, status)
);
// Private messaging
socket.on('private-message', (data, callback) =>
this.handlePrivateMessage(socket, data, callback)
);
// File sharing
socket.on('upload-start', (data, callback) =>
this.handleUploadStart(socket, data, callback)
);
socket.on('upload-chunk', (data, callback) =>
this.handleUploadChunk(socket, data, callback)
);
// Voice/Video calls
socket.on('call-user', (data, callback) =>
this.handleCallUser(socket, data, callback)
);
socket.on('call-answer', (data) =>
this.handleCallAnswer(socket, data)
);
socket.on('ice-candidate', (data) =>
this.handleIceCandidate(socket, data)
);
// Disconnection
socket.on('disconnect', (reason) =>
this.handleDisconnection(socket, reason)
);
});
}
async handleConnection(socket) {
console.log(`User ${socket.userId} connected (${socket.sessionId})`);
// Track user connection
if (!this.users.has(socket.userId)) {
this.users.set(socket.userId, new Set());
}
this.users.get(socket.userId).add(socket.id);
// Update presence
await this.updatePresence(socket.userId, 'online');
// Send connection success with user data
socket.emit('connected', {
sessionId: socket.sessionId,
userId: socket.userId,
serverTime: Date.now()
});
// Rejoin previous rooms from session
const previousRooms = await this.getUserRooms(socket.userId);
for (const roomId of previousRooms) {
socket.join(roomId);
socket.to(roomId).emit('user-joined', {
userId: socket.userId,
user: socket.user,
roomId
});
}
// Send pending messages
const pendingMessages = await this.getPendingMessages(socket.userId);
if (pendingMessages.length > 0) {
socket.emit('pending-messages', pendingMessages);
}
}
async handleJoinRoom(socket, roomId, callback) {
try {
// Validate room access
const hasAccess = await this.validateRoomAccess(socket.userId, roomId);
if (!hasAccess) {
return callback({ error: 'Access denied' });
}
// Join the room
socket.join(roomId);
// Track room membership
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, new Set());
}
this.rooms.get(roomId).add(socket.userId);
// Get room info
const roomInfo = await this.getRoomInfo(roomId);
const members = await this.getRoomMembers(roomId);
// Notify others in room
socket.to(roomId).emit('user-joined', {
userId: socket.userId,
user: socket.user,
roomId
});
// Send room state to joiner
callback({
success: true,
room: roomInfo,
members: members,
recentMessages: await this.getRecentMessages(roomId)
});
// Update user's room list
await this.addUserRoom(socket.userId, roomId);
} catch (error) {
console.error('Join room error:', error);
callback({ error: error.message });
}
}
async handleMessage(socket, data, callback) {
try {
// Validate message
if (!data.roomId || !data.content) {
return callback({ error: 'Invalid message format' });
}
// Check room membership
if (!socket.rooms.has(data.roomId)) {
return callback({ error: 'Not in room' });
}
// Create message object
const message = {
id: crypto.randomBytes(16).toString('hex'),
roomId: data.roomId,
userId: socket.userId,
user: socket.user,
content: data.content,
type: data.type || 'text',
timestamp: Date.now(),
edited: false,
deleted: false
};
// Store message
await this.storeMessage(message);
// Broadcast to room
this.io.to(data.roomId).emit('new-message', message);
// Send acknowledgment
callback({
success: true,
messageId: message.id,
timestamp: message.timestamp
});
// Update room activity
await this.updateRoomActivity(data.roomId);
// Send push notifications to offline users
await this.sendPushNotifications(data.roomId, message, socket.userId);
} catch (error) {
console.error('Message error:', error);
callback({ error: error.message });
}
}
async handleTyping(socket, data) {
if (data.roomId && socket.rooms.has(data.roomId)) {
socket.to(data.roomId).emit('user-typing', {
userId: socket.userId,
user: socket.user,
roomId: data.roomId
});
// Auto-stop typing after 3 seconds
setTimeout(() => {
socket.to(data.roomId).emit('user-stopped-typing', {
userId: socket.userId,
roomId: data.roomId
});
}, 3000);
}
}
async handlePrivateMessage(socket, data, callback) {
try {
const targetUserId = data.targetUserId;
const targetSockets = this.users.get(targetUserId);
const message = {
id: crypto.randomBytes(16).toString('hex'),
fromUserId: socket.userId,
fromUser: socket.user,
toUserId: targetUserId,
content: data.content,
timestamp: Date.now(),
read: false
};
// Store private message
await this.storePrivateMessage(message);
if (targetSockets && targetSockets.size > 0) {
// User is online, deliver to all their connections
for (const socketId of targetSockets) {
this.io.to(socketId).emit('private-message', message);
}
message.delivered = true;
} else {
// User is offline, queue for later
await this.queueMessage(targetUserId, message);
message.delivered = false;
}
callback({
success: true,
messageId: message.id,
delivered: message.delivered
});
} catch (error) {
console.error('Private message error:', error);
callback({ error: error.message });
}
}
async handleDisconnection(socket, reason) {
console.log(`User ${socket.userId} disconnected: ${reason}`);
// Remove socket from user's connections
const userSockets = this.users.get(socket.userId);
if (userSockets) {
userSockets.delete(socket.id);
// If no more connections, mark as offline
if (userSockets.size === 0) {
this.users.delete(socket.userId);
await this.updatePresence(socket.userId, 'offline');
// Notify rooms user was in
for (const roomId of socket.rooms) {
if (roomId !== socket.id) { // Skip default room
socket.to(roomId).emit('user-left', {
userId: socket.userId,
roomId
});
}
}
}
}
// Clean up room memberships
for (const [roomId, members] of this.rooms.entries()) {
members.delete(socket.userId);
if (members.size === 0) {
this.rooms.delete(roomId);
}
}
}
// Presence tracking
startPresenceTracking() {
setInterval(async () => {
for (const [userId, status] of this.presence.entries()) {
// Check if user has active connections
if (!this.users.has(userId)) {
this.presence.set(userId, 'offline');
await this.broadcastPresenceUpdate(userId, 'offline');
}
}
}, 30000); // Check every 30 seconds
}
async updatePresence(userId, status) {
this.presence.set(userId, status);
await this.redis.setex(`presence:${userId}`, 120, status);
await this.broadcastPresenceUpdate(userId, status);
}
async broadcastPresenceUpdate(userId, status) {
// Get all rooms user is in
const userRooms = await this.getUserRooms(userId);
for (const roomId of userRooms) {
this.io.to(roomId).emit('presence-update', {
userId,
status,
timestamp: Date.now()
});
}
}
// Helper methods
async validateUser(userId) {
// Implement user validation
return { id: userId, name: `User ${userId}` };
}
async validateRoomAccess(userId, roomId) {
// Implement room access validation
return true;
}
async getRoomInfo(roomId) {
// Implement room info retrieval
return { id: roomId, name: `Room ${roomId}` };
}
async getRoomMembers(roomId) {
// Implement member list retrieval
return Array.from(this.rooms.get(roomId) || []);
}
async getRecentMessages(roomId, limit = 50) {
// Implement message history retrieval
return [];
}
async storeMessage(message) {
// Implement message storage
await this.redis.lpush(
`messages:${message.roomId}`,
JSON.stringify(message)
);
await this.redis.ltrim(`messages:${message.roomId}`, 0, 999);
}
async getUserRooms(userId) {
// Implement user room list retrieval
return [];
}
async getPendingMessages(userId) {
// Implement pending message retrieval
return [];
}
setupAdminUI() {
// Enable admin UI for monitoring
instrument(this.io, {
auth: {
type: 'basic',
username: process.env.SOCKETIO_ADMIN_USER || 'admin',
password: process.env.SOCKETIO_ADMIN_PASSWORD || 'admin'
},
readonly: false
});
}
// Public methods for external use
async broadcast(event, data) {
this.io.emit(event, data);
}
async broadcastToRoom(roomId, event, data) {
this.io.to(roomId).emit(event, data);
}
async sendToUser(userId, event, data) {
const userSockets = this.users.get(userId);
if (userSockets) {
for (const socketId of userSockets) {
this.io.to(socketId).emit(event, data);
}
}
}
getOnlineUsers() {
return Array.from(this.users.keys());
}
getRoomMembers(roomId) {
return Array.from(this.rooms.get(roomId) || []);
}
}
// Initialize server
const http = require('http');
const express = require('express');
const app = express();
const server = http.createServer(app);
const wsServer = new WebSocketServer(server, {
corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'],
redisUrl: process.env.REDIS_URL || 'redis://localhost:6379'
});
// REST endpoints for WebSocket management
app.get('/ws/stats', (req, res) => {
res.json({
onlineUsers: wsServer.getOnlineUsers().length,
rooms: wsServer.rooms.size,
connections: wsServer.io.engine.clientsCount
});
});
app.post('/ws/broadcast', express.json(), async (req, res) => {
const { event, data } = req.body;
await wsServer.broadcast(event, data);
res.json({ success: true });
});
server.listen(3000, () => {
console.log('WebSocket server running on port 3000');
});
module.exports = { WebSocketServer };
Example 2: High-Performance Native WebSocket Implementation
// websocket/native-server.js - Pure WebSocket with advanced features
const WebSocket = require('ws');
const http = require('http');
const crypto = require('crypto');
const EventEmitter = require('events');
class NativeWebSocketServer extends EventEmitter {
constructor(server, options = {}) {
super();
this.wss = new WebSocket.Server({
server,
perMessageDeflate: options.compression !== false,
maxPayload: options.maxPayload || 10 * 1024 * 1024, // 10MB
clientTracking: true,
...options
});
this.clients = new Map();
this.rooms = new Map();
this.messageHandlers = new Map();
this.heartbeatInterval = options.heartbeatInterval || 30000;
this.setupEventHandlers();
this.startHeartbeat();
}
setupEventHandlers() {
this.wss.on('connection', (ws, req) => {
this.handleConnection(ws, req);
});
this.wss.on('error', (error) => {
console.error('WebSocket server error:', error);
this.emit('error', error);
});
}
handleConnection(ws, req) {
// Generate client ID
const clientId = crypto.randomBytes(16).toString('hex');
// Parse authentication from URL or headers
const token = this.extractToken(req);
const user = this.authenticateToken(token);
if (!user) {
ws.close(1008, 'Unauthorized');
return;
}
// Setup client
const client = {
id: clientId,
ws: ws,
user: user,
rooms: new Set(),
isAlive: true,
joinedAt: Date.now(),
lastActivity: Date.now(),
messageCount: 0
};
this.clients.set(clientId, client);
// Send connection acknowledgment
this.send(client, {
type: 'connected',
clientId: clientId,
timestamp: Date.now()
});
// Setup client event handlers
ws.on('message', (data) => this.handleMessage(client, data));
ws.on('pong', () => this.handlePong(client));
ws.on('close', (code, reason) => this.handleClose(client, code, reason));
ws.on('error', (error) => this.handleError(client, error));
this.emit('connection', client);
}
handleMessage(client, data) {
try {
// Update activity
client.lastActivity = Date.now();
client.messageCount++;
// Parse message
let message;
if (typeof data === 'string') {
message = JSON.parse(data);
} else {
// Handle binary data
message = this.parseBinaryMessage(data);
}
// Rate limiting
if (client.messageCount > 100) {
const timeDiff = Date.now() - client.joinedAt;
if (timeDiff < 60000) { // Less than 1 minute
this.send(client, {
type: 'error',
error: 'Rate limit exceeded'
});
client.ws.close(1008, 'Rate limit exceeded');
return;
}
client.messageCount = 0;
client.joinedAt = Date.now();
}
// Route message to handler
const handler = this.messageHandlers.get(message.type);
if (handler) {
handler(client, message);
} else {
this.handleDefaultMessage(client, message);
}
this.emit('message', client, message);
} catch (error) {
console.error('Message handling error:', error);
this.send(client, {
type: 'error',
error: 'Invalid message format'
});
}
}
handleDefaultMessage(client, message) {
switch (message.type) {
case 'join':
this.joinRoom(client, message.room);
break;
case 'leave':
this.leaveRoom(client, message.room);
break;
case 'broadcast':
this.broadcastToRoom(message.room, message.data, client);
break;
case 'ping':
this.send(client, { type: 'pong', timestamp: Date.now() });
break;
default:
this.emit('custom-message', client, message);
}
}
joinRoom(client, roomId) {
if (!roomId) return;
// Add client to room
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, new Set());
}
this.rooms.get(roomId).add(client.id);
client.rooms.add(roomId);
// Notify room members
this.broadcastToRoom(roomId, {
type: 'user-joined',
userId: client.user.id,
roomId: roomId,
timestamp: Date.now()
}, client);
// Send confirmation
this.send(client, {
type: 'joined',
roomId: roomId,
members: this.getRoomMembers(roomId)
});
this.emit('room-joined', client, roomId);
}
leaveRoom(client, roomId) {
if (!roomId || !client.rooms.has(roomId)) return;
// Remove client from room
const room = this.rooms.get(roomId);
if (room) {
room.delete(client.id);
if (room.size === 0) {
this.rooms.delete(roomId);
}
}
client.rooms.delete(roomId);
// Notify room members
this.broadcastToRoom(roomId, {
type: 'user-left',
userId: client.user.id,
roomId: roomId,
timestamp: Date.now()
});
// Send confirmation
this.send(client, {
type: 'left',
roomId: roomId
});
this.emit('room-left', client, roomId);
}
broadcastToRoom(roomId, data, excludeClient = null) {
const room = this.rooms.get(roomId);
if (!room) return;
const message = typeof data === 'object' ? JSON.stringify(data) : data;
for (const clientId of room) {
if (excludeClient && clientId === excludeClient.id) continue;
const client = this.clients.get(clientId);
if (client && client.ws.readyState === WebSocket.OPEN) {
client.ws.send(message);
}
}
}
broadcast(data, excludeClient = null) {
const message = typeof data === 'object' ? JSON.stringify(data) : data;
this.clients.forEach((client) => {
if (excludeClient && client.id === excludeClient.id) return;
if (client.ws.readyState === WebSocket.OPEN) {
client.ws.send(message);
}
});
}
send(client, data) {
if (client.ws.readyState === WebSocket.OPEN) {
const message = typeof data === 'object' ? JSON.stringify(data) : data;
client.ws.send(message);
}
}
handlePong(client) {
client.isAlive = true;
}
handleClose(client, code, reason) {
console.log(`Client ${client.id} disconnected: ${code} - ${reason}`);
// Leave all rooms
for (const roomId of client.rooms) {
this.leaveRoom(client, roomId);
}
// Remove client
this.clients.delete(client.id);
this.emit('disconnection', client, code, reason);
}
handleError(client, error) {
console.error(`Client ${client.id} error:`, error);
this.emit('client-error', client, error);
}
startHeartbeat() {
setInterval(() => {
this.clients.forEach((client) => {
if (!client.isAlive) {
console.log(`Terminating inactive client ${client.id}`);
client.ws.terminate();
this.clients.delete(client.id);
return;
}
client.isAlive = false;
client.ws.ping();
});
}, this.heartbeatInterval);
}
// Utility methods
extractToken(req) {
// Extract from query string or authorization header
const url = new URL(req.url, `http://${req.headers.host}`);
return url.searchParams.get('token') ||
req.headers.authorization?.replace('Bearer ', '');
}
authenticateToken(token) {
// Implement token validation
if (!token) return null;
return { id: 'user123', name: 'Test User' };
}
getRoomMembers(roomId) {
const room = this.rooms.get(roomId);
if (!room) return [];
return Array.from(room).map(clientId => {
const client = this.clients.get(clientId);
return client ? client.user : null;
}).filter(Boolean);
}
// Public API
registerHandler(type, handler) {
this.messageHandlers.set(type, handler);
}
getClient(clientId) {
return this.clients.get(clientId);
}
getClients() {
return Array.from(this.clients.values());
}
getRooms() {
return Array.from(this.rooms.keys());
}
close() {
clearInterval(this.heartbeatTimer);
this.wss.close();
}
}
// Usage
const server = http.createServer();
const wsServer = new NativeWebSocketServer(server, {
compression: true,
heartbeatInterval: 30000
});
// Register custom message handlers
wsServer.registerHandler('chat', (client, message) => {
wsServer.broadcastToRoom(message.room, {
type: 'chat',
from: client.user.name,
content: message.content,
timestamp: Date.now()
});
});
wsServer.registerHandler('file-upload', (client, message) => {
// Handle file upload
console.log(`File upload from ${client.user.name}:`, message.filename);
});
// Event listeners
wsServer.on('connection', (client) => {
console.log(`New connection: ${client.user.name}`);
});
wsServer.on('room-joined', (client, roomId) => {
console.log(`${client.user.name} joined room ${roomId}`);
});
server.listen(3000, () => {
console.log('Native WebSocket server running on port 3000');
});
Example 3: Client SDK and Testing
// websocket/client.js - Browser/Node.js client
class WebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.ws = null;
this.messageHandlers = new Map();
this.messageQueue = [];
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
this.reconnectDelay = options.reconnectDelay || 1000;
this.isConnected = false;
}
connect() {
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.isConnected = true;
this.reconnectAttempts = 0;
// Send queued messages
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
this.send(message);
}
resolve();
};
this.ws.onmessage = (event) => {
this.handleMessage(event.data);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
reject(error);
};
this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
this.isConnected = false;
this.handleReconnect();
};
} catch (error) {
reject(error);
}
});
}
handleMessage(data) {
try {
const message = JSON.parse(data);
const handler = this.messageHandlers.get(message.type);
if (handler) {
handler(message);
} else {
console.log('Unhandled message:', message);
}
} catch (error) {
console.error('Message parsing error:', error);
}
}
on(type, handler) {
this.messageHandlers.set(type, handler);
}
send(data) {
const message = typeof data === 'object' ? JSON.stringify(data) : data;
if (this.isConnected && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(message);
} else {
// Queue message for later
this.messageQueue.push(data);
}
}
handleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => {
this.connect().catch(console.error);
}, delay);
}
close() {
if (this.ws) {
this.ws.close();
}
}
}
// tests/websocket.test.js
const { WebSocketServer } = require('../websocket/server');
const WebSocketClient = require('../websocket/client');
describe('WebSocket Server Tests', () => {
let server;
let client;
beforeEach(async () => {
server = new WebSocketServer(3001);
await server.start();
client = new WebSocketClient('ws://localhost:3001', {
token: 'test-token'
});
});
afterEach(async () => {
client.close();
await server.close();
});
test('should connect successfully', async () => {
await client.connect();
expect(client.isConnected).toBe(true);
});
test('should join room', async () => {
await client.connect();
const joined = new Promise((resolve) => {
client.on('joined', resolve);
});
client.send({ type: 'join', room: 'test-room' });
const result = await joined;
expect(result.roomId).toBe('test-room');
});
test('should broadcast messages', async () => {
const client2 = new WebSocketClient('ws://localhost:3001');
await client.connect();
await client2.connect();
// Both join same room
client.send({ type: 'join', room: 'test-room' });
client2.send({ type: 'join', room: 'test-room' });
// Set up message listener
const messageReceived = new Promise((resolve) => {
client2.on('chat', resolve);
});
// Send message
client.send({
type: 'chat',
room: 'test-room',
content: 'Hello World'
});
const message = await messageReceived;
expect(message.content).toBe('Hello World');
});
test('should handle reconnection', async () => {
await client.connect();
// Force disconnect
server.disconnectClient(client.id);
// Wait for reconnection
await new Promise(resolve => setTimeout(resolve, 2000));
expect(client.isConnected).toBe(true);
expect(client.reconnectAttempts).toBeGreaterThan(0);
});
});
Error Handling
| Error | Cause | Solution |
|---|---|---|
| "WebSocket connection failed" | Network issues or wrong URL | Check network and WebSocket URL |
| "Authentication required" | Missing or invalid token | Include valid auth token |
| "Rate limit exceeded" | Too many messages | Implement client-side throttling |
| "Maximum payload size exceeded" | Message too large | Split large messages or use chunking |
| "Connection timeout" | No heartbeat response | Check network stability |
Configuration Options
Server Options
perMessageDeflate: Enable compressionmaxPayload: Maximum message sizepingInterval: Heartbeat frequencypingTimeout: Heartbeat timeouttransports: Allowed transports (Socket.IO)
Client Options
reconnection: Enable auto-reconnectionreconnectionAttempts: Max reconnect triesreconnectionDelay: Initial reconnect delaytimeout: Connection timeoutauth: Authentication data
Best Practices
DO:
- Implement heartbeat/ping-pong for connection health
- Use rooms/namespaces for logical grouping
- Add authentication before accepting connections
- Implement message acknowledgments for critical data
- Use compression for text data
- Monitor connection count and memory usage
DON'T:
- Send large payloads without chunking
- Store state only in memory without persistence
- Skip authentication for production
- Ignore connection limits
- Use synchronous operations in handlers
- Broadcast sensitive data to all clients
Performance Considerations
- Use binary frames for large data transfers
- Implement message batching for high-frequency updates
- Use Redis adapter for horizontal scaling
- Enable compression for text-heavy payloads
- Limit connection count per IP/user
- Implement connection pooling for database queries
Security Considerations
- Always use WSS (WebSocket Secure) in production
- Validate all incoming messages
- Implement rate limiting per connection
- Use JWT or session-based authentication
- Sanitize user input before broadcasting
- Implement CORS properly
- Monitor for abnormal connection patterns
Related Commands
/webhook-handler-creator- Handle webhooks/real-time-sync- Implement data synchronization/message-queue-setup- Configure message queues/pubsub-system- Build pub/sub architecture/notification-service- Push notifications
Version History
- v1.0.0 (2024-10): Initial implementation with Socket.IO and native WebSocket
- Planned v1.1.0: Add WebRTC signaling server and video streaming support