11import { Inject , forwardRef } from '@nestjs/common' ;
22import type { OnModuleInit } from '@nestjs/common' ;
3+ import { plainToInstance } from 'class-transformer' ;
4+ import { validate } from 'class-validator' ;
35import {
46 WebSocketGateway ,
57 WebSocketServer ,
@@ -12,10 +14,10 @@ import type { MatchAssignedChannel } from '../../infra/channels/match-assigned-c
1214import { isKickPayload , NODE_INBOX_CHANNEL } from '../../infra/channels/node-inbox-channel.interface' ;
1315import type { NodeInboxChannel } from '../../infra/channels/node-inbox-channel.interface' ;
1416import { NODE_ID } from '../../infra/config/env.config' ;
17+ import { getMatchmakingRole , isApiEnabled } from '../matchmaking/matchmaking-role' ;
1518import { USER_SESSION_REGISTRY } from './user-session-registry.interface' ;
1619import type { UserSessionRegistry } from './user-session-registry.interface' ;
17- import { buildEnqueuedEnvelope , type WsErrorResponse } from './ws-envelope.dto' ;
18- import type { WsEnqueueMessage } from './ws-envelope.dto' ;
20+ import { buildEnqueuedEnvelope , WsEnqueueMessageDto , type WsErrorResponse } from './ws-envelope.dto' ;
1921import { Server , WebSocket as WsWebSocket } from 'ws' ;
2022import { IncomingMessage } from 'http' ;
2123
@@ -79,6 +81,10 @@ export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect
7981 }
8082
8183 handleConnection ( client : WsWebSocket , request : IncomingMessage ) {
84+ if ( ! isApiEnabled ( getMatchmakingRole ( ) ) ) {
85+ client . close ( 4403 , 'This node does not serve WebSocket connections (queue-worker role)' ) ;
86+ return ;
87+ }
8288 const url = new URL ( request . url ?? '' , `http://${ request . headers . host } ` ) ;
8389 const ticketId = url . searchParams . get ( 'ticketId' ) ;
8490 const userId = url . searchParams . get ( 'userId' ) ;
@@ -113,27 +119,38 @@ export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect
113119
114120 private setupMessageHandler ( client : WsWebSocket ) : void {
115121 client . on ( 'message' , ( data : Buffer | string ) => {
116- let msg : unknown ;
117- try {
118- msg = JSON . parse ( data . toString ( ) ) ;
119- } catch {
120- this . sendError ( client , 'Invalid JSON' ) ;
121- return ;
122- }
123- const m = msg as { action ?: string } ;
124- if ( m ?. action === 'enqueue' ) {
125- this . handleEnqueue ( client , m as WsEnqueueMessage ) ;
126- return ;
127- }
128- if ( m ?. action === 'heartbeat' ) {
129- this . handleHeartbeat ( client ) ;
130- return ;
131- }
132- this . sendError ( client , 'Expected action: enqueue or heartbeat' ) ;
122+ void ( async ( ) => {
123+ let msg : unknown ;
124+ try {
125+ msg = JSON . parse ( data . toString ( ) ) ;
126+ } catch {
127+ this . sendError ( client , 'Invalid JSON' ) ;
128+ return ;
129+ }
130+ const m = msg as { action ?: string } ;
131+ if ( m ?. action === 'enqueue' ) {
132+ const dto = plainToInstance ( WsEnqueueMessageDto , m ) ;
133+ const errors = await validate ( dto ) ;
134+ if ( errors . length > 0 ) {
135+ const detail = errors
136+ . flatMap ( ( e ) => Object . values ( e . constraints ?? { } ) )
137+ . join ( ', ' ) ;
138+ this . sendError ( client , `Invalid enqueue payload: ${ detail } ` ) ;
139+ return ;
140+ }
141+ await this . handleEnqueue ( client , dto ) ;
142+ return ;
143+ }
144+ if ( m ?. action === 'heartbeat' ) {
145+ this . handleHeartbeat ( client ) ;
146+ return ;
147+ }
148+ this . sendError ( client , 'Expected action: enqueue or heartbeat' ) ;
149+ } ) ( ) ;
133150 } ) ;
134151 }
135152
136- private async handleEnqueue ( client : WsWebSocket , m : WsEnqueueMessage ) : Promise < void > {
153+ private async handleEnqueue ( client : WsWebSocket , m : WsEnqueueMessageDto ) : Promise < void > {
137154 try {
138155 const result = await this . matchmakingService . enqueue (
139156 {
0 commit comments