@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
JavaScript
"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