Skip to content

WebSocket Gateway

O WebSocket Gateway é o ponto central de comunicação em tempo real, gerenciando conexões, autenticação e distribuição de eventos.

Implementação

Gateway Principal

typescript
// websocket.gateway.ts
@WebSocketGateway({
  cors: {
    origin: process.env.FRONTEND_URL,
    credentials: true,
  },
  namespace: '/',
})
export class WebSocketGatewayService
  implements OnGatewayConnection, OnGatewayDisconnect
{
  @WebSocketServer()
  server: Server;

  private readonly logger = new Logger('WebSocketGateway');
  private connectedUsers = new Map<string, bigint>(); // socketId -> userId

  constructor(
    private sessionService: SessionService,
    private trackingService: WebSocketTrackingService,
  ) {}

  async handleConnection(client: Socket): Promise<void> {
    try {
      // 1. Tentar autenticar
      const sessionId = this.extractSessionId(client);
      
      if (sessionId) {
        const session = await this.sessionService.validate(sessionId);
        
        if (session) {
          // Conexão autenticada
          const userId = session.userId;
          
          // Registrar conexão
          this.connectedUsers.set(client.id, userId);
          await this.trackingService.addConnection(userId, client.id);
          
          // Entrar na sala do usuário
          const room = `user:${userId}`;
          client.join(room);
          
          this.logger.log(
            `Client ${client.id} connected (User: ${userId}, Room: ${room})`,
          );
          
          // Confirmar conexão
          client.emit('connected', {
            message: 'Connected to live drops',
            userId: userId.toString(),
            room,
          });
          
          return;
        }
      }
      
      // 2. Conexão pública (não autenticada)
      this.logger.log(`Client ${client.id} connected (public)`);
      
      client.emit('connected', {
        message: 'Connected to live drops (public)',
      });
      
    } catch (error) {
      this.logger.error(`Connection error: ${error.message}`);
      client.emit('error', { message: 'Connection failed' });
    }
  }

  async handleDisconnect(client: Socket): Promise<void> {
    const userId = this.connectedUsers.get(client.id);
    
    if (userId) {
      await this.trackingService.removeConnection(userId, client.id);
      this.connectedUsers.delete(client.id);
      
      this.logger.log(`Client ${client.id} disconnected (User: ${userId})`);
    } else {
      this.logger.log(`Client ${client.id} disconnected (public)`);
    }
  }

  private extractSessionId(client: Socket): string | null {
    // 1. Tentar do auth
    if (client.handshake.auth?.sessionId) {
      return client.handshake.auth.sessionId;
    }
    
    // 2. Tentar do cookie
    const cookies = client.handshake.headers.cookie;
    if (cookies) {
      const sessionCookie = cookies
        .split(';')
        .find((c) => c.trim().startsWith('sessionId='));
      
      if (sessionCookie) {
        return sessionCookie.split('=')[1].trim();
      }
    }
    
    return null;
  }
}

Métodos de Emissão

typescript
// websocket.gateway.ts (continuação)

// Emitir para usuário específico
emitToUser(userId: bigint, event: string, data: any): void {
  const room = `user:${userId}`;
  this.server.to(room).emit(event, data);
  
  this.logger.debug(`Emitted ${event} to ${room}`);
}

// Emitir para todos (broadcast)
broadcast(event: string, data: any): void {
  this.server.emit(event, data);
  
  this.logger.debug(`Broadcast ${event} to all`);
}

// Emitir para sala específica
emitToRoom(room: string, event: string, data: any): void {
  this.server.to(room).emit(event, data);
  
  this.logger.debug(`Emitted ${event} to room ${room}`);
}

// Atualização de saldo
emitBalanceUpdate(userId: bigint, balanceCents: bigint): void {
  const room = `user:${userId}`;
  const balanceFormatted = Number(balanceCents) / 100;
  
  this.logger.log(
    `[Balance Update] User: ${userId}, Balance: ${balanceFormatted}, Room: ${room}`,
  );
  
  // Verificar se há clientes na sala
  const roomClients = this.server.sockets.adapter.rooms.get(room);
  this.logger.log(`[Balance Update] Clients in room ${room}: ${roomClients?.size || 0}`);
  
  this.server.to(room).emit('balance:updated', {
    balance: balanceFormatted,
    balanceCents: balanceCents.toString(),
    timestamp: new Date().toISOString(),
  });
  
  this.logger.log(`[Balance Update] Event emitted to room ${room}`);
}

// Live drop
emitLiveDrop(drop: LiveDropData): void {
  this.server.emit('live:drop', {
    id: drop.id,
    itemName: drop.itemName,
    itemImage: drop.itemImageUrl,
    rarity: drop.rarity,
    valueCents: drop.valueCents,
    userName: drop.userName,
    userAvatar: drop.userAvatar,
    caseId: drop.caseId,
    caseName: drop.caseName,
    timestamp: new Date().toISOString(),
  });
}

// Notificação
emitNotification(userId: bigint, notification: Notification): void {
  this.emitToUser(userId, 'notification', {
    id: notification.id,
    type: notification.type,
    title: notification.title,
    message: notification.message,
    data: notification.data,
    createdAt: notification.createdAt,
  });
}

Tracking Service

Rastreia conexões ativas por usuário para diagnóstico e métricas.

typescript
// websocket-tracking.service.ts
@Injectable()
export class WebSocketTrackingService {
  constructor(private redis: RedisService) {}

  async addConnection(userId: bigint, socketId: string): Promise<void> {
    const key = `ws:user:${userId}:connections`;
    await this.redis.sadd(key, socketId);
    await this.redis.expire(key, 86400); // 24h TTL
    
    // Incrementar contador global
    await this.redis.incr('ws:connections:total');
    await this.redis.incr('ws:connections:authenticated');
  }

  async removeConnection(userId: bigint, socketId: string): Promise<void> {
    const key = `ws:user:${userId}:connections`;
    await this.redis.srem(key, socketId);
    
    // Decrementar contador global
    await this.redis.decr('ws:connections:total');
    await this.redis.decr('ws:connections:authenticated');
  }

  async getUserConnections(userId: bigint): Promise<string[]> {
    const key = `ws:user:${userId}:connections`;
    return await this.redis.smembers(key);
  }

  async isUserOnline(userId: bigint): Promise<boolean> {
    const connections = await this.getUserConnections(userId);
    return connections.length > 0;
  }

  async getOnlineUsersCount(): Promise<number> {
    const count = await this.redis.get('ws:connections:authenticated');
    return parseInt(count || '0', 10);
  }
}

Módulo WebSocket

typescript
// websocket.module.ts
@Module({
  imports: [
    SessionModule,
    RedisModule,
  ],
  providers: [
    WebSocketGatewayService,
    WebSocketTrackingService,
  ],
  exports: [
    WebSocketGatewayService,
    WebSocketTrackingService,
  ],
})
export class WebSocketModule {}

Configuração no Main

typescript
// main.ts
async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
  );

  // Configurar Redis Adapter para WebSocket
  const redisIoAdapter = new RedisIoAdapter(app);
  await redisIoAdapter.connectToRedis();
  app.useWebSocketAdapter(redisIoAdapter);

  await app.listen(3000, '0.0.0.0');
}

Handlers de Eventos

Eventos do Cliente

typescript
// websocket.gateway.ts

@SubscribeMessage('ping')
handlePing(client: Socket): void {
  client.emit('pong', { timestamp: Date.now() });
}

@SubscribeMessage('join:battle')
async handleJoinBattle(
  client: Socket,
  payload: { battleId: string },
): Promise<void> {
  const userId = this.connectedUsers.get(client.id);
  
  if (!userId) {
    client.emit('error', { message: 'Not authenticated' });
    return;
  }
  
  // Entrar na sala da batalha
  const room = `battle:${payload.battleId}`;
  client.join(room);
  
  client.emit('battle:joined', { room });
}

@SubscribeMessage('leave:battle')
async handleLeaveBattle(
  client: Socket,
  payload: { battleId: string },
): Promise<void> {
  const room = `battle:${payload.battleId}`;
  client.leave(room);
  
  client.emit('battle:left', { room });
}

@SubscribeMessage('subscribe:case')
async handleSubscribeCase(
  client: Socket,
  payload: { caseId: string },
): Promise<void> {
  const room = `case:${payload.caseId}`;
  client.join(room);
  
  client.emit('case:subscribed', { room });
}

Cliente Frontend

Context Provider

tsx
// contexts/WebSocketContext.tsx
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
import { io, Socket } from 'socket.io-client';
import { useAuth } from './AuthContext';

interface WebSocketContextValue {
  socket: Socket | null;
  isConnected: boolean;
  balance: number;
  subscribe: (event: string, handler: (data: any) => void) => () => void;
}

const WebSocketContext = createContext<WebSocketContextValue | null>(null);

export function WebSocketProvider({ children }: { children: React.ReactNode }) {
  const [socket, setSocket] = useState<Socket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [balance, setBalance] = useState(0);
  const { user, sessionId } = useAuth();

  useEffect(() => {
    const socketInstance = io(process.env.NEXT_PUBLIC_WS_URL!, {
      auth: sessionId ? { sessionId } : undefined,
      withCredentials: true,
      transports: ['websocket', 'polling'],
      reconnection: true,
      reconnectionAttempts: 5,
    });

    socketInstance.on('connect', () => {
      console.log('[WebSocket] Connected:', socketInstance.id);
      setIsConnected(true);
    });

    socketInstance.on('disconnect', () => {
      console.log('[WebSocket] Disconnected');
      setIsConnected(false);
    });

    socketInstance.on('connected', (data) => {
      console.log('[WebSocket] Server confirmed:', data);
    });

    socketInstance.on('balance:updated', (data) => {
      console.log('[WebSocket] Balance updated:', data.balance);
      setBalance(data.balance);
    });

    socketInstance.on('connect_error', (error) => {
      console.error('[WebSocket] Connection error:', error);
    });

    setSocket(socketInstance);

    return () => {
      socketInstance.disconnect();
    };
  }, [sessionId]);

  const subscribe = useCallback(
    (event: string, handler: (data: any) => void) => {
      if (!socket) return () => {};

      socket.on(event, handler);
      
      return () => {
        socket.off(event, handler);
      };
    },
    [socket],
  );

  return (
    <WebSocketContext.Provider
      value={{ socket, isConnected, balance, subscribe }}
    >
      {children}
    </WebSocketContext.Provider>
  );
}

export function useWebSocket() {
  const context = useContext(WebSocketContext);
  if (!context) {
    throw new Error('useWebSocket must be used within WebSocketProvider');
  }
  return context;
}

Hook de Eventos

tsx
// hooks/useSocketEvent.ts
import { useEffect } from 'react';
import { useWebSocket } from '../contexts/WebSocketContext';

export function useSocketEvent<T = any>(
  event: string,
  handler: (data: T) => void,
) {
  const { subscribe } = useWebSocket();

  useEffect(() => {
    const unsubscribe = subscribe(event, handler);
    return unsubscribe;
  }, [event, handler, subscribe]);
}

// Uso:
function LiveDropsFeed() {
  const [drops, setDrops] = useState<Drop[]>([]);

  useSocketEvent('live:drop', (drop) => {
    setDrops((prev) => [drop, ...prev.slice(0, 19)]);
  });

  return (
    <div className="live-drops">
      {drops.map((drop) => (
        <DropCard key={drop.id} drop={drop} />
      ))}
    </div>
  );
}

Debugging

Logs do Backend

typescript
// Habilitar logs detalhados
const logger = new Logger('WebSocketGateway');

// Em cada evento
logger.debug(`[Event] ${event} - Data: ${JSON.stringify(data)}`);
logger.log(`[Room] ${room} - Clients: ${this.server.sockets.adapter.rooms.get(room)?.size}`);

Logs do Frontend

typescript
// Adicionar logs detalhados
socketInstance.onAny((event, ...args) => {
  console.log(`[WebSocket] Event: ${event}`, args);
});

Verificar Conexões

typescript
// Admin endpoint para debug
@Get('admin/websocket/status')
async getWebSocketStatus() {
  const rooms = this.server.sockets.adapter.rooms;
  const sockets = await this.server.fetchSockets();
  
  return {
    totalConnections: sockets.length,
    rooms: Array.from(rooms.keys()),
    connectedUsers: this.connectedUsers.size,
  };
}

Troubleshooting

Conexão Não Estabelece

  1. Verificar CORS configuration
  2. Verificar se Redis está acessível
  3. Verificar cookies (SameSite, Secure)
  4. Verificar se o load balancer suporta WebSocket

Eventos Não Chegam

typescript
// Verificar se cliente está na sala correta
const room = `user:${userId}`;
const clients = this.server.sockets.adapter.rooms.get(room);
console.log(`Clients in ${room}:`, clients?.size || 0);

// Verificar se evento está sendo emitido
this.server.to(room).emit('test', { message: 'Test event' });

Múltiplas Conexões

typescript
// Limitar conexões por usuário
const MAX_CONNECTIONS_PER_USER = 5;

async handleConnection(client: Socket) {
  // ... autenticação ...
  
  const connections = await this.trackingService.getUserConnections(userId);
  
  if (connections.length >= MAX_CONNECTIONS_PER_USER) {
    // Desconectar conexão mais antiga
    const oldestSocketId = connections[0];
    const oldSocket = this.server.sockets.sockets.get(oldestSocketId);
    if (oldSocket) {
      oldSocket.disconnect(true);
    }
  }
}

Documentação Técnica CSGOFlip