Files
friendolls-server/src/ws/state/state.gateway.ts

168 lines
5.5 KiB
TypeScript

import { Logger, Inject, OnModuleDestroy } from '@nestjs/common';
import {
OnGatewayConnection,
OnGatewayDisconnect,
OnGatewayInit,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import Redis from 'ioredis';
import type { Server } from 'socket.io';
import {
REDIS_CLIENT,
REDIS_SUBSCRIBER_CLIENT,
} from '../../database/redis.module';
import type { AuthenticatedSocket } from '../../types/socket';
import { JwtVerificationService } from '../../auth/services/jwt-verification.service';
import { CursorPositionDto } from '../dto/cursor-position.dto';
import { UserStatusDto } from '../dto/user-status.dto';
import { SendInteractionDto } from '../dto/send-interaction.dto';
import { PrismaService } from '../../database/prisma.service';
import { UserSocketService } from './user-socket.service';
import { WsNotificationService } from './ws-notification.service';
import { WS_EVENT, REDIS_CHANNEL } from './ws-events';
import { ConnectionHandler } from './connection/handler';
import { CursorHandler } from './cursor/handler';
import { StatusHandler } from './status/handler';
import { InteractionHandler } from './interaction/handler';
import { RedisHandler } from './utils/redis-handler';
import { Broadcaster } from './utils/broadcasting';
import { Throttler } from './utils/throttling';
@WebSocketGateway()
export class StateGateway
implements
OnGatewayInit,
OnGatewayConnection,
OnGatewayDisconnect,
OnModuleDestroy
{
private readonly logger = new Logger(StateGateway.name);
@WebSocketServer() io: Server;
private readonly throttler = new Throttler();
private readonly broadcaster: Broadcaster;
private readonly redisHandler: RedisHandler;
private readonly connectionHandler: ConnectionHandler;
private readonly cursorHandler: CursorHandler;
private readonly statusHandler: StatusHandler;
private readonly interactionHandler: InteractionHandler;
constructor(
private readonly jwtVerificationService: JwtVerificationService,
private readonly prisma: PrismaService,
private readonly userSocketService: UserSocketService,
private readonly wsNotificationService: WsNotificationService,
@Inject(REDIS_CLIENT) private readonly redisClient: Redis | null,
@Inject(REDIS_SUBSCRIBER_CLIENT)
private readonly redisSubscriber: Redis | null,
) {
this.broadcaster = new Broadcaster(
this.userSocketService,
this.wsNotificationService,
);
this.redisHandler = new RedisHandler(this.wsNotificationService);
this.connectionHandler = new ConnectionHandler(
this.jwtVerificationService,
this.prisma,
this.userSocketService,
this.wsNotificationService,
this.logger,
);
this.cursorHandler = new CursorHandler(this.broadcaster, this.throttler);
this.statusHandler = new StatusHandler(this.broadcaster, this.throttler);
this.interactionHandler = new InteractionHandler(
this.prisma,
this.userSocketService,
this.wsNotificationService,
);
// Setup Redis subscription for cross-instance communication
if (this.redisSubscriber) {
this.redisSubscriber
.subscribe(
REDIS_CHANNEL.ACTIVE_DOLL_UPDATE,
REDIS_CHANNEL.FRIEND_CACHE_UPDATE,
(err) => {
if (err) {
this.logger.error(`Failed to subscribe to Redis channels`, err);
} else {
this.logger.log(`Subscribed to Redis channels`);
}
},
)
.catch((err) => {
this.logger.error(`Error subscribing to Redis channels`, err);
});
this.redisSubscriber.on('message', (channel, message) => {
if (channel === REDIS_CHANNEL.ACTIVE_DOLL_UPDATE) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.redisHandler.handleActiveDollUpdateMessage(message);
} else if (channel === REDIS_CHANNEL.FRIEND_CACHE_UPDATE) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.redisHandler.handleFriendCacheUpdateMessage(message);
}
});
}
}
afterInit() {
this.logger.log('Initialized');
this.wsNotificationService.setIo(this.io);
}
handleConnection(client: AuthenticatedSocket) {
this.connectionHandler.handleConnection(client);
}
@SubscribeMessage(WS_EVENT.CLIENT_INITIALIZE)
async handleClientInitialize(client: AuthenticatedSocket) {
await this.connectionHandler.handleClientInitialize(client);
}
async handleDisconnect(client: AuthenticatedSocket) {
await this.connectionHandler.handleDisconnect(client);
// Remove from throttler
const userId = client.data.userId;
if (userId) {
this.throttler.remove(userId);
}
}
async isUserOnline(userId: string): Promise<boolean> {
return this.userSocketService.isUserOnline(userId);
}
@SubscribeMessage(WS_EVENT.CURSOR_REPORT_POSITION)
async handleCursorReportPosition(
client: AuthenticatedSocket,
data: CursorPositionDto,
) {
await this.cursorHandler.handleCursorReportPosition(client, data);
}
@SubscribeMessage(WS_EVENT.CLIENT_REPORT_USER_STATUS)
async handleClientReportUserStatus(
client: AuthenticatedSocket,
data: UserStatusDto,
) {
await this.statusHandler.handleClientReportUserStatus(client, data);
}
async handleSendInteraction(
client: AuthenticatedSocket,
data: SendInteractionDto,
) {
await this.interactionHandler.handleSendInteraction(client, data);
}
onModuleDestroy() {
if (this.redisSubscriber) {
this.redisSubscriber.removeAllListeners('message');
}
}
}