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:

Loading diagram...

Key Components

  1. WebSocketGateway: Base class for WebSocket gateways
  2. RoomManager: Manages client rooms and broadcasting
  3. SSEHandler: Server-Sent Events support
  4. 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

  1. Handle disconnections: Always clean up resources when clients disconnect.

  2. Use rooms: Organize clients into rooms for efficient message broadcasting.

  3. Validate messages: Validate incoming messages before processing.

  4. Rate limiting: Implement rate limiting to prevent abuse.

  5. Error handling: Handle errors gracefully and notify clients.

  6. Authentication: Authenticate WebSocket connections for secure communication.

What's Next?

  • Learn about Auth for WebSocket authentication
  • Explore Cache for caching WebSocket data
  • Check out Config for WebSocket configuration