Scalable WebSocket Solutions for Modern Applications

The Problem with Stateless Applications

Traditional backend applications are designed to be stateless, allowing for easy scalability and fault tolerance. However, when introducing WebSockets, this stateless nature is compromised. WebSockets require maintaining a connection state, which can lead to complexities when scaling applications.

Setting Up a Nest Application

We’ll use the Nest CLI to scaffold our application and Docker with docker-compose to add Redis and Postgres for local development. Our Redis setup will utilize ioredis, providing robust performance and features.


nest new my-app
cd my-app
docker-compose up -d

Creating a State of Sockets

To manage WebSocket connections, we’ll implement a socket state service. This service will store sockets in a Map<string, Socket[]> format, allowing us to easily retrieve sockets for a specified user.


import { Injectable } from '@nestjs/common';
import { Socket } from 'ocket.io';

@Injectable()
export class SocketStateService {
  private sockets: Map<string, socket[]=""> = new Map();

  addSocket(userId: string, socket: Socket): void {
    const userSockets = this.sockets.get(userId) || [];
    userSockets.push(socket);
    this.sockets.set(userId, userSockets);
  }

  removeSocket(userId: string, socket: Socket): void {
    const userSockets = this.sockets.get(userId);
    if (userSockets) {
      const index = userSockets.indexOf(socket);
      if (index!== -1) {
        userSockets.splice(index, 1);
      }
    }
  }

  getSockets(userId: string): Socket[] {
    return this.sockets.get(userId) || [];
  }
}
</string,>

Creating an Adapter

Nest’s adapter system enables seamless integration with various libraries. We’ll extend the existing socket.io adapter to track currently open sockets.


import { Injectable } from '@nestjs/common';
import { Server } from 'ocket.io';
import { SocketStateService } from './socket-state.service';

@Injectable()
export class SocketAdapter extends Server {
  constructor(private readonly socketStateService: SocketStateService) {
    super();
  }

  create(): void {
    //...
  }

  bindClientConnect(): void {
    //...
  }
}

Creating the Redis Event Propagator

With our Redis integration and socket state in place, we’ll create a Redis event propagator service. This service will listen to incoming Redis events and dispatch events to other instances.


import { Injectable } from '@nestjs/common';
import * as ioredis from 'ioredis';

@Injectable()
export class RedisEventPropagatorService {
  private redis: ioredis;

  constructor() {
    this.redis = new ioredis('redis://localhost:6379');
  }

  listen(): void {
    this.redis.subscribe('events');
    this.redis.on('message', (channel, message) => {
      if (channel === 'events') {
        this.dispatch(message);
      }
    });
  }

  dispatch(event: string): void {
    //...
  }
}

Listening to Event Dispatches

To fully utilize the Nest ecosystem, we’ll create an interceptor that will have access to each socket event response.


import { Injectable, NestInterceptor, ExecutionContext } from '@nestjs/common';

@Injectable()
export class SocketEventInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: () => Observable): Observable {
    return next().pipe(
      tap((response) => {
        // React to the response without altering it
      }),
    );
  }
}

Working Example

Our final step is to create a gateway that listens to incoming events and demonstrates the propagation of events across instances.


import { WebSocketGateway, SubscribeMessage, MessageBody } from '@nestjs/websockets';
import { UseInterceptors } from '@nestjs/common';
import { SocketEventInterceptor } from './socket-event.interceptor';

@WebSocketGateway()
@UseInterceptors(SocketEventInterceptor)
export class EventsGateway {
  @SubscribeMessage('events')
  handleEvent(@MessageBody() event: string): void {
    // Handle incoming events
  }
}

We’ll use the @UseInterceptors decorator to register the interceptor and simulate a user session with a fake token.

Leave a Reply