@mos-connection/connector
Version:
MOS compliant TCP/IP Socket connection.
484 lines • 19.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MosSocketClient = void 0;
const eventemitter3_1 = require("eventemitter3");
const net_1 = require("net");
const socketConnection_1 = require("./socketConnection");
const helper_1 = require("@mos-connection/helper");
const NCSServerConnection_1 = require("./NCSServerConnection");
const iconv = require("iconv-lite");
const mosMessageParser_1 = require("./mosMessageParser");
class MosSocketClient extends eventemitter3_1.EventEmitter {
/** */
constructor(host, port, description, timeout, debug, strict) {
super();
this._autoReconnect = true;
this._reconnectDelay = 3000;
this._reconnectAttempts = 0;
this._shouldBeConnected = false;
this._connected = false;
this._lastConnectionAttempt = 0;
this._reconnectAttempt = 0;
this._queueCallback = {};
this._lingeringCallback = {}; // for lingering messages
this._queueMessages = [];
this._sentMessage = null; // sent message, waiting for reply
this._lingeringMessage = null; // sent message, NOT waiting for reply
// private _readyToSendMessage: boolean = true
this._timedOutCommands = {};
// private _startingUp: boolean = true
this._disposed = false;
this._host = host;
this._port = port;
this._description = description;
this._commandTimeout = timeout ?? NCSServerConnection_1.DEFAULT_COMMAND_TIMEOUT;
this._debug = debug ?? false;
this._strict = strict ?? false;
this.messageParser = new mosMessageParser_1.MosMessageParser(description);
this.messageParser.debug = this._debug;
this.messageParser.on('message', (message, messageString) => {
this._handleMessage(message, messageString);
});
}
/** */
set autoReconnect(autoReconnect) {
this._autoReconnect = autoReconnect;
}
/** */
set autoReconnectInterval(autoReconnectInterval) {
this._reconnectDelay = autoReconnectInterval;
}
/** */
set autoReconnectAttempts(autoReconnectAttempts) {
this._reconnectAttempts = autoReconnectAttempts;
}
/** */
connect() {
// prevent manipulation of active socket
if (!this.connected) {
// throttling attempts
if (!this._lastConnectionAttempt || Date.now() - this._lastConnectionAttempt >= this._reconnectDelay) {
// !_lastReconnectionAttempt (means first attempt) OR time > _reconnectionDelay since last attempt
// recreate client if new attempt:
if (this._client?.connecting) {
this._client.destroy();
this._client.removeAllListeners();
delete this._client;
}
// (re)create client, either on first run or new attempt:
if (!this._client) {
this._client = new net_1.Socket();
this._client.on('close', (hadError) => this._onClose(hadError));
this._client.on('connect', () => this._onConnected());
this._client.on('data', (data) => this._onData(data));
this._client.on('error', (error) => this._onError(error));
}
// connect:
this._client.connect(this._port, this._host);
this._shouldBeConnected = true;
this._lastConnectionAttempt = Date.now();
}
// set timer to retry when needed:
if (!this._connectionAttemptTimer) {
this._connectionAttemptTimer = global.setInterval(() => {
this._autoReconnectionAttempt();
}, this._reconnectDelay);
}
}
}
get isConnected() {
return this._connected;
}
/** */
disconnect() {
this.dispose();
}
queueCommand(message, cb, time) {
message.prepare();
// this.debugTrace('queueing', message.messageID, message.constructor.name )
this._queueCallback[message.messageID + ''] = cb;
this._queueMessages.push({ time: time ?? Date.now(), msg: message });
this.processQueue();
}
processQueue() {
if (this._disposed)
return;
if (!this._sentMessage && this.connected) {
if (this.processQueueTimeout) {
clearTimeout(this.processQueueTimeout);
delete this.processQueueTimeout;
}
const message = this._queueMessages.shift();
if (message) {
// Send the message:
this.executeCommand(message);
}
else {
// The queue is empty, do nothing
}
}
else {
if (!this._sentMessage && this._queueMessages.length > 0) {
const timeSinceQueued = Date.now() - this._queueMessages[0].time;
if (timeSinceQueued > this._commandTimeout) {
const msg = this._queueMessages.shift();
if (msg) {
this._queueCallback[msg.msg.messageID]({
error: `Command timed out in queue after ${timeSinceQueued} ms`,
});
delete this._queueCallback[msg.msg.messageID];
this.processQueue();
}
}
else {
// Try again later:
if (this.processQueueTimeout)
clearTimeout(this.processQueueTimeout);
this.processQueueTimeout = setTimeout(() => {
this.processQueue();
}, 200);
}
}
}
for (const t in this._timedOutCommands) {
if (Number(t) < Date.now() - 3600000) {
delete this._timedOutCommands[t];
}
}
}
/**
* Returns a queue of messages to be executed by a different connection.
* Will exclude hearbeats from the returned queue. The heartbeats must stay inside
* the internal queue because they are needed for the connection lifecycle.
*/
handOverQueue() {
const queuedHeartbeats = this._queueMessages.filter((m) => m.msg instanceof helper_1.MosModel.HeartBeat);
const heartBeatCBs = Object.fromEntries(queuedHeartbeats.map((hb) => [hb.msg.messageID + '', this._queueCallback[hb.msg.messageID]]));
const messages = this._queueMessages.filter((m) => !(m.msg instanceof helper_1.MosModel.HeartBeat));
const callbacks = Object.fromEntries(messages.map((m) => [m.msg.messageID, this._queueCallback[m.msg.messageID]]));
if (this._sentMessage && this._sentMessage.msg instanceof helper_1.MosModel.HeartBeat) {
// Temporary hack, to allow heartbeats to be received after a handover:
this._lingeringMessage = this._sentMessage;
this._lingeringCallback[this._sentMessage.msg.messageID + ''] =
this._queueCallback[this._sentMessage.msg.messageID + ''];
}
else if (this._lingeringMessage) {
delete this._lingeringCallback[this._lingeringMessage.msg.messageID + ''];
this._lingeringMessage = null;
}
this._queueMessages = queuedHeartbeats;
this._queueCallback = heartBeatCBs;
this._sentMessage = null;
if (this.processQueueTimeout && !this._queueMessages.length) {
clearTimeout(this.processQueueTimeout);
delete this.processQueueTimeout;
}
return {
messages,
callbacks,
};
}
/** */
get host() {
if (this._client) {
return this._host;
}
return this._host;
}
/** */
get port() {
if (this._client) {
return this._port;
}
return this._port;
}
/** */
dispose() {
this._disposed = true;
this.messageParser.removeAllListeners();
// this._readyToSendMessage = false
this.connected = false;
this._shouldBeConnected = false;
this._clearConnectionAttemptTimer();
if (this.processQueueTimeout) {
clearTimeout(this.processQueueTimeout);
delete this.processQueueTimeout;
}
if (this.queueCleanupTimeout) {
clearTimeout(this.queueCleanupTimeout);
delete this.queueCleanupTimeout;
}
if (this._sentMessageTimeout) {
clearTimeout(this._sentMessageTimeout);
delete this._sentMessageTimeout;
}
if (this._client) {
const client = this._client;
client.once('close', () => {
this.emit(socketConnection_1.SocketConnectionEvent.DISPOSED);
client.removeAllListeners();
});
client.end();
client.destroy();
delete this._client;
}
}
/**
* convenience wrapper to expose all logging calls to parent object
*/
log(...args) {
this.debugTrace(...args);
}
setDebug(debug) {
this._debug = debug;
this.messageParser.debug = this._debug;
}
/** */
set connected(connected) {
this._connected = connected === true;
this.emit(socketConnection_1.SocketConnectionEvent.CONNECTED);
}
/** */
get connected() {
return this._connected;
}
_sendReply(messageId, response) {
const cb = this._queueCallback[messageId + ''] || this._lingeringCallback[messageId + ''];
if (cb) {
cb(response);
}
else {
// this._onUnhandledCommandTimeout()
this.emit('error', new Error(`Error: No callback found for messageId ${messageId}`));
}
this._sentMessage = null;
if (this._sentMessageTimeout) {
clearTimeout(this._sentMessageTimeout);
delete this._sentMessageTimeout;
}
this._lingeringMessage = null;
delete this._queueCallback[messageId + ''];
delete this._lingeringCallback[messageId + ''];
}
/** */
executeCommand(message, isRetry) {
if (this._sentMessage && !isRetry)
throw Error('executeCommand: there already is a sent Command!');
if (!this._client)
throw Error('executeCommand: No client socket connection set up!');
if (this._sentMessageTimeout)
clearTimeout(this._sentMessageTimeout);
this._sentMessage = message;
this._lingeringMessage = null;
const sentMessageId = message.msg.messageID;
const messageString = message.msg.toString();
const buf = iconv.encode(messageString, 'utf16-be');
const sendTime = Date.now();
// Command timeout:
this._sentMessageTimeout = global.setTimeout(() => {
if (this._disposed)
return;
if (this._sentMessage && this._sentMessage.msg.messageID === sentMessageId) {
this.debugTrace('timeout ' + sentMessageId + ' after ' + this._commandTimeout);
if (isRetry) {
const timeSinceSend = Date.now() - sendTime;
this._sendReply(sentMessageId, {
error: new Error(`Sent command timed out after ${timeSinceSend} ms`),
});
this._timedOutCommands[sentMessageId] = Date.now();
this.processQueue();
}
else {
if (this._client) {
this.executeCommand(message, true);
}
}
}
}, this._commandTimeout);
this._client.write(buf, 'ucs2');
this.emit('rawMessage', 'sent', messageString);
}
/** */
_autoReconnectionAttempt() {
if (this._autoReconnect) {
if (this._reconnectAttempts > -1) {
// no reconnection if no valid reconnectionAttemps is set
if (this._reconnectAttempts > 0 && this._reconnectAttempt >= this._reconnectAttempts) {
// if current attempt is not less than max attempts
// reset reconnection behaviour
this._clearConnectionAttemptTimer();
return;
}
// new attempt if not allready connected
if (!this.connected) {
this._reconnectAttempt++;
this.connect();
}
}
}
}
/** */
_clearConnectionAttemptTimer() {
this._reconnectAttempt = 0;
if (this._connectionAttemptTimer) {
global.clearInterval(this._connectionAttemptTimer);
delete this._connectionAttemptTimer;
}
}
/** */
_onConnected() {
this.emit(socketConnection_1.SocketConnectionEvent.ALIVE);
this._clearConnectionAttemptTimer();
this.connected = true;
}
/** */
_onData(data) {
this.emit(socketConnection_1.SocketConnectionEvent.ALIVE);
const messageString = iconv.decode(data, 'utf16-be');
this.emit('rawMessage', 'recieved', messageString);
try {
this.messageParser.parseMessage(messageString);
}
catch (err) {
this.emit('error', err instanceof Error ? err : new Error(`${err}`));
}
}
_handleMessage(parsedData, messageString) {
const messageId = this._getMessageId(parsedData, messageString);
if (messageId) {
const sentMessage = this._sentMessage || this._lingeringMessage;
if (sentMessage) {
if (sentMessage.msg.messageID.toString() === messageId + '') {
this._sendReply(sentMessage.msg.messageID, { reply: parsedData });
}
else {
this.debugTrace('Mos reply id diff: ' + messageId + ', ' + sentMessage.msg.messageID);
this.debugTrace(parsedData);
this.emit('warning', 'Mos reply id diff: ' + messageId + ', ' + sentMessage.msg.messageID);
this._triggerQueueCleanup();
}
}
else if (this._timedOutCommands[messageId]) {
this.debugTrace(`Got a reply (${messageId}), but command timed out ${Date.now() - this._timedOutCommands[messageId]} ms ago`, messageString);
delete this._timedOutCommands[messageId];
}
else {
// huh, we've got a reply to something we've not sent.
this.debugTrace(`Got a reply (${messageId}), but we haven't sent any message: "${messageString}"`);
this.emit('warning', `Got a reply (${messageId}), but we haven't sent any message: "${messageString}"`);
}
}
else {
// error message?
if (helper_1.MosModel.isXMLObject(parsedData.mos.mosAck) && parsedData.mos.mosAck.status === 'NACK') {
if (this._sentMessage &&
parsedData.mos.mosAck.statusDescription ===
'Buddy server cannot respond because main server is available') {
this._sendReply(this._sentMessage.msg.messageID, { reply: parsedData });
}
else {
this.debugTrace('Mos Error message:' + parsedData.mos.mosAck.statusDescription);
this.emit('error', new Error('Error message: ' + parsedData.mos.mosAck.statusDescription));
}
}
else {
// unknown message..
this.emit('error', new Error('Unknown message: ' + messageString));
}
}
// this._readyToSendMessage = true
this.processQueue();
}
_getMessageId(parsedData, messageString) {
if (typeof parsedData.mos.messageID === 'string' && parsedData.mos.messageID !== '') {
// If there is a messageID:
const messageID = parseInt(`${parsedData.mos.messageID}`);
if (isNaN(messageID)) {
if (this._strict) {
this.debugTrace(`Reply with a bad (NaN) messageId: ${messageString}. Try non-strict mode.`);
return undefined;
}
}
else if (messageID < 1) {
if (this._strict) {
this.debugTrace(`Reply with a bad (<1) messageId: ${messageString}. Try non-strict mode.`);
return undefined;
}
}
else {
return `${messageID}`;
}
}
if (this._strict) {
this.debugTrace(`Reply with no messageId: ${messageString}. Try non-strict mode.`);
return undefined;
}
else {
// In non-strict mode: handle special cases:
// <heartbeat> response doesn't contain messageId (compliant with MOS version 2.8)
// we can assume it's the same as our sent message:
if (this._sentMessage &&
this._sentMessage.msg.toString().search('<heartbeat>') >= 0 &&
parsedData.mos.heartbeat) {
return `${this._sentMessage.msg.messageID}`;
}
// <reqMachInfo> response doesn't contain messageId (compliant with MOS version 2.8)
// we can assume it's the same as our sent message:
if (this._sentMessage &&
this._sentMessage.msg.toString().search('<reqMachInfo/>') >= 0 &&
parsedData.mos.listMachInfo) {
return `${this._sentMessage.msg.messageID}`;
}
else {
this.debugTrace(`Invalid reply with no messageId in non-strict mode: ${messageString}`);
return undefined;
}
}
}
/** */
_onError(error) {
// dispatch error!!!!!
this.emit('error', new Error(`Socket ${this._description} ${this._port} event error: ${error.message}`));
this.debugTrace(`Socket event error: ${error.message}`);
}
/** */
_onClose(hadError) {
this.connected = false;
// this._readyToSendMessage = false
if (hadError) {
this.emit('warning', 'Socket closed with error');
this.debugTrace('Socket closed with error');
}
else {
this.debugTrace('Socket closed without error');
}
this.emit(socketConnection_1.SocketConnectionEvent.DISCONNECTED);
if (this._shouldBeConnected === true) {
this.emit('warning', 'Socket should reconnect');
this.debugTrace('Socket should reconnect');
this.connect();
}
}
_triggerQueueCleanup() {
// in case we're in unsync with messages, prevent deadlock:
this.queueCleanupTimeout = setTimeout(() => {
if (this._disposed)
return;
this.debugTrace('QueueCleanup');
for (let i = this._queueMessages.length - 1; i >= 0; i--) {
const message = this._queueMessages[i];
if (Date.now() - message.time > this._commandTimeout) {
this._sendReply(message.msg.messageID, { error: new Error('Command Timeout') });
this._queueMessages.splice(i, 1);
}
}
}, this._commandTimeout);
}
debugTrace(...args) {
// eslint-disable-next-line no-console
if (this._debug)
console.log(...args);
}
}
exports.MosSocketClient = MosSocketClient;
//# sourceMappingURL=mosSocketClient.js.map