WebSocket Package
The @hazeljs/websocket package provides real-time communication capabilities for your HazelJS applications. It includes WebSocket gateways, room management, Server-Sent Events (SSE), and decorators for easy real-time event handling.
Purpose
Building real-time applications requires managing WebSocket connections, handling rooms/channels, broadcasting messages, and managing client state. Implementing this from scratch is complex and error-prone. The @hazeljs/websocket package simplifies real-time communication by providing:
- WebSocket Gateways: Decorator-based gateways for real-time communication
- Room Management: Organize clients into rooms for efficient message broadcasting
- Event Handlers: Decorator-based event handling for connections, disconnections, and messages
- Server-Sent Events: Support for SSE as an alternative to WebSockets
- Client Management: Built-in client tracking and metadata support
Architecture
The package uses a gateway-based architecture with room management:
Key Components
- WebSocketGateway: Base class for WebSocket gateways
- RoomManager: Manages client rooms and broadcasting
- SSEHandler: Server-Sent Events support
- Decorators:
@Realtime,@OnConnect,@OnMessage,@Subscribe
Advantages
1. Real-Time Communication
Enable real-time features like chat, notifications, live updates, and collaborative editing.
2. Room-Based Architecture
Organize clients into rooms for efficient message broadcasting and management.
3. Developer Experience
Use decorators to handle events—no need to manually manage WebSocket connections.
4. Flexible Messaging
Support for broadcasting to all clients, specific rooms, or individual clients.
5. Production Features
Includes client tracking, statistics, metadata support, and proper connection lifecycle management.
6. Multiple Protocols
Support for both WebSockets and Server-Sent Events (SSE) for different use cases.
Installation
npm install @hazeljs/websocket
Quick Start
Basic Setup
import { HazelModule } from '@hazeljs/core';
import { WebSocketModule } from '@hazeljs/websocket';
@HazelModule({
imports: [
WebSocketModule.forRoot({
enableSSE: true,
enableRooms: true,
}),
],
})
export class AppModule {}
WebSocket Gateway
Create a WebSocket gateway using the @Realtime decorator:
import { Injectable } from '@hazeljs/core';
import { Realtime, OnConnect, OnDisconnect, OnMessage, Subscribe, Client, Data } from '@hazeljs/websocket';
import { WebSocketGateway } from '@hazeljs/websocket';
@Realtime('/notifications')
export class NotificationGateway extends WebSocketGateway {
@OnConnect()
handleConnection(@Client() client: WebSocketClient) {
console.log('Client connected:', client.id);
this.sendToClient(client.id, 'welcome', { message: 'Connected!' });
}
@OnDisconnect()
handleDisconnection(@Client() client: WebSocketClient) {
console.log('Client disconnected:', client.id);
}
@OnMessage('chat')
handleChatMessage(@Client() client: WebSocketClient, @Data() data: any) {
console.log('Message from', client.id, ':', data);
this.broadcast('chat', { from: client.id, message: data.message });
}
}
Decorators and Annotations
The WebSocket package provides a comprehensive set of decorators for handling real-time communication. These decorators use metadata to configure WebSocket behavior declaratively.
Understanding WebSocket Decorators
WebSocket decorators work together to create a complete real-time communication system:
- Class Decorators: Configure the gateway itself
- Method Decorators: Handle events and messages
- Parameter Decorators: Inject client and data into handlers
@Realtime Decorator
The @Realtime decorator is a class decorator that marks a class as a WebSocket gateway and configures its behavior.
Purpose: Defines a WebSocket gateway with a specific path and configuration options.
How it works:
- Marks the class as a WebSocket gateway
- Stores gateway configuration in class metadata
- Framework uses this to set up WebSocket server at the specified path
- All methods in the class can use WebSocket decorators
Configuration Options:
interface WebSocketGatewayOptions {
path?: string; // WebSocket path (default: '/')
namespace?: string; // Namespace for the gateway
auth?: boolean; // Require authentication (default: false)
pingInterval?: number; // Ping interval in ms (default: 25000)
pingTimeout?: number; // Ping timeout in ms (default: 5000)
maxPayload?: number; // Maximum message size in bytes (default: 1MB)
}
Example with Detailed Explanation:
import { Realtime } from '@hazeljs/websocket';
import { WebSocketGateway } from '@hazeljs/websocket';
// @Realtime is a class decorator that marks this class as a WebSocket gateway
@Realtime('/notifications')
// Path '/notifications' means clients connect to: ws://host/notifications
export class NotificationGateway extends WebSocketGateway {
// This class now handles WebSocket connections at /notifications
// All methods can use WebSocket decorators (@OnConnect, @OnMessage, etc.)
}
// With full configuration:
@Realtime({
path: '/chat',
auth: true, // Require authentication
pingInterval: 30000, // Send ping every 30 seconds
pingTimeout: 10000, // Timeout after 10 seconds of no pong
maxPayload: 1048576, // 1MB max message size
})
export class ChatGateway extends WebSocketGateway {
// Fully configured gateway with authentication and custom settings
}
@OnConnect Decorator
Purpose: Marks a method as the connection handler. Called when a client establishes a WebSocket connection.
How it works:
- Method is called immediately after client connects
- Receives the client object as a parameter
- Perfect place for initialization, authentication, or welcome messages
Example with Detailed Explanation:
@Realtime('/chat')
export class ChatGateway extends WebSocketGateway {
// @OnConnect is a method decorator
// Marks this method to handle new client connections
@OnConnect()
// @Client() is a parameter decorator that injects the WebSocket client
handleConnection(@Client() client: WebSocketClient) {
// This method is called when a client connects to ws://host/chat
// client object contains:
// - client.id: Unique client identifier
// - client.socket: Raw WebSocket connection
// - client.metadata: Map for storing custom data
// - client.rooms: Set of rooms the client is in
console.log('New client connected:', client.id);
// Store custom metadata
client.metadata.set('connectedAt', Date.now());
client.metadata.set('ip', client.socket.remoteAddress);
// Send welcome message
this.sendToClient(client.id, 'welcome', {
clientId: client.id,
message: 'Welcome to the chat!',
timestamp: Date.now(),
});
// Notify other clients
this.broadcast('user-joined', {
clientId: client.id,
}, client.id); // Exclude the new client from broadcast
}
}
@OnDisconnect Decorator
Purpose: Marks a method as the disconnection handler. Called when a client closes the WebSocket connection.
How it works:
- Method is called when connection closes (normal or error)
- Receives the client object
- Use for cleanup, notifications, or resource management
Example with Detailed Explanation:
@Realtime('/chat')
export class ChatGateway extends WebSocketGateway {
@OnDisconnect()
// Called when client disconnects (closes connection)
handleDisconnection(@Client() client: WebSocketClient) {
// This method is called when:
// - Client closes connection normally
// - Connection times out
// - Network error occurs
// - Server closes connection
console.log('Client disconnected:', client.id);
// Clean up: Remove from all rooms
const rooms = this.getClientRooms(client.id);
rooms.forEach(room => {
this.leaveRoom(client.id, room);
// Notify room members
this.broadcastToRoom(room, 'user-left', {
clientId: client.id,
});
});
// Clean up custom resources
client.metadata.clear();
}
}
@OnMessage Decorator
Purpose: Marks a method as a message handler for a specific event type. Called when a client sends a message with that event name.
How it works:
- Decorator takes an event name as parameter
- Method is called when a message with that event is received
- Receives client and message data as parameters
Message Format:
Clients send messages in this format:
{
"event": "chat",
"data": { "text": "Hello!" },
"timestamp": 1234567890
}
Example with Detailed Explanation:
@Realtime('/chat')
export class ChatGateway extends WebSocketGateway {
// @OnMessage is a method decorator that handles specific message events
@OnMessage('chat')
// Handles messages with event: 'chat'
// @Client() injects the sending client
// @Data() injects the message data
handleChatMessage(
@Client() client: WebSocketClient,
@Data() data: { text: string }
) {
// When client sends: { event: 'chat', data: { text: 'Hello' } }
// This method is called with:
// - client: The WebSocket client that sent the message
// - data: The data object from the message
// Broadcast to all connected clients
this.broadcast('chat', {
from: client.id,
text: data.text,
timestamp: Date.now(),
});
}
@OnMessage('private-message')
// Handles different event type: 'private-message'
handlePrivateMessage(
@Client() client: WebSocketClient,
@Data() data: { to: string; text: string }
) {
// Handle private messages between specific clients
const sent = this.sendToClient(data.to, 'private-message', {
from: client.id,
text: data.text,
timestamp: Date.now(),
});
if (sent) {
// Confirm delivery to sender
this.sendToClient(client.id, 'message-sent', { to: data.to });
} else {
// Recipient not found
this.sendToClient(client.id, 'error', {
message: 'User not found or offline',
});
}
}
}
@Subscribe Decorator
Purpose: Marks a method as a subscription handler for event-based subscriptions. Used for pub/sub patterns.
How it works:
- Takes an event pattern with placeholders
- Matches incoming events against the pattern
- Extracts parameters from the pattern
Example with Detailed Explanation:
@Realtime('/events')
export class EventGateway extends WebSocketGateway {
// @Subscribe is a method decorator for event subscriptions
@Subscribe('user-{userId}')
// Pattern: 'user-{userId}' matches events like 'user-123', 'user-456'
// {userId} is extracted as a parameter
onUserEvent(
@Param('userId') userId: string, // Extracted from pattern
@Data() data: any
) {
// When event 'user-123' is published:
// - userId = '123' (extracted from pattern)
// - data = event data
// - Only clients subscribed to 'user-123' receive this
this.sendToClient(userId, 'event', data);
}
@Subscribe('room-{roomId}-{eventType}')
// Complex pattern with multiple parameters
onRoomEvent(
@Param('roomId') roomId: string,
@Param('eventType') eventType: string,
@Data() data: any
) {
// Matches: 'room-general-message', 'room-general-notification', etc.
// Extracts: roomId='general', eventType='message'
this.broadcastToRoom(roomId, eventType, data);
}
}
Parameter Decorators
@Client Decorator
Purpose: Injects the WebSocket client object into a method parameter.
Usage:
@OnConnect()
handleConnection(@Client() client: WebSocketClient) {
// client is the WebSocket client that connected
console.log(client.id);
}
@Data Decorator
Purpose: Injects the message data payload into a method parameter.
Usage:
@OnMessage('chat')
handleMessage(@Client() client: WebSocketClient, @Data() data: any) {
// data contains the message payload
console.log(data.text);
}
@Param Decorator
Purpose: Extracts parameters from event patterns (used with @Subscribe).
Usage:
@Subscribe('user-{userId}')
onEvent(@Param('userId') userId: string, @Data() data: any) {
// userId extracted from pattern 'user-{userId}'
console.log(userId);
}
Decorator Combination Example:
@Realtime('/chat')
export class ChatGateway extends WebSocketGateway {
@OnMessage('join-room')
handleJoinRoom(
@Client() client: WebSocketClient, // The client making the request
@Data() data: { room: string } // Message data
) {
// All three decorators work together:
// - @OnMessage: Handles 'join-room' events
// - @Client: Injects the client object
// - @Data: Injects the message data
this.joinRoom(client.id, data.room);
this.broadcastToRoom(data.room, 'user-joined', {
clientId: client.id,
}, client.id);
}
}
Event Handlers
Connection Events
@Realtime('/chat')
export class ChatGateway extends WebSocketGateway {
@OnConnect()
handleConnection(@Client() client: WebSocketClient) {
console.log('New client connected:', client.id);
// Send welcome message
this.sendToClient(client.id, 'connected', {
clientId: client.id,
timestamp: Date.now(),
});
}
@OnDisconnect()
handleDisconnection(@Client() client: WebSocketClient) {
console.log('Client disconnected:', client.id);
// Notify other clients
this.broadcast('user-left', {
clientId: client.id,
}, client.id); // Exclude the disconnected client
}
}
Message Events
@Realtime('/chat')
export class ChatGateway extends WebSocketGateway {
@OnMessage('message')
handleMessage(@Client() client: WebSocketClient, @Data() data: { text: string }) {
// Broadcast to all clients
this.broadcast('message', {
from: client.id,
text: data.text,
timestamp: Date.now(),
});
}
@OnMessage('private-message')
handlePrivateMessage(
@Client() client: WebSocketClient,
@Data() data: { to: string; text: string }
) {
// Send to specific client
this.sendToClient(data.to, 'private-message', {
from: client.id,
text: data.text,
});
}
}
Room Management
Joining and Leaving Rooms
@Realtime('/chat')
export class ChatGateway extends WebSocketGateway {
@OnMessage('join-room')
handleJoinRoom(@Client() client: WebSocketClient, @Data() data: { room: string }) {
this.joinRoom(client.id, data.room);
this.broadcastToRoom(data.room, 'user-joined', {
clientId: client.id,
}, client.id);
}
@OnMessage('leave-room')
handleLeaveRoom(@Client() client: WebSocketClient, @Data() data: { room: string }) {
this.leaveRoom(client.id, data.room);
this.broadcastToRoom(data.room, 'user-left', {
clientId: client.id,
});
}
@OnMessage('room-message')
handleRoomMessage(
@Client() client: WebSocketClient,
@Data() data: { room: string; message: string }
) {
// Send message to all clients in the room
this.broadcastToRoom(data.room, 'message', {
from: client.id,
message: data.message,
});
}
}
Room Information
@Realtime('/chat')
export class ChatGateway extends WebSocketGateway {
@OnMessage('get-rooms')
handleGetRooms(@Client() client: WebSocketClient) {
const rooms = this.getClientRooms(client.id);
this.sendToClient(client.id, 'rooms', { rooms });
}
@OnMessage('get-room-clients')
handleGetRoomClients(@Client() client: WebSocketClient, @Data() data: { room: string }) {
const clients = this.getRoomClients(data.room);
this.sendToClient(client.id, 'room-clients', { clients });
}
}
Server-Sent Events (SSE)
Use SSE for one-way server-to-client communication:
import { SSEHandler } from '@hazeljs/websocket';
@Injectable()
export class NotificationService {
constructor(private readonly sseHandler: SSEHandler) {}
sendNotification(userId: string, notification: any) {
this.sseHandler.send(userId, 'notification', notification);
}
broadcastNotification(notification: any) {
this.sseHandler.broadcast('notification', notification);
}
}
Subscribe Decorator
Use the @Subscribe decorator for event-based subscriptions:
@Realtime('/events')
export class EventGateway extends WebSocketGateway {
@Subscribe('user-{userId}')
onUserEvent(@Param('userId') userId: string, @Data() data: any) {
// Handle user-specific events
this.sendToClient(userId, 'event', data);
}
@Subscribe('room-{roomId}')
onRoomEvent(@Param('roomId') roomId: string, @Data() data: any) {
// Handle room-specific events
this.broadcastToRoom(roomId, 'event', data);
}
}
Complete Example
import { Injectable } from '@hazeljs/core';
import { Realtime, OnConnect, OnDisconnect, OnMessage, Client, Data } from '@hazeljs/websocket';
import { WebSocketGateway, WebSocketClient } from '@hazeljs/websocket';
@Realtime('/chat')
export class ChatGateway extends WebSocketGateway {
@OnConnect()
handleConnection(@Client() client: WebSocketClient) {
console.log(`Client ${client.id} connected`);
// Store client metadata
client.metadata.set('connectedAt', Date.now());
// Send welcome
this.sendToClient(client.id, 'welcome', {
message: 'Welcome to the chat!',
clientId: client.id,
});
}
@OnDisconnect()
handleDisconnection(@Client() client: WebSocketClient) {
console.log(`Client ${client.id} disconnected`);
// Remove from all rooms
const rooms = this.getClientRooms(client.id);
rooms.forEach(room => {
this.leaveRoom(client.id, room);
this.broadcastToRoom(room, 'user-left', {
clientId: client.id,
});
});
}
@OnMessage('join-room')
handleJoinRoom(@Client() client: WebSocketClient, @Data() data: { room: string }) {
this.joinRoom(client.id, data.room);
// Notify room members
this.broadcastToRoom(data.room, 'user-joined', {
clientId: client.id,
timestamp: Date.now(),
}, client.id);
// Confirm to client
this.sendToClient(client.id, 'joined-room', {
room: data.room,
members: this.getRoomClients(data.room),
});
}
@OnMessage('leave-room')
handleLeaveRoom(@Client() client: WebSocketClient, @Data() data: { room: string }) {
this.leaveRoom(client.id, data.room);
this.broadcastToRoom(data.room, 'user-left', {
clientId: client.id,
});
}
@OnMessage('message')
handleMessage(
@Client() client: WebSocketClient,
@Data() data: { room: string; text: string }
) {
// Broadcast to room
this.broadcastToRoom(data.room, 'message', {
from: client.id,
text: data.text,
timestamp: Date.now(),
});
}
@OnMessage('private-message')
handlePrivateMessage(
@Client() client: WebSocketClient,
@Data() data: { to: string; text: string }
) {
// Send private message
const sent = this.sendToClient(data.to, 'private-message', {
from: client.id,
text: data.text,
timestamp: Date.now(),
});
if (sent) {
// Confirm delivery
this.sendToClient(client.id, 'message-sent', {
to: data.to,
});
} else {
// User not found
this.sendToClient(client.id, 'error', {
message: 'User not found or offline',
});
}
}
// Get statistics
@OnMessage('stats')
handleStats(@Client() client: WebSocketClient) {
const stats = this.getStats();
this.sendToClient(client.id, 'stats', stats);
}
}
Client-Side Example
// Browser client
const ws = new WebSocket('ws://localhost:3000/chat');
ws.onopen = () => {
console.log('Connected');
// Join a room
ws.send(JSON.stringify({
event: 'join-room',
data: { room: 'general' },
}));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log('Received:', message.event, message.data);
if (message.event === 'message') {
// Display message
displayMessage(message.data);
}
};
// Send a message
function sendMessage(text: string) {
ws.send(JSON.stringify({
event: 'message',
data: { room: 'general', text },
}));
}
Best Practices
-
Handle disconnections: Always clean up resources when clients disconnect.
-
Use rooms: Organize clients into rooms for efficient message broadcasting.
-
Validate messages: Validate incoming messages before processing.
-
Rate limiting: Implement rate limiting to prevent abuse.
-
Error handling: Handle errors gracefully and notify clients.
-
Authentication: Authenticate WebSocket connections for secure communication.