UNPKG

@mos-connection/connector

Version:
484 lines 19.9 kB
"use strict"; 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