UNPKG

@sotatech/nest-quickfix

Version:

A powerful NestJS implementation of the FIX (Financial Information eXchange) protocol. Provides high-performance, reliable messaging for financial trading applications with built-in session management, message validation, and recovery mechanisms.

280 lines 10.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Session = void 0; const events_1 = require("events"); const session_state_1 = require("./session.state"); const messages_1 = require("../protocol/messages"); const fields_1 = require("../fields"); const events_constant_1 = require("../constants/events.constant"); const common_1 = require("@nestjs/common"); const fix_date_1 = require("../common/date/fix.date"); class Session extends events_1.EventEmitter { constructor(config, socket, roomManager, sessionManager) { super(); this.socket = socket; this.roomManager = roomManager; this.sessionManager = sessionManager; this.logger = new common_1.Logger(Session.name); this.HEARTBEAT_TIMEOUT = 1.5; this.TEST_REQUEST_TIMEOUT = 2; this.state = session_state_1.SessionState.DISCONNECTED; this.nextOutgoingSeqNum = 1; this.nextExpectedSeqNum = 1; this.lastTestRequestId = null; this.handlers = { logon: new Set(), logout: new Set(), connected: new Set(), disconnected: new Set(), message: new Set(), }; this.config = config; this.sessionId = `${config.senderCompId}->${config.targetCompId}`; this.logger.debug(`Session created: ${this.sessionId}`); this.setupErrorHandling(); this.setupHeartbeat(); } async logon() { if (this.state !== session_state_1.SessionState.CONNECTED) { throw new Error('Session must be in CONNECTED state to logon'); } try { this.state = session_state_1.SessionState.LOGGING_ON; const logon = new messages_1.LogonMessage(this.config.heartbeatInterval, 0, true); await this.sendMessage(logon); this.emit(events_constant_1.SessionEvents.LOGGING_ON); } catch (error) { this.state = session_state_1.SessionState.ERROR; throw error; } } async logout(reason) { try { if (this.state === session_state_1.SessionState.DISCONNECTED) { throw new Error('Session already disconnected'); } this.state = session_state_1.SessionState.LOGGING_OUT; const logout = new messages_1.LogoutMessage(reason); try { await this.sendMessage(logout); } catch (error) { this.logger.warn('Failed to send logout message:', error); } this.emit(events_constant_1.SessionEvents.LOGGING_OUT); this.handleDisconnect(); if (this.socket?.writable) { this.socket.end(() => { this.logger.debug(`Socket closed for session ${this.sessionId}`); }); } this.sessionManager.removeSession(this.sessionId); this.emit(events_constant_1.SessionEvents.LOGGED_OUT); } catch (error) { this.logger.error('Error handling logout:', error); throw error; } } async sendMessage(message) { try { this.addRequiredFields(message); if (!this.socket?.writable) { throw new Error('Socket not writable'); } const reversedMessage = message.createReverse(); const rawMessage = reversedMessage.toString(); await new Promise((resolve, reject) => { this.socket.write(rawMessage, (error) => { error ? reject(error) : resolve(); }); }); common_1.Logger.debug(`[${this.sessionId}] OUT: ${rawMessage.replace(/\x01/g, '|')}`); } catch (error) { this.logger.error('Failed to send message:', error); throw error; } } async handleMessage(message) { try { const msgType = message.getField(fields_1.Fields.MsgType); switch (msgType) { case fields_1.MsgType.Logon: await this.handleLogon(message); break; case fields_1.MsgType.Logout: await this.handleLogout(message); break; case fields_1.MsgType.Heartbeat: this.handleHeartbeat(message); break; case fields_1.MsgType.TestRequest: await this.handleTestRequest(message); break; } for (const { handler, msgType: handlerMsgType } of this.handlers .message) { if (!handlerMsgType || handlerMsgType === msgType) { await handler(this, message); } } this.lastHeartbeatTime = Date.now(); } catch (error) { console.log('Error handling message:', error); this.logger.error('Error handling message:', error); throw error; } } addRequiredFields(message) { const sendingTime = fix_date_1.FixDateFormat.formatDateTime(new Date()); const fields = { [fields_1.Fields.BeginString]: this.config.beginString, [fields_1.Fields.SenderCompID]: this.config.senderCompId, [fields_1.Fields.TargetCompID]: this.config.targetCompId, [fields_1.Fields.SendingTime]: sendingTime, [fields_1.Fields.MsgSeqNum]: this.nextOutgoingSeqNum++, }; Object.entries(fields).forEach(([field, value]) => { if (!message.hasField(Number(field))) { message.setField(Number(field), value); } }); } setupHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); } this.heartbeatTimer = setInterval(async () => { if (this.state === session_state_1.SessionState.LOGGED_ON) { try { await this.sendMessage(new messages_1.HeartbeatMessage()); } catch (error) { this.logger.error('Failed to send heartbeat:', error); } } }, this.config.heartbeatInterval * 1000); } setupErrorHandling() { this.on('error', (error) => { this.logger.error('Session error:', error); }); } async handleLogon(message) { try { this.state = session_state_1.SessionState.LOGGED_ON; await this.sendLogonResponse(); this.handlers.connected.forEach((handler) => handler(this)); this.handlers.logon.forEach((handler) => handler(this, message)); this.emit(events_constant_1.SessionEvents.LOGGED_ON); this.logger.log('Session logged on'); } catch (error) { this.logger.error('Failed to handle logon:', error); this.state = session_state_1.SessionState.ERROR; throw error; } } async handleLogout(message) { try { this.handlers.logout.forEach((handler) => handler(this, message)); this.state = session_state_1.SessionState.DISCONNECTED; this.emit(events_constant_1.SessionEvents.LOGGED_OUT); this.handleDisconnect(); if (this.socket) { this.socket.end(() => { this.logger.debug(`Socket closed for session ${this.sessionId}`); }); } this.sessionManager.removeSession(this.sessionId); } catch (error) { this.logger.error('Error handling logout:', error); throw error; } } handleHeartbeat(message) { this.lastHeartbeatTime = Date.now(); if (message.hasField(fields_1.Fields.TestReqID) && message.getField(fields_1.Fields.TestReqID) === this.lastTestRequestId) { this.clearTestRequestTimer(); } } async handleTestRequest(message) { const testReqId = message.getField(fields_1.Fields.TestReqID); await this.sendMessage(new messages_1.HeartbeatMessage(testReqId)); } async sendLogonResponse() { const response = new messages_1.LogonMessage(this.config.heartbeatInterval, 0, false); await this.sendMessage(response); } clearTestRequestTimer() { if (this.testRequestTimer) { clearTimeout(this.testRequestTimer); this.testRequestTimer = null; this.lastTestRequestId = null; } } handleDisconnect() { this.state = session_state_1.SessionState.DISCONNECTED; this.clearTimers(); this.handlers.disconnected.forEach((handler) => handler(this)); this.emit(events_constant_1.SessionEvents.DISCONNECT); if (this.socket) { this.socket.removeAllListeners(); } } clearTimers() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } if (this.testRequestTimer) { clearTimeout(this.testRequestTimer); this.testRequestTimer = null; } } getSessionId() { return this.sessionId; } getConfig() { return this.config; } getSocket() { return this.socket; } getNextOutgoingSeqNum() { return this.nextOutgoingSeqNum; } join(roomId) { this.logger.debug(`Joining room: ${roomId}`); this.roomManager.join(roomId, this.sessionId); } leave(roomId) { this.logger.debug(`Leaving room: ${roomId}`); this.roomManager.leave(roomId, this.sessionId); } getRooms() { return this.roomManager.getSessionRooms(this.sessionId); } registerLogonHandler(handler) { this.handlers.logon.add(handler); } registerLogoutHandler(handler) { this.handlers.logout.add(handler); } registerConnectedHandler(handler) { this.handlers.connected.add(handler); } registerDisconnectedHandler(handler) { this.handlers.disconnected.add(handler); } registerMessageHandler(handler, msgType) { this.handlers.message.add({ handler, msgType }); } } exports.Session = Session; //# sourceMappingURL=session.js.map