UNPKG

metaapi.cloud-sdk

Version:

SDK for MetaApi, a professional cloud forex API which includes MetaTrader REST API and MetaTrader websocket API. Supports both MetaTrader 5 (MT5) and MetaTrader 4 (MT4). CopyFactory copy trading API included. (https://metaapi.cloud)

695 lines (653 loc) 31.6 kB
'use strict'; import TerminalState from './terminalState'; import MemoryHistoryStorage from './memoryHistoryStorage'; import TimeoutError from '../clients/timeoutError'; import randomstring from 'randomstring'; import ConnectionHealthMonitor from './connectionHealthMonitor'; import {ValidationError} from '../clients/errorHandler'; import OptionsValidator from '../clients/optionsValidator'; import LoggerManager from '../logger'; import MetaApiConnection from './metaApiConnection'; /** * Exposes MetaApi MetaTrader streaming API connection to consumers */ export default class StreamingMetaApiConnection extends MetaApiConnection { private _minSubscriptionRefreshInterval: any; private _maxSubscriptionRefreshInterval: any; private _historyStartTime: any; private _terminalHashManager: any; private _terminalState: TerminalState; private _historyStorage: any; private _healthMonitor: ConnectionHealthMonitor; private _subscriptions: {}; private _refreshMarketDataSubscriptionSessions: {}; private _refreshMarketDataSubscriptionTimeouts: {}; private _openedInstances: any[]; /** * Constructs MetaApi MetaTrader streaming Api connection * @param {MetaApiOpts} options metaapi options * @param {MetaApiWebsocketClient} websocketClient MetaApi websocket client * @param {TerminalHashManager} terminalHashManager terminal hash manager * @param {MetatraderAccount} account MetaTrader account id to connect to * @param {HistoryStorage} historyStorage terminal history storage. By default an instance of MemoryHistoryStorage * will be used. * @param {ConnectionRegistry} connectionRegistry metatrader account connection registry * @param {Date} [historyStartTime] history start sync time * @param {RefreshSubscriptionsOpts} [refreshSubscriptionsOpts] subscriptions refresh options */ constructor(options, websocketClient, terminalHashManager, account, historyStorage, connectionRegistry, historyStartTime, refreshSubscriptionsOpts) { super(options, websocketClient, account); refreshSubscriptionsOpts = refreshSubscriptionsOpts || {}; const validator = new OptionsValidator(); this._minSubscriptionRefreshInterval = validator.validateNonZero(refreshSubscriptionsOpts.minDelayInSeconds, 1, 'refreshSubscriptionsOpts.minDelayInSeconds'); this._maxSubscriptionRefreshInterval = validator.validateNonZero(refreshSubscriptionsOpts.maxDelayInSeconds, 600, 'refreshSubscriptionsOpts.maxDelayInSeconds'); this._connectionRegistry = connectionRegistry; this._historyStartTime = historyStartTime; this._terminalHashManager = terminalHashManager; this._terminalState = new TerminalState(account, terminalHashManager, this._websocketClient); this._historyStorage = historyStorage || new MemoryHistoryStorage(); this._healthMonitor = new ConnectionHealthMonitor(this); this._websocketClient.addSynchronizationListener(account.id, this); this._websocketClient.addSynchronizationListener(account.id, this._terminalState); this._websocketClient.addSynchronizationListener(account.id, this._historyStorage); this._websocketClient.addSynchronizationListener(account.id, this._healthMonitor); Object.values(account.accountRegions) .forEach(replicaId => this._websocketClient.addReconnectListener(this, replicaId)); this._subscriptions = {}; this._stateByInstanceIndex = {}; this._refreshMarketDataSubscriptionSessions = {}; this._refreshMarketDataSubscriptionTimeouts = {}; this._openedInstances = []; this._logger = LoggerManager.getLogger('MetaApiConnection'); } /** * Opens the connection. Can only be called the first time, next calls will be ignored. * @param {string} instanceId connection instance id * @return {Promise} promise resolving when the connection is opened */ async connect(instanceId) { if (!this._openedInstances.includes(instanceId)) { this._openedInstances.push(instanceId); } if (!this._opened) { this._logger.trace(`${this._account.id}: Opening connection`); this._opened = true; try { this._healthMonitor.start(); await this.initialize(); await this.subscribe(); } catch (err) { await this.close(); throw err; } } } /** * Clears the order and transaction history of a specified application and removes application * @return {Promise} promise resolving when the history is cleared and application is removed */ removeApplication() { this._checkIsConnectionActive(); this._historyStorage.clear(); return this._websocketClient.removeApplication(this._account.id); } /** * Requests the terminal to start synchronization process * (see https://metaapi.cloud/docs/client/websocket/synchronizing/synchronize/) * @param {String} instanceIndex instance index * @returns {Promise} promise which resolves when synchronization started */ async synchronize(instanceIndex) { this._checkIsConnectionActive(); const region = this.getRegion(instanceIndex); const instance = this.getInstanceNumber(instanceIndex); const host = this.getHostName(instanceIndex); let startingHistoryOrderTime = new Date(Math.max( (this._historyStartTime || new Date(0)).getTime(), (await this._historyStorage.lastHistoryOrderTime(instance)).getTime() )); let startingDealTime = new Date(Math.max( (this._historyStartTime || new Date(0)).getTime(), (await this._historyStorage.lastDealTime(instance)).getTime() )); let synchronizationId = randomstring.generate(32); this._getState(instanceIndex).lastSynchronizationId = synchronizationId; const accountId = this._account.accountRegions[region]; this._logger.debug(`${this._account.id}:${instanceIndex}: initiating synchronization ${synchronizationId}`); return this._websocketClient.synchronize(accountId, instance, host, synchronizationId, startingHistoryOrderTime, startingDealTime, this.terminalState.getHashes()); } /** * Initializes meta api connection * @return {Promise} promise which resolves when meta api connection is initialized */ async initialize() { this._checkIsConnectionActive(); await this._historyStorage.initialize(this._account.id, this._connectionRegistry.application); this._websocketClient.addAccountCache(this._account.id, this._account.accountRegions); } /** * Initiates subscription to MetaTrader terminal * @returns {Promise} promise which resolves when subscription is initiated */ async subscribe() { this._checkIsConnectionActive(); const accountRegions = this._account.accountRegions; Object.entries(accountRegions).forEach(([region, replicaId]) => { if (!this._options.region || this._options.region === region) { this._websocketClient.ensureSubscribe(replicaId, 0); this._websocketClient.ensureSubscribe(replicaId, 1); } }); } /** * Subscribes on market data of specified symbol (see * https://metaapi.cloud/docs/client/websocket/marketDataStreaming/subscribeToMarketData/). * @param {String} symbol symbol (e.g. currency pair or an index) * @param {Array<MarketDataSubscription>} subscriptions array of market data subscription to create or update. Please * note that this feature is not fully implemented on server-side yet * @param {number} [timeoutInSeconds] timeout to wait for prices in seconds, default is 30 * @param {boolean} [waitForQuote] if set to false, the method will resolve without waiting for the first quote to * arrive. Default is to wait for quote if quotes subscription is requested. * @returns {Promise} promise which resolves when subscription request was processed */ async subscribeToMarketData(symbol, subscriptions, timeoutInSeconds?, waitForQuote = true) { this._checkIsConnectionActive(); if (!this._terminalState.specification(symbol)) { throw new ValidationError(`${this._account.id}: Cannot subscribe to market data for symbol ${symbol} because ` + 'symbol does not exist'); } else { subscriptions = subscriptions || [{type: 'quotes'}]; if (this._subscriptions[symbol]) { const prevSubscriptions = this._subscriptions[symbol].subscriptions; subscriptions.forEach(subscription => { const index = subscription.type === 'candles' ? prevSubscriptions.findIndex(item => item.type === subscription.type && item.timeframe === subscription.timeframe) : prevSubscriptions.findIndex(item => item.type === subscription.type); if (index === -1) { prevSubscriptions.push(subscription); } else { prevSubscriptions[index] = subscription; } }); } else { this._subscriptions[symbol] = {subscriptions}; } await this._websocketClient.subscribeToMarketData(this._account.id, symbol, subscriptions, this._account.reliability); if (waitForQuote !== false && subscriptions.find(s => s.type === 'quotes')) { return this.terminalState.waitForPrice(symbol, timeoutInSeconds); } } } /** * Unsubscribes from market data of specified symbol (see * https://metaapi.cloud/docs/client/websocket/marketDataStreaming/unsubscribeFromMarketData/). * @param {String} symbol symbol (e.g. currency pair or an index) * @param {Array<MarketDataUnsubscription>} unsubscriptions array of subscriptions to cancel * @returns {Promise} promise which resolves when unsubscription request was processed */ unsubscribeFromMarketData(symbol, unsubscriptions) { this._checkIsConnectionActive(); if (!unsubscriptions) { delete this._subscriptions[symbol]; } else if (this._subscriptions[symbol]) { this._subscriptions[symbol].subscriptions = this._subscriptions[symbol].subscriptions.filter(subscription => { return !unsubscriptions.find(unsubscription => subscription.type === unsubscription.type && (!unsubscription.timeframe || subscription.timeframe === unsubscription.timeframe)); }); if (!this._subscriptions[symbol].subscriptions.length) { delete this._subscriptions[symbol]; } } return this._websocketClient.unsubscribeFromMarketData(this._account.id, symbol, unsubscriptions, this._account.reliability); } /** * Invoked when subscription downgrade has occurred * @param {String} instanceIndex index of an account instance connected * @param {string} symbol symbol to update subscriptions for * @param {Array<MarketDataSubscription>} updates array of market data subscription to update * @param {Array<MarketDataUnsubscription>} unsubscriptions array of subscriptions to cancel * @return {Promise} promise which resolves when the asynchronous event is processed */ // eslint-disable-next-line complexity async onSubscriptionDowngraded(instanceIndex, symbol, updates, unsubscriptions) { if (unsubscriptions?.length) { this.unsubscribeFromMarketData(symbol, unsubscriptions).catch(err => { let method = err.name !== 'ValidationError' ? 'error' : 'trace'; this._logger[method](`${this._account.id}: failed do unsubscribe from market data on subscription downgraded`, err); }); } if (updates?.length) { this.subscribeToMarketData(symbol, updates).catch(err => { this._logger.error(`${this._account.id}: failed do subscribe from market data on subscription downgraded`, err); }); } } /** * Returns list of the symbols connection is subscribed to * @returns {Array<String>} list of the symbols connection is subscribed to */ get subscribedSymbols() { return Object.keys(this._subscriptions); } /** * Returns subscriptions for a symbol * @param {string} symbol symbol to retrieve subscriptions for * @returns {Array<MarketDataSubscription>} list of market data subscriptions for the symbol */ subscriptions(symbol) { this._checkIsConnectionActive(); return (this._subscriptions[symbol] || {}).subscriptions; } /** * Returns local copy of terminal state * @returns {TerminalState} local copy of terminal state */ get terminalState() { return this._terminalState; } /** * Returns local history storage * @returns {HistoryStorage} local history storage */ get historyStorage() { return this._historyStorage; } /** * Invoked when connection to MetaTrader terminal established * @param {String} instanceIndex index of an account instance connected * @param {Number} replicas number of account replicas launched * @return {Promise} promise which resolves when the asynchronous event is processed */ async onConnected(instanceIndex, replicas) { let key = randomstring.generate(32); let state = this._getState(instanceIndex); const region = this.getRegion(instanceIndex); this.cancelRefresh(region); await this._terminalHashManager.refreshIgnoredFieldLists(region); state.shouldSynchronize = key; state.synchronizationRetryIntervalInSeconds = 1; state.synchronized = false; this._ensureSynchronized(instanceIndex, key); this._logger.debug(`${this._account.id}:${instanceIndex}: connected to broker`); } /** * Invoked when connection to MetaTrader terminal terminated * @param {String} instanceIndex index of an account instance connected */ async onDisconnected(instanceIndex) { let state = this._getState(instanceIndex); state.lastDisconnectedSynchronizationId = state.lastSynchronizationId; state.lastSynchronizationId = undefined; state.shouldSynchronize = undefined; state.synchronized = false; state.disconnected = true; const instanceNumber = this.getInstanceNumber(instanceIndex); const region = this.getRegion(instanceIndex); const instance = `${region}:${instanceNumber}`; delete this._refreshMarketDataSubscriptionSessions[instance]; clearTimeout(this._refreshMarketDataSubscriptionTimeouts[instance]); delete this._refreshMarketDataSubscriptionTimeouts[instance]; clearTimeout(state.synchronizationTimeout); delete state.synchronizationTimeout; clearTimeout(state.ensureSynchronizeTimeout); delete state.ensureSynchronizeTimeout; this._logger.debug(`${this._account.id}:${instanceIndex}: disconnected from broker`); } /** * Invoked when a symbol specifications were updated * @param {String} instanceIndex index of account instance connected * @param {Array<MetatraderSymbolSpecification>} specifications updated specifications * @param {Array<String>} removedSymbols removed symbols */ async onSymbolSpecificationsUpdated(instanceIndex, specifications, removedSymbols) { this._scheduleSynchronizationTimeout(instanceIndex); } /** * Invoked when position synchronization finished to indicate progress of an initial terminal state synchronization * @param {string} instanceIndex index of an account instance connected * @param {String} synchronizationId synchronization request id */ async onPositionsSynchronized(instanceIndex, synchronizationId) { this._scheduleSynchronizationTimeout(instanceIndex); } /** * Invoked when pending order synchronization fnished to indicate progress of an initial terminal state * synchronization * @param {string} instanceIndex index of an account instance connected * @param {String} synchronizationId synchronization request id */ async onPendingOrdersSynchronized(instanceIndex, synchronizationId) { this._scheduleSynchronizationTimeout(instanceIndex); } /** * Invoked when a synchronization of history deals on a MetaTrader account have finished to indicate progress of an * initial terminal state synchronization * @param {String} instanceIndex index of an account instance connected * @param {String} synchronizationId synchronization request id * @return {Promise} promise which resolves when the asynchronous event is processed */ async onDealsSynchronized(instanceIndex, synchronizationId) { let state = this._getState(instanceIndex); state.dealsSynchronized[synchronizationId] = true; this._scheduleSynchronizationTimeout(instanceIndex); this._logger.debug(`${this._account.id}:${instanceIndex}: finished synchronization ${synchronizationId}`); } /** * Invoked when a synchronization of history orders on a MetaTrader account have finished to indicate progress of an * initial terminal state synchronization * @param {String} instanceIndex index of an account instance connected * @param {String} synchronizationId synchronization request id * @return {Promise} promise which resolves when the asynchronous event is processed */ async onHistoryOrdersSynchronized(instanceIndex, synchronizationId) { let state = this._getState(instanceIndex); state.ordersSynchronized[synchronizationId] = true; this._scheduleSynchronizationTimeout(instanceIndex); } /** * Invoked when connection to MetaApi websocket API restored after a disconnect * @param {String} region reconnected region * @param {Number} instanceNumber reconnected instance number * @return {Promise} promise which resolves when connection to MetaApi websocket API restored after a disconnect */ async onReconnected(region, instanceNumber) { const instanceTemplate = `${region}:${instanceNumber}`; Object.keys(this._stateByInstanceIndex) .filter(key => key.startsWith(`${instanceTemplate}:`)).forEach(key => { delete this._stateByInstanceIndex[key]; }); delete this._refreshMarketDataSubscriptionSessions[instanceTemplate]; clearTimeout(this._refreshMarketDataSubscriptionTimeouts[instanceTemplate]); delete this._refreshMarketDataSubscriptionTimeouts[instanceTemplate]; } /** * Invoked when a stream for an instance index is closed * @param {String} instanceIndex index of an account instance connected * @return {Promise} promise which resolves when the asynchronous event is processed */ async onStreamClosed(instanceIndex) { delete this._stateByInstanceIndex[instanceIndex]; } /** * Invoked when MetaTrader terminal state synchronization is started * @param {string} instanceIndex index of an account instance connected * @param {string} specificationsHash specifications hash * @param {string} positionsHash positions hash * @param {string} ordersHash orders hash * @param {string} synchronizationId synchronization id * @return {Promise} promise which resolves when the asynchronous event is processed */ async onSynchronizationStarted(instanceIndex, specificationsHash, positionsHash, ordersHash, synchronizationId) { this._logger.debug(`${this._account.id}:${instanceIndex}: starting synchronization ${synchronizationId}`); const instanceNumber = this.getInstanceNumber(instanceIndex); const region = this.getRegion(instanceIndex); const instance = `${region}:${instanceNumber}`; const accountId = this._account.accountRegions[region]; delete this._refreshMarketDataSubscriptionSessions[instance]; let sessionId = randomstring.generate(32); this._refreshMarketDataSubscriptionSessions[instance] = sessionId; clearTimeout(this._refreshMarketDataSubscriptionTimeouts[instance]); delete this._refreshMarketDataSubscriptionTimeouts[instance]; await this._refreshMarketDataSubscriptions(accountId, instanceNumber, sessionId); this._scheduleSynchronizationTimeout(instanceIndex); let state = this._getState(instanceIndex); if (state && !this._closed) { state.lastSynchronizationId = synchronizationId; } } /** * Invoked when account region has been unsubscribed * @param {String} region account region unsubscribed * @return {Promise} promise which resolves when the asynchronous event is processed */ async onUnsubscribeRegion(region) { Object.keys(this._refreshMarketDataSubscriptionTimeouts) .filter(instance => instance.startsWith(`${region}:`)) .forEach(instance => { clearTimeout(this._refreshMarketDataSubscriptionTimeouts[instance]); delete this._refreshMarketDataSubscriptionTimeouts[instance]; delete this._refreshMarketDataSubscriptionSessions[instance]; }); Object.keys(this._stateByInstanceIndex) .filter(instance => instance.startsWith(`${region}:`)) .forEach(instance => delete this._stateByInstanceIndex[instance]); } /** * Returns flag indicating status of state synchronization with MetaTrader terminal * @param {String} instanceIndex index of an account instance connected * @param {String} synchronizationId optional synchronization request id, last synchronization request id will be used * by default * @return {Promise<Boolean>} promise resolving with a flag indicating status of state synchronization with MetaTrader * terminal */ async isSynchronized(instanceIndex, synchronizationId) { return Object.values<any>(this._stateByInstanceIndex).reduce((acc, s) => { if (instanceIndex !== undefined && s.instanceIndex !== instanceIndex) { return acc; } const checkSynchronizationId = synchronizationId || s.lastSynchronizationId; let synchronized = !!s.ordersSynchronized[checkSynchronizationId] && !!s.dealsSynchronized[checkSynchronizationId]; return acc || synchronized; }, false); } /** * @typedef {Object} SynchronizationOptions * @property {String} [applicationPattern] application regular expression pattern, default is .* * @property {String} [synchronizationId] synchronization id, last synchronization request id will be used by * default * @property {Number} [instanceIndex] index of an account instance to ensure synchronization on, default is to wait * for the first instance to synchronize * @property {Number} [timeoutInSeconds] wait timeout in seconds, default is 5m * @property {Number} [intervalInMilliseconds] interval between account reloads while waiting for a change, default is 1s */ /** * Waits until synchronization to MetaTrader terminal is completed * @param {SynchronizationOptions} opts synchronization options * @return {Promise} promise which resolves when synchronization to MetaTrader terminal is completed * @throws {TimeoutError} if application failed to synchronize with the teminal within timeout allowed */ // eslint-disable-next-line complexity async waitSynchronized(opts) { this._checkIsConnectionActive(); opts = opts || {}; let instanceIndex = opts.instanceIndex; let synchronizationId = opts.synchronizationId; let timeoutInSeconds = opts.timeoutInSeconds || 300; let intervalInMilliseconds = opts.intervalInMilliseconds || 1000; let applicationPattern = opts.applicationPattern || ((this._account as any).application === 'CopyFactory' ? 'CopyFactory.*|RPC' : 'RPC'); let startTime = Date.now(); let synchronized; while (!(synchronized = await this.isSynchronized(instanceIndex, synchronizationId)) && (startTime + timeoutInSeconds * 1000) > Date.now()) { await new Promise(res => setTimeout(res, intervalInMilliseconds)); } let state; if (instanceIndex === undefined) { for (let s of Object.values<any>(this._stateByInstanceIndex)) { if (await this.isSynchronized(s.instanceIndex, synchronizationId)) { state = s; instanceIndex = s.instanceIndex; } } } else { state = Object.values<any>(this._stateByInstanceIndex).find(s => s.instanceIndex === instanceIndex); } if (!synchronized) { throw new TimeoutError('Timed out waiting for MetaApi to synchronize to MetaTrader account ' + this._account.id + ', synchronization id ' + (synchronizationId || (state && state.lastSynchronizationId) || (state && state.lastDisconnectedSynchronizationId))); } let timeLeftInSeconds = Math.max(0, timeoutInSeconds - (Date.now() - startTime) / 1000); const region = this.getRegion(state.instanceIndex); const accountId = this._account.accountRegions[region]; await this._websocketClient.waitSynchronized(accountId, this.getInstanceNumber(instanceIndex), applicationPattern, timeLeftInSeconds); } /** * Closes the connection. The instance of the class should no longer be used after this method is invoked. * @param {string} instanceId connection instance id */ async close(instanceId?) { if (this._opened) { this._openedInstances = this._openedInstances.filter(id => id !== instanceId); if (!this._openedInstances.length && !this._closed) { this._logger.debug(`${this._account.id}: Closing connection`); Object.values<any>(this._stateByInstanceIndex).forEach(state => clearTimeout(state.synchronizationTimeout)); this._stateByInstanceIndex = {}; await this._connectionRegistry.removeStreaming(this._account); this._terminalState.close(); const accountRegions = this._account.accountRegions; this._websocketClient.removeSynchronizationListener(this._account.id, this); this._websocketClient.removeSynchronizationListener(this._account.id, this._terminalState); this._websocketClient.removeSynchronizationListener(this._account.id, this._historyStorage); this._websocketClient.removeSynchronizationListener(this._account.id, this._healthMonitor); this._websocketClient.removeReconnectListener(this); this._healthMonitor.stop(); this._refreshMarketDataSubscriptionSessions = {}; Object.values<any>(this._refreshMarketDataSubscriptionTimeouts).forEach(timeout => clearTimeout(timeout)); this._refreshMarketDataSubscriptionTimeouts = {}; Object.values(accountRegions).forEach(replicaId => this._websocketClient.removeAccountCache(replicaId)); this._closed = true; this._logger.trace(`${this._account.id}: Closed connection`); } } } /** * Returns synchronization status * @return {boolean} synchronization status */ get synchronized() { return Object.values<any>(this._stateByInstanceIndex).reduce((acc, s) => acc || s.synchronized, false); } /** * Returns MetaApi account * @return {MetatraderAccount} MetaApi account */ get account() { return this._account; } /** * Returns connection health monitor instance * @return {ConnectionHealthMonitor} connection health monitor instance */ get healthMonitor() { return this._healthMonitor; } async _refreshMarketDataSubscriptions(accountId, instanceNumber, session) { const region = this._websocketClient.getAccountRegion(accountId); const instance = `${region}:${instanceNumber}`; try { if (this._refreshMarketDataSubscriptionSessions[instance] === session) { const subscriptionsList = []; Object.keys(this._subscriptions).forEach(key => { const subscriptions = this.subscriptions(key); const subscriptionsItem: any = {symbol: key}; if (subscriptions) { subscriptionsItem.subscriptions = subscriptions; } subscriptionsList.push(subscriptionsItem); }); await this._websocketClient.refreshMarketDataSubscriptions(accountId, instanceNumber, subscriptionsList); } } catch (err) { this._logger.error(`Error refreshing market data subscriptions job for account ${this._account.id} ` + `${instanceNumber}`, err); } finally { if (this._refreshMarketDataSubscriptionSessions[instance] === session) { let refreshInterval = (Math.random() * (this._maxSubscriptionRefreshInterval - this._minSubscriptionRefreshInterval) + this._minSubscriptionRefreshInterval) * 1000; this._refreshMarketDataSubscriptionTimeouts[instance] = setTimeout(() => this._refreshMarketDataSubscriptions(accountId, instanceNumber, session), refreshInterval); } } } _generateStopOptions(stopLoss, takeProfit) { let trade: any = {}; if (typeof stopLoss === 'number') { trade.stopLoss = stopLoss; } else if (stopLoss) { trade.stopLoss = stopLoss.value; trade.stopLossUnits = stopLoss.units; } if (typeof takeProfit === 'number') { trade.takeProfit = takeProfit; } else if (takeProfit) { trade.takeProfit = takeProfit.value; trade.takeProfitUnits = takeProfit.units; } return trade; } async _ensureSynchronized(instanceIndex, key) { let state = this._getState(instanceIndex); if (state && state.shouldSynchronize && !this._closed) { try { const synchronizationResult = await this.synchronize(instanceIndex); if (synchronizationResult) { state.synchronized = true; state.synchronizationRetryIntervalInSeconds = 1; delete state.ensureSynchronizeTimeout; } this._scheduleSynchronizationTimeout(instanceIndex); } catch (err) { const level = this._latencyService.getSynchronizedAccountInstances(this._account.id).length ? 'debug' : 'error'; this._logger[level]('MetaApi websocket client for account ' + this._account.id + ':' + instanceIndex + ' failed to synchronize', err); if (state.shouldSynchronize === key) { clearTimeout(state.ensureSynchronizeTimeout); state.ensureSynchronizeTimeout = setTimeout(this._ensureSynchronized.bind(this, instanceIndex, key), state.synchronizationRetryIntervalInSeconds * 1000); state.synchronizationRetryIntervalInSeconds = Math.min(state.synchronizationRetryIntervalInSeconds * 2, 300); } } } } _getState(instanceIndex) { if (!this._stateByInstanceIndex['' + instanceIndex]) { this._stateByInstanceIndex['' + instanceIndex] = { instanceIndex, ordersSynchronized: {}, dealsSynchronized: {}, shouldSynchronize: undefined, synchronizationRetryIntervalInSeconds: 1, synchronized: false, lastDisconnectedSynchronizationId: undefined, lastSynchronizationId: undefined, disconnected: false }; } return this._stateByInstanceIndex['' + instanceIndex]; } _scheduleSynchronizationTimeout(instanceIndex) { let state = this._getState(instanceIndex); if (state && !this._closed) { clearTimeout(state.synchronizationTimeout); state.synchronizationTimeout = setTimeout(() => this._checkSynchronizationTimedOut(instanceIndex), 2 * 60 * 1000); this._logger.debug(`${this._account.id}:${instanceIndex}: scheduled synchronization timeout`); } } _checkSynchronizationTimedOut(instanceIndex) { this._logger.debug(`${this._account.id}:${instanceIndex}: checking if synchronization timed out out`); let state = this._getState(instanceIndex); if (state && !this._closed) { let synchronizationId = state.lastSynchronizationId; let synchronized = !!state.dealsSynchronized[synchronizationId]; if (!synchronized && synchronizationId && state.shouldSynchronize) { this._logger.warn(`${this._account.id}:${instanceIndex}: resynchronized since latest synchronization ` + `${synchronizationId} did not finish in time`); this._ensureSynchronized(instanceIndex, state.shouldSynchronize); } } } }