import fs from 'fs' import { Server as HTTPServer } from 'http' import { pathToFileURL } from 'url' import { SocketEvent } from '@/application/enums' import Logger, { LoggerType } from '@/application/logger' import Storage from '@/application/storage' import type { TSocket, UUID } from '@/application/types' import { Authentication } from '@/middleware/authentication' import { Server as SocketServer } from 'socket.io' class SocketManager { private io: SocketServer | null = null private readonly logger = Logger.type(LoggerType.APP) /** * Initialize Socket.IO server */ public async boot(http: HTTPServer): Promise { this.io = new SocketServer(http) // Apply authentication middleware this.io.use(Authentication) // Set up connection handler this.io.on(SocketEvent.CONNECTION, this.handleConnection.bind(this)) } /** * Get Socket.IO server instance */ public getIO(): SocketServer { if (!this.io) { throw new Error('Socket.IO has not been initialized') } return this.io } /** * Handle socket connection */ private async handleConnection(socket: TSocket): Promise { try { await this.loadEventHandlers('events', '', socket) } catch (error: any) { this.logger.error(`Failed to load event handlers: ${error.message}`) } } /** * Load event handlers recursively from the events directory */ private async loadEventHandlers(baseDir: string, subDir: string, socket: TSocket): Promise { try { const fullDir = Storage.getAppPath(baseDir, subDir) const files = await fs.promises.readdir(fullDir, { withFileTypes: true }) for (const file of files) { const filePath = Storage.getAppPath(baseDir, subDir, file.name) if (file.isDirectory()) { await this.loadEventHandlers(baseDir, `${subDir}/${file.name}`, socket) continue } if (!file.isFile() || (!file.name.endsWith('.ts') && !file.name.endsWith('.js'))) { continue } try { const module = await import(pathToFileURL(filePath).href) if (typeof module.default !== 'function') { this.logger.warn(`Unrecognized export in ${file.name}`) continue } const EventClass = module.default const eventInstance = new EventClass(this.io, socket) eventInstance.listen() } catch (error) { this.logger.error(`Error loading event handler ${file.name}: ${error instanceof Error ? error.message : String(error)}`) } } } catch (error) { this.logger.error(`Error reading directory: ${error instanceof Error ? error.message : String(error)}`) } } /** * Emit event to all connected clients */ public emit(event: string, ...args: any[]): void { this.getIO().emit(event, ...args) } /** * Emit event to specific room */ public emitToRoom(room: string, event: string, ...args: any[]): void { this.getIO() .to(room) .emit(event, ...args) } public getSocketByUserId(userId: UUID): TSocket | undefined { const sockets = Array.from(this.getIO().sockets.sockets.values()) return sockets.find((socket: TSocket) => socket.userId === userId) } public getSocketByCharacterId(characterId: UUID): TSocket | undefined { const sockets = Array.from(this.getIO().sockets.sockets.values()) return sockets.find((socket: TSocket) => socket.characterId === characterId) } } export default new SocketManager()