import { WebSocketGateway, WebSocketServer, // SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { Logger, Inject, forwardRef } from '@nestjs/common'; import { NotificationService } from './services/notification.service'; import { WsAdminAuthGuard, type AuthenticatedSocket } from './guards/ws-admin-auth.guard'; import { ModuleRef } from '@nestjs/core'; import { ExecutionContext } from '@nestjs/common'; import { IInAppNotificationPayload } from './interfaces/notification.interface'; @WebSocketGateway({ cors: { origin: true, credentials: true, }, namespace: '/notifications', transports: ['websocket', 'polling'], }) export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server!: Server; private readonly logger = new Logger(NotificationsGateway.name); private readonly pingIntervals = new Map(); constructor( @Inject(forwardRef(() => NotificationService)) private readonly notificationService: NotificationService, private readonly moduleRef: ModuleRef, ) { } async handleConnection(client: Socket) { try { await this.authenticateConnection(client); const authenticatedClient = client as AuthenticatedSocket; this.logger.log(`Admin connected: ${authenticatedClient.adminId}`); this.handleJoinRoom(authenticatedClient); // Start ping interval for this client // const intervalId = setInterval(() => { // void authenticatedClient.emit('ping', { message: 'ping', timestamp: new Date().toISOString() }); // }, 2000); // this.pingIntervals.set(client.id, intervalId); } catch (error) { this.logger.error( `Connection authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`, ); void client.emit('error', { message: 'Authentication failed' }); client.disconnect(); } } handleDisconnect(client: Socket) { this.logger.log(`Client disconnected: ${client.id}`); this.handleLeaveRoom(client); // Clear ping interval for this client // const intervalId = this.pingIntervals.get(client.id); // if (intervalId) { // clearInterval(intervalId); // this.pingIntervals.delete(client.id); // } } sendInAppNotification(repipient: { adminId: string; }, payload: IInAppNotificationPayload) { const room = this.getRoom(repipient.adminId,); this.logger.log(`Sending in app notification to admin: ${repipient.adminId}`); this.server.to(room).emit('notifications', payload, async () => { // 3. ACK (delivered) // await this.notificationService.markAsDelivered(payload.notificationId); }); this.logger.log(`In app notification sent to admin: ${repipient.adminId}`); } private getRoom(adminId: string,) { return `admin:${adminId}`; } handleLeaveRoom(client: AuthenticatedSocket) { const room = this.getRoom(client.adminId!,); void client.leave(room); this.logger.log(`Admin ${client.adminId} (Client ${client.id}) left room: ${room}`); void client.emit('left', { room, message: 'Successfully Admin left room' }); } handleJoinRoom(client: AuthenticatedSocket) { const room = this.getRoom(client.adminId!,); void client.join(room); this.logger.log(`Admin ${client.adminId} (Client ${client.id}) joined room: ${room}`); void client.emit('joined', { room, message: 'Successfully Admin joined room' }); } private async authenticateConnection(client: Socket): Promise { const guard = this.moduleRef.get(WsAdminAuthGuard); const context = { switchToWs: () => ({ getClient: () => client }), getClass: () => NotificationsGateway, getHandler: () => this.handleConnection, } as unknown as ExecutionContext; const canActivate = await guard.canActivate(context); if (!canActivate) { throw new Error('Authentication failed'); } } }