Skip to content
npm version

WebSocket: Bidirectional Communication

Overview

Implement type-safe bidirectional communication between frontend and backend, supporting frontend calling backend methods, backend pushing messages to frontend, with complete type inference.

Core Features

  • Frontend calls backend: Call backend gateway methods directly through emitter
  • Backend pushes to frontend: Use emitWith to send type-safe messages to different targets
  • Event listening: Frontend listens to backend-pushed events through createListener
  • Request-response: Support request-response pattern with ACK

Backend Implementation

Event Definition

Event Definition Example:

ts
import { Emit } from 'vtzac/typed-emit';

export class ChatEvents {
  @Emit('welcome')
  welcome(nickname: string) {
    return {
      message: `Welcome ${nickname}!`,
      timestamp: Date.now(),
    };
  }

  @Emit('message')
  message(text: string) {
    return { text, timestamp: Date.now() };
  }

  @Emit('pong')
  pong() {
    return { message: 'pong', timestamp: Date.now() };
  }
}

Gateway Implementation

Backend Gateway Example:

ts
import type { Server, Socket } from 'socket.io';
import {
  ConnectedSocket,
  MessageBody,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { emitWith } from 'vtzac/typed-emit';
import { ChatEvents } from './chat-events';

@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway {
  private readonly events = new ChatEvents();

  @WebSocketServer()
  server!: Server;

  // Send welcome message when new client connects
  handleConnection(client: Socket): void {
    const nickname = `User${client.id.slice(-4)}`;
    emitWith(this.events.welcome, this.events)(nickname).toClient(client);
    // Actual event sent:
    // emit 'welcome' { message: 'Welcome User1234!', timestamp: 1703123456789 }
  }

  // Heartbeat detection
  @SubscribeMessage('ping')
  handlePing(@ConnectedSocket() client?: Socket): void {
    emitWith(this.events.pong, this.events)().toClient(client!);
    // Actual event sent:
    // emit 'pong' { message: 'pong', timestamp: 1703123456789 }
  }

  // Broadcast message
  @SubscribeMessage('say')
  handleSay(@MessageBody() data: { text: string }): void {
    emitWith(this.events.message, this.events)(data.text).toServer(this.server);
    // Actual event sent:
    // Broadcast 'message' { text: 'Hello everyone!', timestamp: 1703123456789 } to all clients
  }

  // Join room
  @SubscribeMessage('joinRoom')
  handleJoinRoom(
    @MessageBody() room: string,
    @ConnectedSocket() client?: Socket
  ) {
    client!.join(room); // Join room first
    emitWith(
      this.events.message,
      this.events
    )(`Joined room ${room}`).toRoomAll(this.server, room);
    // Actual event sent:
    // Send 'message' event to all clients in the room
  }
}

Frontend Implementation

Frontend Usage Example:

ts
import { _socket } from 'vtzac';
import { ChatGateway } from './chat.gateway';
import { ChatEvents } from './chat-events';

// Establish connection
const { emitter, createListener, socket, disconnect } = _socket(
  'http://localhost:3000',
  ChatGateway,
  {
    socketIoOptions: { transports: ['websocket'] },
  }
);

// Call backend methods
emitter.handlePing();
console.log('Sent heartbeat'); // Output: Sent heartbeat

emitter.handleSay({ text: 'Hello everyone!' });
console.log('Sent message'); // Output: Sent message

emitter.handleJoinRoom('room1');
console.log('Joined room'); // Output: Joined room

// Create event listener
const events = createListener(ChatEvents);

// Listen to backend-pushed events
events.pong(data => {
  console.log('Received heartbeat response:', data);
  // Output: Received heartbeat response: { message: 'pong', timestamp: 1703123456789 }
});

events.welcome(data => {
  console.log('Received welcome message:', data);
  // Output: Received welcome message: { message: 'Welcome User1234!', timestamp: 1703123456789 }
});

events.message(data => {
  console.log('Received message:', data);
  // Output: Received message: { text: 'Hello everyone!', timestamp: 1703123456789 }
});

// Disconnect
setTimeout(() => disconnect(), 10000);
// Actual WebSocket events:
// emit 'ping' (no data)
// emit 'say' { text: 'Hello everyone!' }
// emit 'joinRoom' 'room1'
// Listen to events: 'pong', 'welcome', 'message'

Advanced Features

Request-Response Pattern

When backend methods return values, frontend calls will return Promise:

Backend ACK Response Example:

ts
@WebSocketGateway()
export class ChatGateway {
  @SubscribeMessage('getOnlineCount')
  handleGetOnlineCount() {
    return { count: 42 }; // Return online count
  }
}

Frontend ACK Call Example:

ts
const { emitter } = _socket('http://localhost:3000', ChatGateway);
// If there's a return value, it will automatically adjust to emitWithAck call
const result = await emitter.handleGetOnlineCount();
console.log('Online count:', result.count); // Output: Online count: 42

Room Management

Room Broadcast Example:

ts
// Send to all clients
emitWith(events.message, events)('Site-wide announcement').toServer(server);

// Send to specific room
emitWith(
  events.message,
  events
)('Room announcement').toRoomAll(server, 'room1');

Namespace

Namespace Gateway Example:

ts
@WebSocketGateway({ cors: { origin: '*' }, namespace: '/chat' })
export class ChatGateway {}

Frontend Connect to Namespace:

ts
// Automatically connect to /chat namespace
const { emitter } = _socket('http://localhost:3000', ChatGateway);

Native Socket Access

You can directly access the native Socket instance for custom operations:

Native Socket Usage Example:

ts
import { _socket } from 'vtzac';

const { socket } = _socket('http://localhost:3000', ChatGateway);

// Use native Socket API
socket.on('connect', () => {
  console.log('Connected successfully'); // Output: Connected successfully
});

socket.emit('customEvent', { data: 'test' });
console.log('Sent custom event'); // Output: Sent custom event

Summary

  • Type Safety: Full type inference and checking throughout frontend-backend communication
  • Simplified Development: Avoid manual event names and data structures, reduce errors
  • Bidirectional Communication: Support complete scenarios of frontend calling backend and backend pushing to frontend