Add an RT WebSocket Event¶
Canonical example: blackjack events at libs/shared/src/events/bj.events.ts:9-38.
Architecture overview¶
The RT app (apps/rt/, port 4001) runs a single Socket.IO namespace /events in
websocket-only mode (client.gateway.ts:48-53). Clients connect, authenticate via a
socket_token cookie, then exchange events through a wildcard handler (@SubscribeMessage('*') at client.gateway.ts:164).
Events flow through three channels defined in libs/gateway/src/events.ts:1-5:
| Channel | Prefix | Direction | Example |
|---|---|---|---|
Server |
server_channel_event |
Server → Client | BlackjackTableUpdated |
Client |
client_channel_event |
Client → Server | BlackjackTableJoin |
Private |
private_channel_event |
Internal only | AuthSocket |
1. Define the event constants¶
Create or extend a game-events file. Follow bj.events.ts:9-38:
// libs/shared/src/events/your-game.events.ts
import { GATEWAY_CHANNEL, GATEWAY_EVENTS, GatewayEvents } from '@app/gateway/events';
export const GATEWAY_YOUR_GAME_EVENTS = {
Server: {
YourGameStateChanged: `${GATEWAY_CHANNEL.Server}.YourGameStateChanged`,
YourGameResult: `${GATEWAY_CHANNEL.Server}.YourGameResult`,
},
Client: {
YourGameJoin: `${GATEWAY_CHANNEL.Client}.YourGameJoin`,
YourGameLeave: `${GATEWAY_CHANNEL.Client}.YourGameLeave`,
YourGameAction: `${GATEWAY_CHANNEL.Client}.YourGameAction`,
},
Private: {
...GATEWAY_EVENTS.Private,
},
} satisfies GatewayEvents;
The satisfies GatewayEvents constraint (bj.events.ts:38) ensures the shape matches
{ Server, Client, Private } with string values.
2. Register client-listened events¶
The RT gateway maintains a whitelist of allowed client events (client.gateway.ts:57).
Unknown events are rejected at client.gateway.ts:178.
Your game's microservice must respond to RequestClientEvents by sending back its
Client event keys:
@Controller()
export class GatewayController {
@EventPattern(GATEWAY_EVENTS.Private.RequestClientEvents)
onRequestClientEvents() {
return {
event: GATEWAY_EVENTS.Private.ClientEvents,
data: Object.values(GATEWAY_YOUR_GAME_EVENTS.Client),
};
}
}
3. Handle client → server events¶
The RT gateway's wildcard handler (client.gateway.ts:164-220) forwards all client
events to the backend microservice via Redis transport. Your game service handles
them with @MessagePattern:
// apps/your-game/src/gateway/gateway.controller.ts
@MessagePattern(GATEWAY_YOUR_GAME_EVENTS.Client.YourGameJoin)
async onJoin(@Payload() message: EventMessage) {
const { userId } = message.user;
const { roomId } = message.data;
// Return a room-join instruction — the RT gateway
// calls socket.join(room) automatically (client.gateway.ts:229-248)
return {
type: GatewayMessageType.ClientRoom,
data: { action: 'join', rooms: [`your-game:${roomId}`] },
};
}
@MessagePattern(GATEWAY_YOUR_GAME_EVENTS.Client.YourGameAction)
async onAction(@Payload() message: EventMessage) {
const result = await this.gameService.handleAction(message.data);
// Return data directly to the calling client via ack
return {
type: GatewayMessageType.ClientMessage,
data: result,
};
}
Return types from libs/gateway/src/dto/message.dto.ts:
- ClientRoom — instructs the gateway to join/leave socket.io rooms.
- ClientMessage — sends data back to the requesting client.
4. Push server → client events¶
For broadcasting state changes to connected clients without a client request,
emit through the Server channel. The RT gateway routes based on the message
metadata (client.gateway.ts:308-321):
// Inside your game service
import { ClientProxyFactory } from '@nestjs/microservices';
import { GATEWAY_YOUR_GAME_EVENTS } from '@app/shared/events/your-game.events';
// Emit to a specific user
this.gatewayProxy.emit(
GATEWAY_YOUR_GAME_EVENTS.Server.YourGameResult,
{
user: { id: userId },
data: { won: true, payout: '1.50' },
},
);
// Emit to a socket.io room (all clients who joined)
this.gatewayProxy.emit(
GATEWAY_YOUR_GAME_EVENTS.Server.YourGameStateChanged,
{
room: `your-game:${roomId}`,
data: { state: 'dealing', round: 5 },
},
);
// Broadcast to all connected clients (omit user + room)
this.gatewayProxy.emit(
GATEWAY_YOUR_GAME_EVENTS.Server.YourGameStateChanged,
{ data: { announcement: 'New round starting' } },
);
5. Subscribe on the frontend¶
Follow the pattern from ebit-fe/src/providers/socketsWithAuth.tsx:
// React hook pattern
import { useEffect } from 'react';
import { useSocket } from '@/providers/socketsWithAuth';
export function useYourGameEvents(roomId: string) {
const { publicChatSocket } = useSocket();
useEffect(() => {
if (!publicChatSocket) return;
// Join the game room (with ack)
publicChatSocket.emitWithAck('YourGameJoin', { roomId });
const onStateChanged = (data: YourGameState) => {
if (data.__m?.windowId === window.__windowId) return; // dedup
setGameState(data);
};
publicChatSocket.on('YourGameStateChanged', onStateChanged);
return () => {
publicChatSocket.off('YourGameStateChanged', onStateChanged);
publicChatSocket.emit('YourGameLeave', { roomId });
};
}, [publicChatSocket, roomId]);
}
Client-side event names omit the channel prefix — the gateway strips it.
server_channel_event.YourGameStateChanged arrives as YourGameStateChanged.
6. Auth and throttling¶
-
Auth on connect: The gateway extracts
socket_tokenfrom the handshake and callsauthorizeClient()(client.gateway.ts:94-114). Setsclient.authorizedandclient.userfor subsequent events. -
WsThrottlerGuard: Lives at
libs/ws-throttler/src/ws-throttler.guard.ts. Rate-limits by IP, emitserrorevent and disconnects if blocked. Apply via@UseGuards(WsThrottlerGuard)if needed.