WebSocket Guide
The PXL Framework provides comprehensive WebSocket support for building real-time applications with features like room-based messaging, authentication, and multi-worker coordination.
Table of Contents
- Overview
- Quick Start
- Architecture
- Configuration
- Server-Side Implementation
- Client-Side Implementation
- Room Management
- Authentication
- Multi-Worker Coordination
- Broadcasting Utilities
- Subscriber Utilities
- Subscriber Middleware
- Advanced Features
- Best Practices
- Troubleshooting
Overview
The WebSocket module provides:
- Bidirectional Communication: Real-time messaging between server and clients
- Room-Based Messaging: Group clients into rooms for targeted broadcasts
- Controller-Based Routing: Organize WebSocket handlers using controllers
- Authentication: JWT-based WebSocket authentication
- Multi-Worker Support: Redis-based pub/sub for scaling across workers
- Client Management: Track connected clients with metadata
- Automatic Cleanup: Graceful shutdown and inactive client management
Quick Start
Basic WebSocket Server
import { WebApplication } from '@scpxl/nodejs-framework';
const app = new WebApplication({
name: 'my-app',
webserver: { port: 3000 },
websocket: {
enabled: true,
type: 'server',
url: 'ws://localhost:3000/ws',
},
redis: { host: '127.0.0.1', port: 6379 },
});
await app.start();
console.log('WebSocket server running at ws://localhost:3000/ws');Basic WebSocket Client
import { WebSocket } from 'ws';
const ws = new WebSocket('ws://localhost:3000/ws');
ws.on('open', () => {
console.log('Connected to WebSocket server');
ws.send(
JSON.stringify({
type: 'chat',
action: 'message',
data: { text: 'Hello!' },
}),
);
});
ws.on('message', data => {
const message = JSON.parse(data.toString());
console.log('Received:', message);
});Architecture
Components
┌─────────────────────────────────────────────────────┐
│ WebSocket Server │
│ ┌───────────────────────────────────────────────┐ │
│ │ Client Manager │ │
│ │ - Track connected clients │ │
│ │ - Manage client metadata │ │
│ │ - Handle authentication │ │
│ └───────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Room Manager │ │
│ │ - Create/delete rooms │ │
│ │ - Add/remove clients from rooms │ │
│ │ - Broadcast to room members │ │
│ └───────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Controller-Based Routing │ │
│ │ - Route messages to controllers │ │
│ │ - type + action → controller method │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│
├─── Redis Pub/Sub
│ (Multi-worker coordination)
│
┌────────┴────────┐
│ │
Worker 1 Worker 2Message Flow
- Client connects → Server assigns client ID
- Client sends message → Server routes to controller based on
typeandaction - Controller processes → Returns response or broadcasts to other clients
- Cross-worker messages → Published via Redis, received by all workers
Configuration
WebSocket Server Configuration
const config = {
webSocket: {
enabled: true,
type: 'server', // 'server' or 'client'
host: '0.0.0.0',
url: 'ws://localhost:3000/ws',
// Controllers and routing
controllersDirectory: './src/websocket/controllers',
routes: [
{
type: 'chat', // Message type
action: 'send', // Message action
controllerName: 'chat', // Controller file name
controller: ChatController, // Optional: direct class reference
},
],
// Debug options
debug: {
printRoutes: true, // Log registered routes on startup
},
// Room configuration
rooms: {
clientCanJoinMultipleRooms: true, // Allow clients in multiple rooms
},
// Inactive client management
disconnectInactiveClients: {
enabled: true,
inactiveTime: 300000, // 5 minutes in milliseconds
intervalCheckTime: 60000, // Check every minute
log: false, // Log disconnections
},
// Event handlers
events: {
onServerStarted: ({ webSocketServer }) => {
console.log('WebSocket server started');
},
onConnected: ({ ws, clientId }) => {
console.log(`Client ${clientId} connected`);
},
onDisconnected: ({ clientId }) => {
console.log(`Client ${clientId} disconnected`);
},
onMessage: ({ ws, clientId, data }) => {
console.log(`Message from ${clientId}:`, data);
},
onError: ({ error }) => {
console.error('WebSocket error:', error);
},
},
subscriberHandlers: {
directory: path.join(baseDir, 'websocket', 'subscribers'),
},
},
};Server-Side Implementation
Creating a WebSocket Controller
Controllers organize your WebSocket message handlers:
// src/websocket/controllers/chat.controller.ts
import { WebSocketServerBaseController } from '@scpxl/nodejs-framework/websocket';
import type { WebSocket } from 'ws';
export default class ChatController extends WebSocketServerBaseController {
/**
* Handle chat messages
* Route: type: 'chat', action: 'send'
*/
public send = (ws: WebSocket, clientId: string, data: any) => {
const message = data?.text || '';
const username = data?.username || 'Anonymous';
// Broadcast to all clients
this.webSocketServer.sendMessageToAll({
data: {
type: 'chat',
action: 'message',
data: {
username,
message,
timestamp: new Date().toISOString(),
},
},
});
// Return acknowledgment to sender
return {
success: true,
messageId: Date.now(),
};
};
/**
* Handle typing indicators
* Route: type: 'chat', action: 'typing'
*/
public typing = (ws: WebSocket, clientId: string, data: any) => {
const username = data?.username || 'Anonymous';
this.webSocketServer.broadcastToAllClients({
data: {
type: 'chat',
action: 'userTyping',
data: { username },
},
excludeClientId: clientId, // Don't send back to sender
});
return { success: true };
};
}Registering Routes
import ChatController from './controllers/chat.controller.js';
const config = {
webSocket: {
routes: [
{
type: 'chat',
action: 'send',
controllerName: 'chat',
controller: ChatController,
},
{
type: 'chat',
action: 'typing',
controllerName: 'chat',
controller: ChatController,
},
],
},
};Sending Messages
// Broadcast to all clients
app.websocket.server.sendMessageToAll({
data: {
type: 'notification',
action: 'alert',
data: { message: 'Server maintenance in 5 minutes' },
},
});
// Send to specific client
const client = app.websocket.server.clientManager.getClient({
clientId: 'some-client-id',
});
if (client?.ws) {
app.websocket.server.sendClientMessage(client.ws, {
type: 'private',
action: 'message',
data: { text: 'Hello!' },
});
}Using WebSocket Service
The WebSocketService provides a higher-level API:
import { WebSocketService } from '@scpxl/nodejs-framework/websocket';
// In your application
const wsService = new WebSocketService({
webSocketServer: app.websocket.server,
redisInstance: app.redis.instance,
workerId: String(process.pid),
});
// Broadcast to all clients
await wsService.broadcast({
type: 'notification',
action: 'update',
data: { message: 'New features available!' },
});
// Send to specific rooms
await wsService.sendToRooms(['room1', 'room2'], {
type: 'chat',
action: 'message',
data: { text: 'Hello room members!' },
});
// Convenience methods
await wsService.sendUserMessage('profileUpdated', { userId: 123 });
await wsService.sendSystemMessage('maintenance', { minutes: 5 });
await wsService.sendErrorMessage('authFailed', new Error('Invalid token'));Client-Side Implementation
Browser WebSocket Client
// Frontend (Browser)
const ws = new WebSocket('ws://localhost:3000/ws');
ws.onopen = () => {
console.log('Connected');
// Send a message
ws.send(
JSON.stringify({
type: 'chat',
action: 'send',
data: {
username: 'Alice',
text: 'Hello everyone!',
},
}),
);
};
ws.onmessage = event => {
const message = JSON.parse(event.data);
console.log('Received:', message);
// Handle different message types
switch (message.type) {
case 'chat':
if (message.action === 'message') {
displayChatMessage(message.data);
} else if (message.action === 'userTyping') {
showTypingIndicator(message.data.username);
}
break;
case 'notification':
showNotification(message.data.message);
break;
}
};
ws.onerror = error => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('Disconnected');
// Implement reconnection logic
};Node.js WebSocket Client
import { WebSocketClient } from '@scpxl/nodejs-framework/websocket';
const client = new WebSocketClient({
applicationConfig: config,
options: {
url: 'ws://localhost:3000/ws',
controllersDirectory: './src/websocket/controllers/client',
events: {
onConnected: ({ ws, clientId }) => {
console.log('Connected:', clientId);
},
onMessage: ({ data }) => {
console.log('Received:', data);
},
},
},
redisInstance: app.redis.instance,
queueManager: app.queue.manager,
databaseInstance: app.database.instance,
routes: [],
});
await client.load();
await client.connectToServer();
// Send messages
client.sendClientMessage({
type: 'chat',
action: 'send',
data: { text: 'Hello from Node.js client' },
});Room Management
Joining Rooms
Rooms allow you to group clients and broadcast messages to specific groups.
// Server-side: Join a room
await app.websocket.server.joinRoom({
ws: clientWebSocket,
userId: 123,
username: 'alice',
userType: 'member',
roomName: 'general',
});
// System controller (built-in)
// Clients can join rooms by sending:
// { type: 'system', action: 'joinRoom', data: { roomName: 'general', username: 'alice' } }Leaving Rooms
// Server-side: Leave a room
app.websocket.server.leaveRoom({
ws: clientWebSocket,
roomName: 'general',
});
// System controller (built-in)
// Clients can leave rooms by sending:
// { type: 'system', action: 'leaveRoom', data: { roomName: 'general' } }Broadcasting to Rooms
// Get clients in a room
const roomClients = app.websocket.server.rooms.get('general');
if (roomClients) {
roomClients.forEach(clientId => {
const client = app.websocket.server.clientManager.getClient({ clientId });
if (client?.ws) {
app.websocket.server.sendClientMessage(client.ws, {
type: 'chat',
action: 'message',
data: { text: 'Room-specific message' },
});
}
});
}
// Using WebSocketService
const wsService = new WebSocketService({ webSocketServer: app.websocket.server });
await wsService.sendToRooms(['general', 'announcements'], {
type: 'announcement',
action: 'new',
data: { text: 'Important update!' },
});Room Configuration
const config = {
webSocket: {
rooms: {
// Allow clients to be in multiple rooms simultaneously
clientCanJoinMultipleRooms: true,
},
},
};When clientCanJoinMultipleRooms: false, joining a new room automatically removes the client from their current room.
Authentication
JWT Authentication
WebSocket connections can be authenticated using JWT tokens passed as query parameters:
// Frontend: Connect with authentication
const token = 'your-jwt-token';
const ws = new WebSocket(`ws://localhost:3000/ws?token=${token}`);Server-Side Authentication
The server automatically validates JWT tokens if provided:
const config = {
auth: {
jwtSecretKey: process.env.JWT_SECRET_KEY,
},
webSocket: {
enabled: true,
events: {
onConnected: ({ ws, clientId }) => {
// Access authenticated user info from client manager
const client = app.websocket.server.clientManager.getClient({ clientId });
const userId = client?.user?.userId;
const userPayload = client?.user?.payload;
console.log(`User ${userId} connected`);
},
},
},
};Authentication Flow
- Client requests WebSocket connection with
?token=<jwt>query parameter - Server validates JWT using configured secret key
- If valid, extracts user ID from token payload (
subclaim) - Stores user info in client metadata
- If invalid, connection is rejected with 401 Unauthorized
Accessing User Data in Controllers
export default class ChatController extends WebSocketServerBaseController {
public send = (ws: WebSocket, clientId: string, data: any) => {
// Get authenticated user info
const client = this.webSocketServer.clientManager.getClient({ clientId });
const userId = client?.user?.userId;
const userEmail = client?.user?.email;
if (!userId) {
return { error: 'Authentication required' };
}
// Process message with user context
this.webSocketServer.sendMessageToAll({
data: {
type: 'chat',
action: 'message',
data: {
userId,
userEmail,
message: data.text,
},
},
});
return { success: true };
};
}Multi-Worker Coordination
When running multiple Node.js workers (cluster mode), WebSocket connections are distributed across workers. Redis pub/sub enables cross-worker communication.
How It Works
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Worker 1 │ │ Worker 2 │ │ Worker 3 │
│ (Client A) │ │ (Client B) │ │ (Client C) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└─────────────┬───────┴─────────────────────┘
│
┌──────▼──────┐
│ Redis │
│ Pub/Sub │
└─────────────┘- Client A (on Worker 1) sends a message
- Worker 1 publishes to Redis:
WebSocketRedisSubscriberEvent.SendMessageToAll - All workers (1, 2, 3) receive the Redis message
- Each worker broadcasts to its local clients
Automatic Events
The framework automatically coordinates these events across workers:
ClientConnected- Notify all workers when a client connectsClientDisconnected- Notify all workers when a client disconnectsClientJoinedRoom- Sync room membership across workersClientLeftRoom- Sync room changes across workersSendMessageToAll- Broadcast to all clients across all workersDisconnectClient- Request to disconnect a client (on any worker)MessageError- Forward errors to specific clientsQueueJobCompleted- Notify about completed queue jobsQueueJobError- Notify about failed queue jobsCustom- Custom application events
Custom Cross-Worker Messages
// Send custom message across workers
app.websocket.server.sendCustomMessage({
data: {
type: 'custom',
action: 'userStatusChanged',
userId: 123,
status: 'online',
},
});
// src/websocket/subscribers/custom.ts
import { defineWebSocketSubscriber, WebSocketRedisSubscriberEvent } from '@scpxl/nodejs-framework/websocket';
export default defineWebSocketSubscriber({
channel: WebSocketRedisSubscriberEvent.Custom,
handle: ({ message, webSocketServer }) => {
if (message.type === 'custom' && message.action === 'userStatusChanged') {
webSocketServer.sendMessageToAll({
data: {
type: 'status',
action: 'update',
data: {
userId: message.userId,
status: message.status,
},
},
});
}
},
});Subscriber Matching and Ordering
- Pick one of
channel,channels, ormatchwhen callingdefineWebSocketSubscriberto describe how a handler should be triggered. matchaccepts a string, regular expression, predicate function, or an array mixing any of those for advanced routing.- Use
priorityto decide execution order when more than one handler matches the same payload (higher runs first). - Add
nameanddescriptionso operational logs and the debug printer call out the handler you expect.
// src/websocket/subscribers/queue-dashboard.ts
export default defineWebSocketSubscriber({
name: 'queueDashboard',
description: 'Forward queue updates to the live dashboard',
channels: [WebSocketRedisSubscriberEvent.QueueJobCompleted, WebSocketRedisSubscriberEvent.QueueJobError],
priority: 20,
handle: ({ channel, message, webSocketServer }) => {
webSocketServer.sendCustomMessage({
data: {
type: 'queue',
action: channel === WebSocketRedisSubscriberEvent.QueueJobCompleted ? 'done' : 'errored',
data: message,
},
});
},
});// src/websocket/subscribers/segment-targeting.ts
export default defineWebSocketSubscriber({
name: 'segmentTargeting',
match: [/^analytics:/, ({ message }) => message?.segment === 'vip'],
handle: ({ message, queueManager }) => {
queueManager.add('analytics', { message });
},
});Broadcasting Utilities
The framework provides convenient methods for broadcasting messages to different client groups.
Broadcast to All Clients
// Broadcast to all connected clients
app.websocket.server.broadcastToAllClients({
data: {
type: 'notification',
action: 'update',
data: { message: 'Server maintenance in 5 minutes' },
},
});
// Broadcast to all except sender
app.websocket.server.broadcastToAllClients({
data: {
/* ... */
},
excludeClientId: sendingClientId,
});
// Broadcast with custom filtering
app.websocket.server.broadcastToAllClients({
data: {
/* ... */
},
predicate: ({ clientId, userData }) => {
// Only broadcast to premium users
return userData?.subscription === 'premium';
},
});Broadcast to Room
// Broadcast to all clients in a room
app.websocket.server.broadcastToRoom({
roomName: 'general',
data: {
type: 'chat',
action: 'message',
data: { text: 'Welcome to general chat!' },
},
});
// Broadcast to room excluding sender
app.websocket.server.broadcastToRoom({
roomName: 'general',
data: {
/* ... */
},
excludeClientId: sendingClientId,
});Broadcast to Specific Users
// Broadcast to specific user IDs
app.websocket.server.broadcastToUsers({
userIds: [123, 456, 789],
data: {
type: 'notification',
action: 'personalAlert',
data: { message: 'This is for you!' },
},
});Broadcast to Single Client
// Broadcast to a specific client
app.websocket.server.broadcastToClient({
clientId: 'specific-client-id',
data: {
type: 'private',
action: 'message',
data: { text: 'Private message' },
},
});Subscriber Utilities
The framework provides composable utilities for building WebSocket subscriber handlers with common patterns like validation, error handling, rate limiting, and more.
Creating Matchers
import { matchByProperty, matchByPropertyPredicate } from '@scpxl/nodejs-framework/websocket';
// Match by exact property value
export default defineWebSocketSubscriber({
match: matchByProperty('type', 'analytics'),
handle: ({ message }) => {
/* ... */
},
});
// Match by property predicate
export default defineWebSocketSubscriber({
match: matchByPropertyPredicate('priority', value => value > 5),
handle: ({ message }) => {
/* ... */
},
});
// Combine multiple matchers
export default defineWebSocketSubscriber({
match: [/^analytics:/, matchByProperty('userId', 123)],
handle: ({ message }) => {
/* ... */
},
});Error Handling
import { withErrorHandler } from '@scpxl/nodejs-framework/websocket';
const handler = async ({ webSocketServer, message }) => {
// Your handler logic
};
export default defineWebSocketSubscriber({
channel: 'custom',
handle: withErrorHandler(handler, (error, context) => {
console.error(`Error in channel ${context.channel}:`, error.message);
// Send error notification to client
}),
});Rate Limiting
import { withRateLimit } from '@scpxl/nodejs-framework/websocket';
const handler = async ({ webSocketServer }) => {
webSocketServer.broadcastToAllClients({
data: {
/* ... */
},
});
};
// Allow max 10 executions per 1 minute
export default defineWebSocketSubscriber({
channel: 'updates',
handle: withRateLimit(
handler,
10, // max executions
60000, // 1 minute window
context => {
console.log('Rate limit exceeded for channel:', context.channel);
},
),
});Retry Logic
import { withRetry } from '@scpxl/nodejs-framework/websocket';
const handler = async ({ databaseInstance }) => {
// Attempt database operation
const result = await databaseInstance.query('SELECT ...');
return result;
};
// Retry up to 3 times with 1 second delay and exponential backoff
export default defineWebSocketSubscriber({
channel: 'database-sync',
handle: withRetry(
handler,
3, // max retries
1000, // initial delay (1 second)
2, // backoff multiplier (exponential)
),
});Validation
import { withValidation } from '@scpxl/nodejs-framework/websocket';
const handler = async ({ message, webSocketServer }) => {
webSocketServer.broadcastToAllClients({
data: message,
});
};
export default defineWebSocketSubscriber({
channel: 'messages',
handle: withValidation(message => {
// Validate message structure
if (!message?.type) throw new Error('Missing type');
if (!message?.action) throw new Error('Missing action');
if (!message?.data) throw new Error('Missing data');
}, handler),
});Conditional Execution
import { withFilter } from '@scpxl/nodejs-framework/websocket';
const handler = async ({ webSocketServer, message }) => {
// Only broadcast to authenticated users
webSocketServer.broadcastToAllClients({
data: message,
predicate: ({ userData }) => !!userData?.userId,
});
};
export default defineWebSocketSubscriber({
channel: 'protected',
handle: withFilter(context => {
// Only execute if conditions are met
return context.message?.authenticated === true;
}, handler),
});Composing Handlers
import { composeHandlers, withLogging, withErrorHandler, withRateLimit } from '@scpxl/nodejs-framework/websocket';
const validateMessage = async ({ message }) => {
if (!message?.data) throw new Error('Invalid message');
};
const broadcastMessage = async ({ webSocketServer, message }) => {
webSocketServer.broadcastToAllClients({ data: message });
};
const logMetrics = async ({ channel, message }) => {
console.log(`Processed message on ${channel}:`, message.type);
};
export default defineWebSocketSubscriber({
channel: 'analytics',
handle: composeHandlers([
withLogging(validateMessage, 'validate'),
withLogging(broadcastMessage, 'broadcast'),
withLogging(logMetrics, 'metrics'),
]),
});Subscriber Middleware
Middleware allows you to intercept and modify handler execution for cross-cutting concerns like logging, timing, and error recovery.
Using Built-in Middleware
import {
defineWebSocketSubscriber,
loggingMiddleware,
timingMiddleware,
rateLimitMiddleware,
} from '@scpxl/nodejs-framework/websocket';
export default defineWebSocketSubscriber({
channels: ['updates'],
handle: async ({ webSocketServer, message }) => {
webSocketServer.broadcastToAllClients({
data: message,
});
},
middleware: [
loggingMiddleware('updatesBroadcaster'),
timingMiddleware(),
rateLimitMiddleware(10, 60000), // 10 per minute
],
});Custom Middleware
import type { WebSocketSubscriberMiddleware } from '@scpxl/nodejs-framework/websocket';
const authenticationMiddleware: WebSocketSubscriberMiddleware = {
name: 'authentication',
onBefore: async context => {
// Check if message is authenticated
const isAuthenticated = context.message?.authenticated === true;
if (!isAuthenticated) {
console.warn('Unauthenticated message on channel:', context.channel);
return false; // Skip handler execution
}
return true; // Proceed with handler
},
};
const metricsMiddleware: WebSocketSubscriberMiddleware = {
name: 'metrics',
onAfter: async (context, result) => {
// Record metrics after successful execution
console.log('Handler executed successfully', {
channel: context.channel,
resultType: typeof result,
});
},
onError: async (context, error) => {
// Handle errors
console.error('Handler error:', error.message);
return false; // Don't suppress the error
},
};
export default defineWebSocketSubscriber({
channels: ['secure-updates'],
handle: async ({ webSocketServer, message }) => {
webSocketServer.broadcastToAllClients({
data: message,
});
},
middleware: [authenticationMiddleware, metricsMiddleware],
});Middleware Execution Flow
Middleware executes in three phases:
- Before - Runs before the handler, can skip handler execution
- Handler - Your subscriber handler runs here
- After - Runs after successful execution
- Error - Runs if handler throws, can suppress or propagate error
interface WebSocketSubscriberMiddleware {
name: string;
// Called before handler (can return false to skip handler)
onBefore?: (context) => boolean | Promise<boolean>;
// Called after successful handler execution
onAfter?: (context, result) => void | Promise<void>;
// Called if handler throws (return true to suppress error)
onError?: (context, error) => boolean | Promise<boolean>;
}Advanced Features
Inactive Client Management
Automatically disconnect clients that haven't sent messages within a timeout period:
const config = {
webSocket: {
disconnectInactiveClients: {
enabled: true,
inactiveTime: 300000, // 5 minutes
intervalCheckTime: 60000, // Check every minute
log: true, // Log disconnections
},
},
};How it works:
- Each client has a
lastActivitytimestamp updated on every message - Periodic check runs at
intervalCheckTimeinterval - Clients inactive longer than
inactiveTimeare disconnected - Useful for cleaning up abandoned connections
Client Metadata
Store custom data with each client:
// Get client
const client = app.websocket.server.clientManager.getClient({ clientId });
// Update client metadata
app.websocket.server.clientManager.updateClient({
clientId,
key: 'preferences',
data: { theme: 'dark', notifications: true },
});
// Access metadata in controllers
export default class UserController extends WebSocketServerBaseController {
public updatePreferences = (ws: WebSocket, clientId: string, data: any) => {
this.webSocketServer.clientManager.updateClient({
clientId,
key: 'preferences',
data,
});
return { success: true };
};
}Performance Monitoring
WebSocket connections are tracked in the performance monitoring system:
const config = {
performanceMonitoring: {
enabled: true,
},
};
// Access WebSocket performance data
app.performance.getWebSocketHealth();
// Returns: { status: 'healthy', activeConnections: 42, rooms: 5, ... }Graceful Shutdown
WebSocket servers automatically handle graceful shutdown:
// Cleanup happens automatically on app.stop()
await app.stop();
// The framework will:
// 1. Stop accepting new connections
// 2. Close all client connections gracefully
// 3. Unsubscribe from Redis events
// 4. Clean up intervals and timers
// 5. Reset managers and stateBest Practices
1. Message Protocol
Define a consistent message structure:
interface WebSocketMessage {
type: string; // Category: 'chat', 'notification', 'system'
action: string; // Specific action: 'send', 'typing', 'update'
data?: any; // Payload
}2. Error Handling
Always handle errors in controllers:
export default class ChatController extends WebSocketServerBaseController {
public send = (ws: WebSocket, clientId: string, data: any) => {
try {
// Validate input
if (!data?.text || typeof data.text !== 'string') {
return {
error: 'Invalid message format',
code: 'INVALID_MESSAGE',
};
}
// Process message
this.webSocketServer.sendMessageToAll({
data: {
/* ... */
},
});
return { success: true };
} catch (error) {
console.error('Error in chat.send:', error);
return {
error: 'Failed to send message',
code: 'SEND_ERROR',
};
}
};
}3. Authentication
Always authenticate sensitive operations:
export default class AdminController extends WebSocketServerBaseController {
public broadcast = (ws: WebSocket, clientId: string, data: any) => {
const client = this.webSocketServer.clientManager.getClient({ clientId });
const userRole = client?.user?.payload?.role;
if (userRole !== 'admin') {
return { error: 'Unauthorized', code: 'FORBIDDEN' };
}
// Admin-only logic
this.webSocketServer.sendMessageToAll({ data });
return { success: true };
};
}4. Room Naming
Use consistent, hierarchical room names:
// Good
'chat:general';
'chat:support';
'game:lobby:1';
'notifications:user:123';
// Avoid
'room1';
'lobby';
'general_chat';5. Connection Management
Implement reconnection logic on the client:
class WebSocketManager {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
connect(url: string) {
this.ws = new WebSocket(url);
this.ws.onopen = () => {
console.log('Connected');
this.reconnectAttempts = 0;
};
this.ws.onclose = () => {
console.log('Disconnected');
this.reconnect(url);
};
this.ws.onerror = error => {
console.error('WebSocket error:', error);
};
}
private reconnect(url: string) {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => this.connect(url), delay);
} else {
console.error('Max reconnection attempts reached');
}
}
}6. Rate Limiting
Prevent message flooding:
export default class ChatController extends WebSocketServerBaseController {
private messageRateLimits = new Map<string, { count: number; resetAt: number }>();
public send = (ws: WebSocket, clientId: string, data: any) => {
// Check rate limit
const limit = this.messageRateLimits.get(clientId);
const now = Date.now();
if (limit) {
if (now < limit.resetAt) {
if (limit.count >= 10) {
return { error: 'Rate limit exceeded', code: 'RATE_LIMIT' };
}
limit.count++;
} else {
limit.count = 1;
limit.resetAt = now + 60000; // 1 minute
}
} else {
this.messageRateLimits.set(clientId, { count: 1, resetAt: now + 60000 });
}
// Process message
this.webSocketServer.sendMessageToAll({ data });
return { success: true };
};
}7. Logging
Log important events:
import { Logger } from '@scpxl/nodejs-framework/logger';
export default class ChatController extends WebSocketServerBaseController {
public send = (ws: WebSocket, clientId: string, data: any) => {
const client = this.webSocketServer.clientManager.getClient({ clientId });
Logger.info({
message: 'Chat message sent',
meta: {
clientId,
userId: client?.user?.userId,
messageLength: data?.text?.length,
},
});
this.webSocketServer.sendMessageToAll({ data });
return { success: true };
};
}Troubleshooting
Connection Refused
Problem: Client cannot connect to WebSocket server
Solutions:
- Verify server is running:
lsof -i :3000 - Check firewall settings
- Ensure correct URL scheme (
ws://orwss://) - Verify CORS settings if connecting from browser
Authentication Failed
Problem: Connection rejected with 401 Unauthorized
Solutions:
- Verify JWT secret key is configured correctly
- Check token expiration
- Ensure token is in query parameter:
?token=<jwt> - Validate token claims (must have
subfor user ID)
// Debug token validation
const config = {
webSocket: {
events: {
onConnected: ({ ws, clientId }) => {
const client = app.websocket.server.clientManager.getClient({ clientId });
console.log('Authenticated user:', client?.user);
},
},
},
};Messages Not Received Across Workers
Problem: Messages sent from one worker not received by clients on other workers
Solutions:
- Verify Redis is running and configured correctly
- Check Redis pub/sub is working:
redis-cli MONITOR - Ensure all workers connect to same Redis instance
- Verify
workerIdis unique per worker
// Debug Redis pub/sub
const config = {
webSocket: {
subscriberHandlers: {
handlers: [
{
channels: ['*'],
handle: ({ channel, message }) => {
console.log('Redis event received:', channel, message);
},
},
],
},
},
};Room Messages Not Delivered
Problem: Messages sent to room not received by room members
Solutions:
- Verify client joined room successfully
- Check room exists:
app.websocket.server.rooms.get('room-name') - Ensure client ID is in room's client set
- Verify room name matches exactly (case-sensitive)
// Debug room membership
app.websocket.server.roomManager.printRooms();Memory Leaks
Problem: Memory usage increases over time
Solutions:
- Enable inactive client disconnection
- Implement cleanup in event handlers
- Remove event listeners on disconnect
- Clear rate limit maps periodically
// Clean up rate limits periodically
setInterval(() => {
const now = Date.now();
for (const [clientId, limit] of this.messageRateLimits.entries()) {
if (now > limit.resetAt + 3600000) {
// 1 hour old
this.messageRateLimits.delete(clientId);
}
}
}, 600000); // Every 10 minutesHigh Latency
Problem: Slow message delivery
Solutions:
- Check Redis latency:
redis-cli --latency - Monitor network conditions
- Reduce message size
- Implement message batching
- Use rooms to limit broadcast scope
// Batch messages
const messageQueue: any[] = [];
setInterval(() => {
if (messageQueue.length > 0) {
app.websocket.server.sendMessageToAll({
data: {
type: 'batch',
action: 'messages',
data: messageQueue.splice(0),
},
});
}
}, 100); // Send every 100msConnection Drops
Problem: Clients frequently disconnected
Solutions:
- Implement heartbeat/ping-pong
- Increase inactive timeout
- Check network stability
- Implement reconnection logic on client
// Server heartbeat
setInterval(() => {
app.websocket.server.sendMessageToAll({
data: { type: 'system', action: 'ping' },
});
}, 30000); // Every 30 seconds
// Client response
ws.onmessage = event => {
const message = JSON.parse(event.data);
if (message.type === 'system' && message.action === 'ping') {
ws.send(JSON.stringify({ type: 'system', action: 'pong' }));
}
};