import { ICommunicationManager, IConnection, IMessageConfiguration, IMessageEvent } from '@nitrots/api'; import { GetConfiguration } from '@nitrots/configuration'; import { GetEventDispatcher, NitroEvent, NitroEventType } from '@nitrots/events'; import { GetTickerTime, NitroLogger } from '@nitrots/utils'; import { NitroMessages } from './NitroMessages'; import { SocketConnection } from './SocketConnection'; import { AuthenticatedEvent, ClientHelloMessageComposer, ClientPingEvent, InfoRetrieveMessageComposer, PongMessageComposer, SSOTicketMessageComposer, UniqueIDMessageComposer } from './messages'; import { Thumbmark } from '@thumbmarkjs/thumbmarkjs'; export class CommunicationManager implements ICommunicationManager { private _connection: IConnection = new SocketConnection(); private _messages: IMessageConfiguration = new NitroMessages(); private _pongInterval: any = null; private _messageEvents: IMessageEvent[] = []; private _socketClosedCallback: () => void = null; private _socketOpenedCallback: () => void = null; private _socketErrorCallback: () => void = null; private _socketReconnectedCallback: () => void = null; private _machineIdPromise: Promise = null; private _initResolved: boolean = false; private async generateMachineID(): Promise { try { const result = await new Thumbmark().get(); return result.thumbmark ? `IID-${result.thumbmark}` : 'FAILED'; } catch(error) { NitroLogger.warn('[CommunicationManager] Failed to generate machine ID', error); return 'FAILED'; } } private async sendHandshake(): Promise { if(!this._machineIdPromise) this._machineIdPromise = this.generateMachineID(); const machineId = await this._machineIdPromise; this._connection.send(new ClientHelloMessageComposer(null, null, null, null)); this._connection.send(new SSOTicketMessageComposer(GetConfiguration().getValue('sso.ticket', null), GetTickerTime())); this._connection.send(new UniqueIDMessageComposer(machineId, '', '')); } constructor() { this._connection.registerMessages(this._messages); } public async init(): Promise { // Store callback for cleanup this._socketClosedCallback = () => { this.stopPong(); }; GetEventDispatcher().addEventListener(NitroEventType.SOCKET_CLOSED, this._socketClosedCallback); // Handle reconnection - re-authenticate when socket reconnects this._socketReconnectedCallback = () => { NitroLogger.log('[CommunicationManager] Socket reconnected, re-authenticating...'); if(GetConfiguration().getValue('system.pong.manually', false)) this.startPong(); void this.sendHandshake(); }; GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECTED, this._socketReconnectedCallback); return new Promise((resolve, reject) => { // Store callback for cleanup this._socketOpenedCallback = () => { if(GetConfiguration().getValue('system.pong.manually', false)) this.startPong(); void this.sendHandshake(); }; GetEventDispatcher().addEventListener(NitroEventType.SOCKET_OPENED, this._socketOpenedCallback); // Store callback for cleanup this._socketErrorCallback = () => { if(!this._initResolved) reject(); }; GetEventDispatcher().addEventListener(NitroEventType.SOCKET_ERROR, this._socketErrorCallback); // Store message events for cleanup const pingEvent = new ClientPingEvent((event: ClientPingEvent) => this.sendPong()); const authEvent = new AuthenticatedEvent((event: AuthenticatedEvent) => { const isReconnect = this._initResolved; NitroLogger.log('[CommunicationManager] AuthenticatedEvent received (isReconnect=' + isReconnect + ')'); this._connection.authenticated(); if(!this._initResolved) { this._initResolved = true; resolve(); } if(isReconnect) { this._connection.ready(); } event.connection.send(new InfoRetrieveMessageComposer()); if(isReconnect) { NitroLogger.log('[CommunicationManager] Dispatching SOCKET_REAUTHENTICATED'); GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_REAUTHENTICATED)); } }); this._messageEvents.push(pingEvent, authEvent); this._connection.addMessageEvent(pingEvent); this._connection.addMessageEvent(authEvent); this._connection.init(GetConfiguration().getValue('socket.url')); }); } public dispose(): void { // Stop pong interval this.stopPong(); // Remove event dispatcher listeners if(this._socketClosedCallback) { GetEventDispatcher().removeEventListener(NitroEventType.SOCKET_CLOSED, this._socketClosedCallback); this._socketClosedCallback = null; } if(this._socketOpenedCallback) { GetEventDispatcher().removeEventListener(NitroEventType.SOCKET_OPENED, this._socketOpenedCallback); this._socketOpenedCallback = null; } if(this._socketErrorCallback) { GetEventDispatcher().removeEventListener(NitroEventType.SOCKET_ERROR, this._socketErrorCallback); this._socketErrorCallback = null; } if(this._socketReconnectedCallback) { GetEventDispatcher().removeEventListener(NitroEventType.SOCKET_RECONNECTED, this._socketReconnectedCallback); this._socketReconnectedCallback = null; } // Remove message events for(const event of this._messageEvents) { this._connection.removeMessageEvent(event); } this._messageEvents = []; } protected startPong(): void { if(this._pongInterval) this.stopPong(); this._pongInterval = setInterval(() => this.sendPong(), GetConfiguration().getValue('system.pong.interval.ms', 20000)); } protected stopPong(): void { if(!this._pongInterval) return; clearInterval(this._pongInterval); this._pongInterval = null; } protected sendPong(): void { this._connection?.send(new PongMessageComposer()); } public registerMessageEvent(event: IMessageEvent): IMessageEvent { if(this._connection) this._connection.addMessageEvent(event); return event; } public removeMessageEvent(event: IMessageEvent): void { if(!this._connection) return; this._connection.removeMessageEvent(event); } public subscribeMessage(eventCtor: new (callback: (event: T) => void) => T, handler: (event: T) => void): () => void { if(!eventCtor || !handler) return () => {}; const event = new eventCtor(handler); this.registerMessageEvent(event); return () => this.removeMessageEvent(event); } public get connection(): IConnection { return this._connection; } }