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)

733 lines (732 loc) 121 kB
'use strict'; function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } function _async_to_generator(fn) { return function() { var self = this, args = arguments; return new Promise(function(resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } import randomstring from 'randomstring'; import SynchronizationListener from '../clients/metaApi/synchronizationListener'; import LoggerManager from '../logger'; import { ConditionPromise } from '../helpers/promises'; let TerminalState = class TerminalState extends SynchronizationListener { get id() { return this._id; } /** * Returns true if MetaApi have connected to MetaTrader terminal * @return {Boolean} true if MetaApi have connected to MetaTrader terminal */ get connected() { return Object.values(this._stateByInstanceIndex).reduce((acc, s)=>acc || s.connected, false); } /** * Returns true if MetaApi have connected to MetaTrader terminal and MetaTrader terminal is connected to broker * @return {Boolean} true if MetaApi have connected to MetaTrader terminal and MetaTrader terminal is connected to * broker */ get connectedToBroker() { return Object.values(this._stateByInstanceIndex).reduce((acc, s)=>acc || s.connectedToBroker, false); } /** * Returns a local copy of account information * @returns {MetatraderAccountInformation} local copy of account information */ get accountInformation() { return this._combinedState.accountInformation; } /** * Returns a local copy of MetaTrader positions opened * @returns {Array<MetatraderPosition>} a local copy of MetaTrader positions opened */ get positions() { const hash = this._combinedState.positionsHash; return hash ? Object.values(this._terminalHashManager.getPositionsByHash(hash) || {}) : []; } /** * Returns a local copy of MetaTrader orders opened * @returns {Array<MetatraderOrder>} a local copy of MetaTrader orders opened */ get orders() { const hash = this._combinedState.ordersHash; return hash ? Object.values(this._terminalHashManager.getOrdersByHash(hash) || {}) : []; } /** * Returns a local copy of symbol specifications available in MetaTrader trading terminal * @returns {Array<MetatraderSymbolSpecification>} a local copy of symbol specifications available in MetaTrader * trading terminal */ get specifications() { const hash = this._combinedState.specificationsHash; return hash ? Object.values(this._terminalHashManager.getSpecificationsByHash(this._combinedState.specificationsHash) || {}) : []; } /** * Returns hashes of terminal state data for incremental synchronization * @returns {Promise<Object>} promise resolving with hashes of terminal state data */ // eslint-disable-next-line complexity getHashes() { const specificationsHashes = this._terminalHashManager.getLastUsedSpecificationHashes(this._account.server); const positionsHashes = this._terminalHashManager.getLastUsedPositionHashes(this._account.id); const ordersHashes = this._terminalHashManager.getLastUsedOrderHashes(this._account.id); return { specificationsHashes: specificationsHashes, positionsHashes: positionsHashes, ordersHashes: ordersHashes }; } /** * Returns MetaTrader symbol specification by symbol * @param {String} symbol symbol (e.g. currency pair or an index) * @return {MetatraderSymbolSpecification} MetatraderSymbolSpecification found or undefined if specification for a * symbol is not found */ specification(symbol) { if (this._combinedState.specificationsHash) { const state = this._terminalHashManager.getSpecificationsByHash(this._combinedState.specificationsHash); return state[symbol]; } else { return null; } } /** * Returns MetaTrader symbol price by symbol * @param {String} symbol symbol (e.g. currency pair or an index) * @return {MetatraderSymbolPrice} MetatraderSymbolPrice found or undefined if price for a symbol is not found */ price(symbol) { return this._combinedState.pricesBySymbol[symbol]; } /** * Quote time * @typdef {Object} QuoteTime * @property {Date} time quote time * @property {String} brokerTime quote time in broker timezone, YYYY-MM-DD HH:mm:ss.SSS format */ /** * Returns time of the last received quote * @return {QuoteTime} time of the last received quote */ get lastQuoteTime() { if (this._combinedState.lastQuoteTime) { return { time: this._combinedState.lastQuoteTime, brokerTime: this._combinedState.lastQuoteBrokerTime }; } else { return undefined; } } /** * Waits for price to be received * @param {string} symbol symbol (e.g. currency pair or an index) * @param {number} [timeoutInSeconds] timeout in seconds, default is 30 * @return {Promise<MetatraderSymbolPrice>} promise resolving with price or undefined if price has not been received */ waitForPrice(symbol, timeoutInSeconds = 30) { var _this = this; return _async_to_generator(function*() { _this._waitForPriceResolves[symbol] = _this._waitForPriceResolves[symbol] || []; if (!_this.price(symbol)) { yield Promise.race([ new Promise((res)=>_this._waitForPriceResolves[symbol].push(res)), new Promise((res)=>setTimeout(res, timeoutInSeconds * 1000)) ]); } return _this.price(symbol); })(); } /** * Invoked when connection to MetaTrader terminal established * @param {String} instanceIndex index of an account instance connected */ onConnected(instanceIndex) { this._getState(instanceIndex).connected = true; } /** * Invoked when connection to MetaTrader terminal terminated * @param {String} instanceIndex index of an account instance connected */ onDisconnected(instanceIndex) { let state = this._getState(instanceIndex); state.connected = false; state.connectedToBroker = false; } /** * Invoked when broker connection status have changed * @param {String} instanceIndex index of an account instance connected * @param {Boolean} connected is MetaTrader terminal is connected to broker */ onBrokerConnectionStatusChanged(instanceIndex, connected) { this._combinedState.lastStatusTime = Date.now(); this._getState(instanceIndex).connectedToBroker = connected; } /** * 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 */ onSynchronizationStarted(instanceIndex, specificationsHash, positionsHash, ordersHash, synchronizationId) { const unsynchronizedStates = this._getStateIndicesOfSameInstanceNumber(instanceIndex).filter((stateIndex)=>!this._stateByInstanceIndex[stateIndex].ordersInitialized); unsynchronizedStates.sort((a, b)=>b.lastSyncUpdateTime - a.lastSyncUpdateTime); unsynchronizedStates.slice(1).forEach((stateIndex)=>this._removeState(stateIndex)); let state = this._getState(instanceIndex); state.isSpecificationsExpected = !specificationsHash; state.isPositionsExpected = !positionsHash; state.isOrdersExpected = !ordersHash; state.lastSyncUpdateTime = Date.now(); state.accountInformation = undefined; state.pricesBySymbol = {}; state.positions = []; if (!positionsHash) { state.positionsInitialized = false; state.positionsHash = null; } else { state.positionsHash = positionsHash; } state.orders = []; if (!ordersHash) { state.ordersInitialized = false; state.ordersHash = null; } else { state.ordersHash = ordersHash; } state.specificationsBySymbol = {}; if (!specificationsHash) { this._logger.trace(()=>`${this._account.id}:${instanceIndex}:${synchronizationId}: cleared specifications ` + 'on synchronization start'); state.specificationsHash = null; } else { this._logger.trace(()=>`${this._account.id}:${instanceIndex}:${synchronizationId}: no need to clear ` + `specifications on synchronization start, ${Object.keys(state.specificationsBySymbol || {}).length} ` + 'specifications reused'); state.specificationsHash = specificationsHash; } } /** * Invoked when MetaTrader account information is updated * @param {String} instanceIndex index of an account instance connected * @param {MetatraderAccountInformation} accountInformation updated MetaTrader account information */ onAccountInformationUpdated(instanceIndex, accountInformation) { let state = this._getState(instanceIndex); this._refreshStateUpdateTime(instanceIndex); state.accountInformation = accountInformation; if (accountInformation) { this._combinedState.accountInformation = Object.assign({}, accountInformation); } } /** * Invoked when the positions are replaced as a result of initial terminal state synchronization * @param {String} instanceIndex index of an account instance connected * @param {Array<MetatraderPosition>} positions updated array of positions * @return {Promise} promise which resolves when the asynchronous event is processed */ onPositionsReplaced(instanceIndex, positions) { let state = this._getState(instanceIndex); this._refreshStateUpdateTime(instanceIndex); if (state.isPositionsExpected) { state.positions = positions; } } /** * Invoked when position 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 * @return {Promise} promise which resolves when the asynchronous event is processed */ onPositionsSynchronized(instanceIndex, synchronizationId) { let state = this._getState(instanceIndex); state.positionsInitialized = true; } /** * Invoked when MetaTrader positions are updated * @param {string} instanceIndex index of an account instance connected * @param {MetatraderPosition[]} positions updated MetaTrader positions * @param {string[]} removedPositionIds removed position ids * @return {Promise} promise which resolves when the asynchronous event is processed */ onPositionsUpdated(instanceIndex, positions, removedPositionIds) { var _this = this; return _async_to_generator(function*() { let instanceState = _this._getState(instanceIndex); _this._refreshStateUpdateTime(instanceIndex); const date = Date.now(); removedPositionIds.forEach((id)=>_this._combinedState.removedPositions[id] = date); positions = _this._filterRemovedPositions(positions); Object.keys(_this._combinedState.removedPositions).forEach((id)=>{ if (_this._combinedState.removedPositions[id] < date - 24 * 60 * 60 * 1000) { delete _this._combinedState.removedPositions[id]; } }); if (instanceState.ordersInitialized) { const updatePositions = function() { var _ref = _async_to_generator(function*(state, instance) { const hash = yield _this._terminalHashManager.updatePositions(_this._account.id, _this._account.type, _this._id, instance, positions, removedPositionIds, state.positionsHash); state.positionsHash = hash; }); return function updatePositions(state, instance) { return _ref.apply(this, arguments); }; }(); yield updatePositions(instanceState, instanceIndex); yield updatePositions(_this._combinedState, _this._combinedInstanceIndex); } else { instanceState.positions = instanceState.positions.filter((position)=>!removedPositionIds.includes(position.id)); positions.forEach((position)=>{ let index = instanceState.positions.findIndex((p)=>p.id === position.id); if (index !== -1) { instanceState.positions[index] = position; } else { instanceState.positions.push(position); } }); } })(); } /** * Invoked when the orders are replaced as a result of initial terminal state synchronization * @param {String} instanceIndex index of an account instance connected * @param {Array<MetatraderOrder>} orders updated array of pending orders * @return {Promise} promise which resolves when the asynchronous event is processed */ onPendingOrdersReplaced(instanceIndex, orders) { let state = this._getState(instanceIndex); this._refreshStateUpdateTime(instanceIndex); if (state.isOrdersExpected) { state.orders = orders; } } /** * 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 * @return {Promise} promise which resolves when the asynchronous event is processed */ // eslint-disable-next-line complexity, max-statements onPendingOrdersSynchronized(instanceIndex, synchronizationId) { var _this = this; return _async_to_generator(function*() { let state = _this._getState(instanceIndex); state.positionsInitialized = true; state.ordersInitialized = true; _this._combinedState.accountInformation = state.accountInformation ? Object.assign({}, state.accountInformation) : undefined; state.positions = _this._filterRemovedPositions(state.positions); if (state.positions.length) { const hash = _this._terminalHashManager.recordPositions(_this._account.id, _this._account.type, _this._id, instanceIndex, state.positions); state.positionsHash = hash; _this._combinedState.positions = (state.positions || []).map((p)=>Object.assign({}, p)); _this._combinedState.positionsHash = hash; } else if (state.positionsHash) { _this._terminalHashManager.removePositionReference(_this.id, instanceIndex); _this._terminalHashManager.addPositionReference(state.positionsHash, _this.id, instanceIndex); _this._combinedState.positionsHash = state.positionsHash; _this._terminalHashManager.removePositionReference(_this.id, _this._combinedInstanceIndex); _this._terminalHashManager.addPositionReference(state.positionsHash, _this.id, _this._combinedInstanceIndex); } state.orders = _this._filterRemovedOrders(state.orders); if (state.orders.length) { const hash = _this._terminalHashManager.recordOrders(_this._account.id, _this._account.type, _this._id, instanceIndex, state.orders); state.ordersHash = hash; _this._combinedState.orders = (state.orders || []).map((o)=>Object.assign({}, o)); _this._combinedState.ordersHash = hash; } else if (state.ordersHash) { _this._terminalHashManager.removeOrderReference(_this.id, instanceIndex); _this._terminalHashManager.addOrderReference(state.ordersHash, _this.id, instanceIndex); _this._combinedState.ordersHash = state.ordersHash; _this._terminalHashManager.removeOrderReference(_this.id, _this._combinedInstanceIndex); _this._terminalHashManager.addOrderReference(state.ordersHash, _this.id, _this._combinedInstanceIndex); } _this._logger.trace(()=>`${_this._account.id}:${instanceIndex}:${synchronizationId}: assigned specifications to ` + 'combined state from ' + `${instanceIndex}, ${Object.keys(state.specificationsBySymbol || {}).length} specifications assigned`); _this._combinedState.positionsInitialized = true; _this._combinedState.ordersInitialized = true; if (Object.keys(state.specificationsBySymbol || {}).length) { if (state.isSpecificationsExpected) { const hash = yield _this._terminalHashManager.recordSpecifications(_this._account.server, _this._account.type, _this._id, instanceIndex, Object.values(state.specificationsBySymbol)); _this._combinedState.specificationsHash = hash; state.specificationsHash = hash; state.specificationsBySymbol = null; } else if (state.specificationsHash) { const hash = yield _this._terminalHashManager.updateSpecifications(_this._account.server, _this._account.type, _this._id, instanceIndex, Object.values(state.specificationsBySymbol), [], state.specificationsHash); state.specificationsHash = hash; } } else if (state.specificationsHash) { _this._terminalHashManager.removeSpecificationReference(_this.id, instanceIndex); _this._terminalHashManager.addSpecificationReference(state.specificationsHash, _this.id, instanceIndex); _this._combinedState.specificationsHash = state.specificationsHash; _this._terminalHashManager.removeSpecificationReference(_this.id, _this._combinedInstanceIndex); _this._terminalHashManager.addSpecificationReference(state.specificationsHash, _this.id, _this._combinedInstanceIndex); } for (let stateIndex of _this._getStateIndicesOfSameInstanceNumber(instanceIndex)){ if (!_this._stateByInstanceIndex[stateIndex].connected) { _this._removeState(stateIndex); } } })(); } /** * Invoked when MetaTrader pending orders are updated or completed * @param {string} instanceIndex index of an account instance connected * @param {MetatraderOrder[]} orders updated MetaTrader pending orders * @param {string[]} completedOrderIds completed MetaTrader pending order ids * @return {Promise} promise which resolves when the asynchronous event is processed */ onPendingOrdersUpdated(instanceIndex, orders, completedOrderIds) { var _this = this; return _async_to_generator(function*() { let instanceState = _this._getState(instanceIndex); _this._refreshStateUpdateTime(instanceIndex); const date = Date.now(); completedOrderIds.forEach((id)=>_this._combinedState.completedOrders[id] = date); orders = _this._filterRemovedOrders(orders); Object.keys(_this._combinedState.completedOrders).forEach((id)=>{ if (_this._combinedState.completedOrders[id] < date - 24 * 60 * 60 * 1000) { delete _this._combinedState.completedOrders[id]; } }); if (instanceState.ordersInitialized) { const updatePendingOrders = function() { var _ref = _async_to_generator(function*(state, instance) { const hash = yield _this._terminalHashManager.updateOrders(_this._account.id, _this._account.type, _this._id, instance, orders, completedOrderIds, state.ordersHash); state.ordersHash = hash; }); return function updatePendingOrders(state, instance) { return _ref.apply(this, arguments); }; }(); yield updatePendingOrders(instanceState, instanceIndex); yield updatePendingOrders(_this._combinedState, _this._combinedInstanceIndex); } else { instanceState.orders = instanceState.orders.filter((order)=>!completedOrderIds.includes(order.id)); orders.forEach((order)=>{ let index = instanceState.orders.findIndex((o)=>o.id === order.id); if (index !== -1) { instanceState.orders[index] = order; } else { instanceState.orders.push(order); } }); } })(); } /** * Invoked when a symbol specification was updated * @param {String} instanceIndex index of account instance connected * @param {Array<MetatraderSymbolSpecification>} specifications updated specifications * @param {Array<String>} removedSymbols removed symbols */ onSymbolSpecificationsUpdated(instanceIndex, specifications, removedSymbols) { var _this = this; return _async_to_generator(function*() { let instanceState = _this._getState(instanceIndex); _this._refreshStateUpdateTime(instanceIndex); if (!instanceState.ordersInitialized) { for (let specification of specifications){ instanceState.specificationsBySymbol[specification.symbol] = specification; } } else { const hash = _this._terminalHashManager.updateSpecifications(_this._account.server, _this._account.type, _this._id, instanceIndex, specifications, removedSymbols, instanceState.specificationsHash); instanceState.specificationsHash = hash; const combinedHash = _this._terminalHashManager.updateSpecifications(_this._account.server, _this._account.type, _this._id, _this._combinedInstanceIndex, specifications, removedSymbols, _this._combinedState.specificationsHash); _this._combinedState.specificationsHash = combinedHash; } _this._logger.trace(()=>`${_this._account.id}:${instanceIndex}: updated ${specifications.length} specifications, ` + `removed ${removedSymbols.length} specifications. There are ` + `${Object.keys(instanceState.specificationsBySymbol || {}).length} specifications after update`); })(); } /** * Invoked when prices for several symbols were updated * @param {String} instanceIndex index of an account instance connected * @param {Array<MetatraderSymbolPrice>} prices updated MetaTrader symbol prices * @param {Number} equity account liquidation value * @param {Number} margin margin used * @param {Number} freeMargin free margin * @param {Number} marginLevel margin level calculated as % of equity/margin */ // eslint-disable-next-line complexity onSymbolPricesUpdated(instanceIndex, prices, equity, margin, freeMargin, marginLevel) { let instanceState = this._getState(instanceIndex); this._refreshStateUpdateTime(instanceIndex); // eslint-disable-next-line complexity,max-statements const updateSymbolPrices = (state)=>{ let pricesInitialized = false; let priceUpdated = false; for (let price of prices || []){ let currentPrice = state.pricesBySymbol[price.symbol]; if (currentPrice && currentPrice.time.getTime() > price.time.getTime()) { continue; } else { priceUpdated = true; } if (!state.lastQuoteTime || state.lastQuoteTime.getTime() < price.time.getTime()) { state.lastQuoteTime = price.time; state.lastQuoteBrokerTime = price.brokerTime; } state.pricesBySymbol[price.symbol] = price; const allPositions = Object.values(this._terminalHashManager.getPositionsByHash(state.positionsHash) || {}); const allOrders = Object.values(this._terminalHashManager.getOrdersByHash(state.ordersHash) || {}); let positions = allPositions.filter((p)=>p.symbol === price.symbol); let otherPositions = allPositions.filter((p)=>p.symbol !== price.symbol); let orders = allOrders.filter((o)=>o.symbol === price.symbol); pricesInitialized = true; for (let position of otherPositions){ let p = state.pricesBySymbol[position.symbol]; if (p) { if (position.unrealizedProfit === undefined) { this._updatePositionProfits(position, p); } } else { pricesInitialized = false; } } for (let position of positions){ this._updatePositionProfits(position, price); } for (let order of orders){ order.currentPrice = order.type === 'ORDER_TYPE_BUY' || order.type === 'ORDER_TYPE_BUY_LIMIT' || order.type === 'ORDER_TYPE_BUY_STOP' || order.type === 'ORDER_TYPE_BUY_STOP_LIMIT' ? price.ask : price.bid; } let priceResolves = this._waitForPriceResolves[price.symbol] || []; if (priceResolves.length) { for (let resolve of priceResolves){ resolve(); } delete this._waitForPriceResolves[price.symbol]; } } if (priceUpdated && state.accountInformation) { const positions = Object.values(this._terminalHashManager.getPositionsByHash(state.positionsHash) || {}); if (state.positionsInitialized && pricesInitialized) { if (state.accountInformation.platform === 'mt5') { state.accountInformation.equity = equity !== undefined ? equity : state.accountInformation.balance + positions.reduce((acc, p)=>acc + Math.round((p.unrealizedProfit || 0) * 100) / 100 + Math.round((p.swap || 0) * 100) / 100, 0); } else { state.accountInformation.equity = equity !== undefined ? equity : state.accountInformation.balance + positions.reduce((acc, p)=>acc + Math.round((p.swap || 0) * 100) / 100 + Math.round((p.commission || 0) * 100) / 100 + Math.round((p.unrealizedProfit || 0) * 100) / 100, 0); } state.accountInformation.equity = Math.round(state.accountInformation.equity * 100) / 100; } else { state.accountInformation.equity = equity !== undefined ? equity : state.accountInformation.equity; } var _prices__accountCurrencyExchangeRate; state.accountInformation.accountCurrencyExchangeRate = (_prices__accountCurrencyExchangeRate = prices[0].accountCurrencyExchangeRate) !== null && _prices__accountCurrencyExchangeRate !== void 0 ? _prices__accountCurrencyExchangeRate : state.accountInformation.accountCurrencyExchangeRate; state.accountInformation.margin = margin !== undefined ? margin : state.accountInformation.margin; state.accountInformation.freeMargin = freeMargin !== undefined ? freeMargin : state.accountInformation.freeMargin; state.accountInformation.marginLevel = freeMargin !== undefined ? marginLevel : state.accountInformation.marginLevel; } }; updateSymbolPrices(instanceState); updateSymbolPrices(this._combinedState); for (let price of prices){ for (let call of Object.values(this._processThrottledQuotesCalls)){ var _call_expectedSymbols; this._logger.trace(`${this._account.id}:${instanceIndex}: refreshed ${price.symbol} price`); (_call_expectedSymbols = call.expectedSymbols) === null || _call_expectedSymbols === void 0 ? void 0 : _call_expectedSymbols.delete(price.symbol); call.receivedSymbols.add(price.symbol); call.promise.check(); } } } /** * 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 */ onStreamClosed(instanceIndex) { var _this = this; return _async_to_generator(function*() { if (_this._stateByInstanceIndex[instanceIndex]) { for (let stateIndex of _this._getStateIndicesOfSameInstanceNumber(instanceIndex)){ const instanceState = _this._stateByInstanceIndex[stateIndex]; if (!_this._stateByInstanceIndex[instanceIndex].ordersInitialized && _this._stateByInstanceIndex[instanceIndex].lastSyncUpdateTime <= instanceState.lastSyncUpdateTime) { _this._removeState(instanceIndex); break; } if (instanceState.connected && instanceState.ordersInitialized) { _this._removeState(instanceIndex); break; } } } })(); } /** * Forces refresh of most recent quote updates for symbols subscribed to by the terminal, and waits for them all to * be processed by this terminal state. This method does not waits for all other listeners to receive and process the * quote updates * @param {RefreshTerminalStateOptions} [options] additional options * @returns {Promise} promise resolving when the terminal state received and processed the latest quotes */ refreshTerminalState(options) { var _this = this; return _async_to_generator(function*() { let callData = { receivedSymbols: new Set() }; let callId = randomstring.generate(8); _this._processThrottledQuotesCalls[callId] = callData; callData.promise = new ConditionPromise(()=>callData.expectedSymbols && !callData.expectedSymbols.size); var _options_timeoutInSeconds; callData.promise.timeout(1000 * ((_options_timeoutInSeconds = options === null || options === void 0 ? void 0 : options.timeoutInSeconds) !== null && _options_timeoutInSeconds !== void 0 ? _options_timeoutInSeconds : 10), 'refreshing terminal state timed out'); try { let symbols = yield Promise.race([ _this._websocketClient.refreshTerminalState(_this._account.id), callData.promise // will only throw timeout error at this point ]); _this._logger.debug(`${_this._account.id}: expecting for ${symbols.length ? symbols : 0} symbols to refresh`); let expectedSymbols = new Set(); for (let symbol of symbols){ if (!callData.receivedSymbols.has(symbol)) { expectedSymbols.add(symbol); } } callData.expectedSymbols = expectedSymbols; callData.promise.check(); yield callData.promise; } finally{ delete _this._processThrottledQuotesCalls[callId]; } })(); } /** * Removes connection related data from terminal hash manager */ close() { clearInterval(this._checkCombinedStateActivityJobInterval); Object.keys(this._stateByInstanceIndex).forEach((instanceIndex)=>{ this._removeFromHashManager(instanceIndex); }); this._removeFromHashManager(this._combinedInstanceIndex); } // resets combined state and removes from hash manager if has been disconnected for a long time _checkCombinedStateActivityJob() { if (!this.connectedToBroker && this._combinedState.lastStatusTime < Date.now() - 30 * 60 * 1000) { this._removeFromHashManager(this._combinedInstanceIndex); this._combinedState.accountInformation = undefined; this._combinedState.specificationsBySymbol = null; this._combinedState.pricesBySymbol = {}; this._combinedState.specificationsHash = null; this._combinedState.orders = []; this._combinedState.ordersHash = null; this._combinedState.positions = []; this._combinedState.positionsHash = null; this._combinedState.ordersInitialized = false; this._combinedState.positionsInitialized = false; this._combinedState.lastStatusTime = 0; this._combinedState.lastQuoteTime = undefined; this._combinedState.lastQuoteBrokerTime = undefined; } } _removeState(instanceIndex) { delete this._stateByInstanceIndex[instanceIndex]; this._removeFromHashManager(instanceIndex); } _removeFromHashManager(instanceIndex) { this._terminalHashManager.removeConnectionReferences(this._account.server, this._account.id, this._id, instanceIndex); } _refreshStateUpdateTime(instanceIndex) { const state = this._stateByInstanceIndex[instanceIndex]; if (state && state.ordersInitialized) { state.lastSyncUpdateTime = Date.now(); } } _getStateIndicesOfSameInstanceNumber(instanceIndex) { const region = instanceIndex.split(':')[0]; const instanceNumber = instanceIndex.split(':')[1]; return Object.keys(this._stateByInstanceIndex).filter((stateInstanceIndex)=>stateInstanceIndex.startsWith(`${region}:${instanceNumber}:`) && instanceIndex !== stateInstanceIndex); } // eslint-disable-next-line complexity _updatePositionProfits(position, price) { let specification = this.specification(position.symbol); if (specification) { let multiplier = Math.pow(10, specification.digits); if (position.profit !== undefined) { position.profit = Math.round(position.profit * multiplier) / multiplier; } if (position.unrealizedProfit === undefined || position.realizedProfit === undefined) { position.unrealizedProfit = (position.type === 'POSITION_TYPE_BUY' ? 1 : -1) * (position.currentPrice - position.openPrice) * position.currentTickValue * position.volume / specification.tickSize; position.unrealizedProfit = Math.round(position.unrealizedProfit * multiplier) / multiplier; position.realizedProfit = position.profit - position.unrealizedProfit; } let newPositionPrice = position.type === 'POSITION_TYPE_BUY' ? price.bid : price.ask; let isProfitable = (position.type === 'POSITION_TYPE_BUY' ? 1 : -1) * (newPositionPrice - position.openPrice); let currentTickValue = isProfitable > 0 ? price.profitTickValue : price.lossTickValue; let unrealizedProfit = (position.type === 'POSITION_TYPE_BUY' ? 1 : -1) * (newPositionPrice - position.openPrice) * currentTickValue * position.volume / specification.tickSize; unrealizedProfit = Math.round(unrealizedProfit * multiplier) / multiplier; position.unrealizedProfit = unrealizedProfit; position.profit = position.unrealizedProfit + position.realizedProfit; position.profit = Math.round(position.profit * multiplier) / multiplier; position.currentPrice = newPositionPrice; position.currentTickValue = currentTickValue; } } _filterRemovedPositions(positions) { return positions.filter((position)=>!this._combinedState.removedPositions[position.id]); } _filterRemovedOrders(orders) { return orders.filter((order)=>!this._combinedState.completedOrders[order.id]); } _getState(instanceIndex) { if (!this._stateByInstanceIndex['' + instanceIndex]) { this._logger.trace(`${this._account.id}:${instanceIndex}: constructed new state`); this._stateByInstanceIndex['' + instanceIndex] = this._constructTerminalState(instanceIndex); } return this._stateByInstanceIndex['' + instanceIndex]; } _constructTerminalState(instanceIndex) { return { instanceIndex, connected: false, connectedToBroker: false, accountInformation: undefined, positions: [], orders: [], specificationsBySymbol: {}, pricesBySymbol: {}, ordersInitialized: false, positionsInitialized: false, lastSyncUpdateTime: 0, positionsHash: null, ordersHash: null, specificationsHash: null, isSpecificationsExpected: true, isPositionsExpected: true, isOrdersExpected: true, lastQuoteTime: undefined, lastQuoteBrokerTime: undefined }; } /** * Constructs the instance of terminal state class * @param {MetatraderAccount} account mt account * @param {TerminalHashManager} terminalHashManager terminal hash manager * @param {MetaApiWebsocketClient} websocketClient websocket client */ constructor(account, terminalHashManager, websocketClient){ super(); this._id = randomstring.generate(32); this._account = account; this._terminalHashManager = terminalHashManager; this._websocketClient = websocketClient; this._stateByInstanceIndex = {}; this._waitForPriceResolves = {}; this._combinedInstanceIndex = 'combined'; this._combinedState = { accountInformation: undefined, positions: [], orders: [], specificationsBySymbol: null, pricesBySymbol: {}, removedPositions: {}, completedOrders: {}, specificationsHash: null, positionsHash: null, ordersHash: null, ordersInitialized: false, positionsInitialized: false, lastStatusTime: 0, lastQuoteTime: undefined, lastQuoteBrokerTime: undefined }; this._processThrottledQuotesCalls = {}; this._logger = LoggerManager.getLogger('TerminalState'); this._checkCombinedStateActivityJob = this._checkCombinedStateActivityJob.bind(this); this._checkCombinedStateActivityJobInterval = setInterval(this._checkCombinedStateActivityJob, 5 * 60 * 1000); } }; /** * Responsible for storing a local copy of remote terminal state */ export { TerminalState as default }; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIjxhbm9uPiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbmltcG9ydCByYW5kb21zdHJpbmcgZnJvbSAncmFuZG9tc3RyaW5nJztcbmltcG9ydCBTeW5jaHJvbml6YXRpb25MaXN0ZW5lciBmcm9tICcuLi9jbGllbnRzL21ldGFBcGkvc3luY2hyb25pemF0aW9uTGlzdGVuZXInO1xuaW1wb3J0IE1ldGFBcGlXZWJzb2NrZXRDbGllbnQgZnJvbSAnLi4vY2xpZW50cy9tZXRhQXBpL21ldGFBcGlXZWJzb2NrZXQuY2xpZW50JztcbmltcG9ydCBMb2dnZXJNYW5hZ2VyIGZyb20gJy4uL2xvZ2dlcic7XG5pbXBvcnQgVGVybWluYWxIYXNoTWFuYWdlciBmcm9tICcuL3Rlcm1pbmFsSGFzaE1hbmFnZXInO1xuaW1wb3J0IE1ldGF0cmFkZXJBY2NvdW50IGZyb20gJy4vbWV0YXRyYWRlckFjY291bnQnO1xuaW1wb3J0IHtDb25kaXRpb25Qcm9taXNlfSBmcm9tICcuLi9oZWxwZXJzL3Byb21pc2VzJztcblxuLyoqXG4gKiBSZXNwb25zaWJsZSBmb3Igc3RvcmluZyBhIGxvY2FsIGNvcHkgb2YgcmVtb3RlIHRlcm1pbmFsIHN0YXRlXG4gKi9cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIFRlcm1pbmFsU3RhdGUgZXh0ZW5kcyBTeW5jaHJvbml6YXRpb25MaXN0ZW5lciB7XG5cbiAgLyoqXG4gICAqIENvbnN0cnVjdHMgdGhlIGluc3RhbmNlIG9mIHRlcm1pbmFsIHN0YXRlIGNsYXNzXG4gICAqIEBwYXJhbSB7TWV0YXRyYWRlckFjY291bnR9IGFjY291bnQgbXQgYWNjb3VudFxuICAgKiBAcGFyYW0ge1Rlcm1pbmFsSGFzaE1hbmFnZXJ9IHRlcm1pbmFsSGFzaE1hbmFnZXIgdGVybWluYWwgaGFzaCBtYW5hZ2VyXG4gICAqIEBwYXJhbSB7TWV0YUFwaVdlYnNvY2tldENsaWVudH0gd2Vic29ja2V0Q2xpZW50IHdlYnNvY2tldCBjbGllbnRcbiAgICovXG4gIGNvbnN0cnVjdG9yKGFjY291bnQsIHRlcm1pbmFsSGFzaE1hbmFnZXIsIHdlYnNvY2tldENsaWVudCkge1xuICAgIHN1cGVyKCk7XG4gICAgdGhpcy5faWQgPSByYW5kb21zdHJpbmcuZ2VuZXJhdGUoMzIpO1xuICAgIHRoaXMuX2FjY291bnQgPSBhY2NvdW50O1xuICAgIHRoaXMuX3Rlcm1pbmFsSGFzaE1hbmFnZXIgPSB0ZXJtaW5hbEhhc2hNYW5hZ2VyO1xuICAgIHRoaXMuX3dlYnNvY2tldENsaWVudCA9IHdlYnNvY2tldENsaWVudDtcbiAgICB0aGlzLl9zdGF0ZUJ5SW5zdGFuY2VJbmRleCA9IHt9O1xuICAgIHRoaXMuX3dhaXRGb3JQcmljZVJlc29sdmVzID0ge307XG4gICAgdGhpcy5fY29tYmluZWRJbnN0YW5jZUluZGV4ID0gJ2NvbWJpbmVkJztcbiAgICB0aGlzLl9jb21iaW5lZFN0YXRlID0ge1xuICAgICAgYWNjb3VudEluZm9ybWF0aW9uOiB1bmRlZmluZWQsXG4gICAgICBwb3NpdGlvbnM6IFtdLFxuICAgICAgb3JkZXJzOiBbXSxcbiAgICAgIHNwZWNpZmljYXRpb25zQnlTeW1ib2w6IG51bGwsXG4gICAgICBwcmljZXNCeVN5bWJvbDoge30sXG4gICAgICByZW1vdmVkUG9zaXRpb25zOiB7fSxcbiAgICAgIGNvbXBsZXRlZE9yZGVyczoge30sXG4gICAgICBzcGVjaWZpY2F0aW9uc0hhc2g6IG51bGwsXG4gICAgICBwb3NpdGlvbnNIYXNoOiBudWxsLFxuICAgICAgb3JkZXJzSGFzaDogbnVsbCxcbiAgICAgIG9yZGVyc0luaXRpYWxpemVkOiBmYWxzZSxcbiAgICAgIHBvc2l0aW9uc0luaXRpYWxpemVkOiBmYWxzZSxcbiAgICAgIGxhc3RTdGF0dXNUaW1lOiAwLFxuICAgICAgbGFzdFF1b3RlVGltZTogdW5kZWZpbmVkLFxuICAgICAgbGFzdFF1b3RlQnJva2VyVGltZTogdW5kZWZpbmVkXG4gICAgfTtcbiAgICB0aGlzLl9wcm9jZXNzVGhyb3R0bGVkUXVvdGVzQ2FsbHMgPSB7fTtcbiAgICB0aGlzLl9sb2dnZXIgPSBMb2dnZXJNYW5hZ2VyLmdldExvZ2dlcignVGVybWluYWxTdGF0ZScpO1xuICAgIHRoaXMuX2NoZWNrQ29tYmluZWRTdGF0ZUFjdGl2aXR5Sm9iID0gdGhpcy5fY2hlY2tDb21iaW5lZFN0YXRlQWN0aXZpdHlKb2IuYmluZCh0aGlzKTtcbiAgICB0aGlzLl9jaGVja0NvbWJpbmVkU3RhdGVBY3Rpdml0eUpvYkludGVydmFsID0gc2V0SW50ZXJ2YWwodGhpcy5fY2hlY2tDb21iaW5lZFN0YXRlQWN0aXZpdHlKb2IsIDUgKiA2MCAqIDEwMDApO1xuICB9XG5cbiAgZ2V0IGlkKCkge1xuICAgIHJldHVybiB0aGlzLl9pZDtcbiAgfVxuXG4gIC8qKlxuICAgKiBSZXR1cm5zIHRydWUgaWYgTWV0YUFwaSBoYXZlIGNvbm5lY3RlZCB0byBNZXRhVHJhZGVyIHRlcm1pbmFsXG4gICAqIEByZXR1cm4ge0Jvb2xlYW59IHRydWUgaWYgTWV0YUFwaSBoYXZlIGNvbm5lY3RlZCB0byBNZXRhVHJhZGVyIHRlcm1pbmFsXG4gICAqL1xuICBnZXQgY29ubmVjdGVkKCkge1xuICAgIHJldHVybiBPYmplY3QudmFsdWVzKHRoaXMuX3N0YXRlQnlJbnN0YW5jZUluZGV4KS5yZWR1Y2UoKGFjYywgcykgPT4gYWNjIHx8IHMuY29ubmVjdGVkLCBmYWxzZSk7XG4gIH1cblxuICAvKipcbiAgICogUmV0dXJucyB0cnVlIGlmIE1ldGFBcGkgaGF2ZSBjb25uZWN0ZWQgdG8gTWV0YVRyYWRlciB0ZXJtaW5hbCBhbmQgTWV0YVRyYWRlciB0ZXJtaW5hbCBpcyBjb25uZWN0ZWQgdG8gYnJva2VyXG4gICAqIEByZXR1cm4ge0Jvb2xlYW59IHRydWUgaWYgTWV0YUFwaSBoYXZlIGNvbm5lY3RlZCB0byBNZXRhVHJhZGVyIHRlcm1pbmFsIGFuZCBNZXRhVHJhZGVyIHRlcm1pbmFsIGlzIGNvbm5lY3RlZCB0b1xuICAgKiBicm9rZXJcbiAgICovXG4gIGdldCBjb25uZWN0ZWRUb0Jyb2tlcigpIHtcbiAgICByZXR1cm4gT2JqZWN0LnZhbHVlcyh0aGlzLl9zdGF0ZUJ5SW5zdGFuY2VJbmRleCkucmVkdWNlKChhY2MsIHMpID0+IGFjYyB8fCBzLmNvbm5lY3RlZFRvQnJva2VyLCBmYWxzZSk7XG4gIH1cblxuICAvKipcbiAgICogUmV0dXJucyBhIGxvY2FsIGNvcHkgb2YgYWNjb3VudCBpbmZvcm1hdGlvblxuICAgKiBAcmV0dXJucyB7TWV0YXRyYWRlckFjY291bnRJbmZvcm1hdGlvbn0gbG9jYWwgY29weSBvZiBhY2NvdW50IGluZm9ybWF0aW9uXG4gICAqL1xuICBnZXQgYWNjb3VudEluZm9ybWF0aW9uKCkge1xuICAgIHJldHVybiB0aGlzLl9jb21iaW5lZFN0YXRlLmFjY291bnRJbmZvcm1hdGlvbjtcbiAgfVxuXG4gIC8qKlxuICAgKiBSZXR1cm5zIGEgbG9jYWwgY29weSBvZiBNZXRhVHJhZGVyIHBvc2l0aW9ucyBvcGVuZWRcbiAgICogQHJldHVybnMge0FycmF5PE1ldGF0cmFkZXJQb3NpdGlvbj59IGEgbG9jYWwgY29weSBvZiBNZXRhVHJhZGVyIHBvc2l0aW9ucyBvcGVuZWRcbiAgICovXG4gIGdldCBwb3NpdGlvbnMoKSB7XG4gICAgY29uc3QgaGFzaCA9IHRoaXMuX2NvbWJpbmVkU3RhdGUucG9zaXRpb25zSGFzaDtcbiAgICByZXR1cm4gaGFzaCA/IE9iamVjdC52YWx1ZXModGhpcy5fdGVybWluYWxIYXNoTWFuYWdlci5nZXRQb3NpdGlvbnNCeUhhc2goaGFzaCkgfHwge30pIDogW107XG4gIH1cblxuICAvKipcbiAgICogUmV0dXJucyBhIGxvY2FsIGNvcHkgb2YgTWV0YVRyYWRlciBvcmRlcnMgb3BlbmVkXG4gICAqIEByZXR1cm5zIHtBcnJheTxNZXRhdHJhZGVyT3JkZXI+fSBhIGxvY2FsIGNvcHkgb2YgTWV0YVRyYWRlciBvcmRlcnMgb3BlbmVkXG4gICAqL1xuICBnZXQgb3JkZXJzKCkge1xuICAgIGNvbnN0IGhhc2ggPSB0aGlzLl9jb21iaW5lZFN0YXRlLm9yZGVyc0hhc2g7XG4gICAgcmV0dXJuIGhhc2ggPyBPYmplY3QudmFsdWVzKHRoaXMuX3Rlcm1pbmFsSGFzaE1hbmFnZXIuZ2V0T3JkZXJzQnlIYXNoKGhhc2gpIHx8IHt9KSA6IFtdO1xuICB9XG5cbiAgLyoqXG4gICAqIFJldHVybnMgYSBsb2NhbCBjb3B5IG9mIHN5bWJvbCBzcGVjaWZpY2F0aW9ucyBhdmFpbGFibGUgaW4gTWV0YVRyYWRlciB0cmFkaW5nIHRlcm1pbmFsXG4gICAqIEByZXR1cm5zIHtBcnJheTxNZXRhdHJhZGVyU3ltYm9sU3BlY2lmaWNhdGlvbj59IGEgbG9jYWwgY29weSBvZiBzeW1ib2wgc3BlY2lmaWNhdGlvbnMgYXZhaWxhYmxlIGluIE1ldGFUcmFkZXJcbiAgICogdHJhZGluZyB0ZXJtaW5hbFxuICAgKi9cbiAgZ2V0IHNwZWNpZmljYXRpb25zKCkge1xuICAgIGNvbnN0IGhhc2ggPSB0aGlzLl9jb21iaW5lZFN0YXRlLnNwZWNpZmljYXRpb25zSGFzaDtcbiAgICByZXR1cm4gaGFzaCA/IE9iamVjdC52YWx1ZXModGhpcy5fdGVybWluYWxIYXNoTWFuYWdlci5nZXRTcGVjaWZpY2F0aW9uc0J5SGFzaChcbiAgICAgIHRoaXMuX2NvbWJpbmVkU3RhdGUuc3BlY2lmaWNhdGlvbnNIYXNoKSB8fCB7fSkgOiBbXTtcbiAgfVxuXG4gIC8qKlxuICAgKiBSZXR1cm5zIGhhc2hlcyBvZiB0ZXJtaW5hbCBzdGF0ZSBkYXRhIGZvciBpbmNyZW1lbnRhbCBzeW5jaHJvbml6YXRpb25cbiAgICogQHJldHVybnMge1Byb21pc2U8T2JqZWN0Pn0gcHJvbWlzZSByZXNvbHZpbmcgd2l0aCBoYXNoZXMgb2YgdGVybWluYWwgc3RhdGUgZGF0YVxuICAgKi9cbiAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGNvbXBsZXhpdHlcbiAgZ2V0SGFzaGVzKCkge1xuICAgIGNvbnN0IHNwZWNpZmljYXRpb25zSGFzaGVzID0gdGhpcy5fdGVybWluYWxIYXNoTWFuYWdlci5nZXRMYXN0VXNlZFNwZWNpZmljYXRpb25IYXNoZXModGhpcy5fYWNjb3VudC5zZXJ2ZXIpO1xuICAgIGNvbnN0IHBvc2l0aW9uc0hhc2hlcyA9IHRoaXMuX3Rlcm1pbmFsSGFzaE1hbmFnZXIuZ2V0TGFzdFVzZWRQb3NpdGlvbkhhc2hlcyh0aGlzLl9hY2NvdW50LmlkKTtcbiAgICBjb25zdCBvcmRlcnNIYXNoZXMgPSB0aGlzLl90ZXJtaW5hbEhhc2hNYW5hZ2VyLmdldExhc3RVc2VkT3JkZXJIYXNoZXModGhpcy5fYWNjb3VudC5pZCk7XG5cbiAgICByZXR1cm4ge1xuICAgICAgc3BlY2lmaWNhdGlvbnNIYXNoZXM6IHNwZWNpZmljYXRpb25zSGFzaGVzLFxuICAgICAgcG9zaXRpb25zSGFzaGVzOiBwb3NpdGlvbnNIYXNoZXMsXG4gICAgICBvcmRlcnNIYXNoZXM6IG9yZGVyc0hhc2hlc1xuICAgIH07XG4gIH1cblxuICAvKipcbiAgICogUmV0dXJucyBNZXRhVHJhZGVyIHN5bWJvbCBzcGVjaWZpY2F0aW9uIGJ5IHN5bWJvbFxuICAgKiBAcGFyYW0ge1N0cmluZ30gc3ltYm9sIHN5bWJvbCAoZS5nLiBjdXJyZW5jeSBwYWlyIG9yIGFuIGluZGV4KVxuICAgKiBAcmV0dXJuIHtNZXRhdHJhZGVyU3ltYm9sU3BlY2lmaWNhdGlvbn0gTWV0YXRyYWRlclN5bWJvbFNwZWNpZmljYXRpb24gZm91bmQgb3IgdW5kZWZpbmVkIGlmIHNwZWNpZmljYXRpb24gZm9yIGFcbiAgICogc3ltYm9sIGlzIG5vdCBmb3VuZFxuICAgKi9cbiAgc3BlY2lmaWNhdGlvbihzeW1ib2wpIHtcbiAgICBpZih0aGlzLl9jb21iaW5lZFN0YXRlLnNwZWNpZmljYXRpb25zSGFzaCkge1xuICAgICAgY29uc3Qgc3RhdGUgPSB0aGlzLl90ZXJtaW5hbEhhc2hNYW5hZ2VyLmdldFNwZWNpZmljYXRpb25zQnlIYXNoKFxuICAgICAgICB0aGlzLl9jb21iaW5lZFN0YXRlLnNwZWNpZmljYXRpb25zSGFzaCk7XG4gICAgICByZXR1cm4gc3RhdGVbc3ltYm9sXTtcbiAgICB9IGVsc2Uge1xuICAgICAgcmV0dXJuIG51bGw7XG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIFJldHVybnMgTWV0YVRyYWRlciBzeW1ib2wgcHJpY2UgYnkgc3ltYm9sXG4gICAqIEBwYXJhbSB7U3RyaW5nfSBzeW1ib2wgc3ltYm9sIChlLmcuIGN1cnJlbmN5IHBhaXIgb3IgYW4gaW5kZXgpXG4gICAqIEByZXR1cm4ge01ldGF0cmFkZXJTeW1ib2xQcmljZX0gTWV0YXRyYWRlclN5bWJvbFByaWNlIGZvdW5kIG9yIHVuZGVmaW5lZCBpZiBwcmljZSBmb3IgYSBzeW1ib2wgaXMgbm90IGZvdW5kXG4gICAqL1xuICBwcmljZShzeW1ib2wpIHtcbiAgICByZXR1cm4gdGhpcy5fY29tYmluZWRTdGF0ZS5wcmljZXNCeVN5bWJvbFtzeW1ib2xdO1xuICB9XG5cbiAgLyoqXG4gICAqIFF1b3RlIHRpbWVcbiAgICogQHR5cGRlZiB7T2JqZWN0fSBRdW90ZVRpbWVcbiAgICogQHByb3BlcnR5IHtEYXRlfSB0aW1lIHF1b3RlIHRpbWVcbiAgICogQHByb3BlcnR5IHtTdHJpbmd9IGJyb2tlclRpbWUgcXVvdGUgdGltZSBpbiBicm9rZXIgdGltZXpvbmUsIFlZWVktTU0tREQgSEg6bW06c3MuU1NTIGZvcm1hdFxuICAgKi9cblxuICAvKipcbiAgICogUmV0dXJucyB0aW1lIG9mIHRoZSBsYXN0IHJlY2VpdmVkIHF1b3RlXG4gICAqIEByZXR1cm4ge1F1b3RlVGltZX0gdGltZSBvZiB0aGUgbGFzdCByZWNlaXZlZCBxdW90ZVxuICAgKi9cbiAgZ2V0IGxhc3RRdW90ZVRpbWUoKSB7XG4gICAgaWYgKHRoaXMuX2NvbWJpbmVkU3RhdGUubGFzdFF1b3RlVGltZSkge1xuICAgICAgcmV0dXJuIHtcbiAgICAgICAgdGltZTogdGhpcy5fY29tYmluZWRTdGF0ZS5sYXN0UXVvdGVUaW1lLFxuICAgICAgICBicm9rZXJUaW1lOiB0aGlzLl9jb21iaW5lZFN0YXRlLmxhc3RRdW90ZUJyb2tlclRpbWUsXG4gICAgICB9O1xuICAgIH0gZWxzZSB7XG4gICAgICByZXR1cm4gdW5kZWZpbmVkO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBXYWl0cyBmb3IgcHJpY2UgdG8gYmUgcmVjZWl2ZWRcbiAgICogQHBhcmFtIHtzdHJpbmd9IHN5bWJvbCBzeW1ib2wgKGUuZy4gY3VycmVuY3kgcGFpciBvciBhbiBpbmRleClcbiAgICogQHBhcmFtIHtudW1iZXJ9IFt0aW1lb3V0SW5TZWNvbmRzXSB0aW1lb3V0IGluIHNlY29uZHMsIGRlZmF1bHQgaXMgMzBcbiAgICogQHJldHVybiB7UHJvbWlzZTxNZXRhdHJhZGVyU3ltYm9sUHJpY2U+fSBwcm9taXNlIHJlc29sdmluZyB3aXRoIHByaWNlIG9yIHVuZGVmaW5lZCBpZiBwcmljZSBoYXMgbm90IGJlZW4gcmVjZWl2ZWRcbiAgICovXG4gIGFzeW5jIHdhaXRGb3JQcmljZShzeW1ib2wsIHRpbWVvdXRJblNlY29uZHMgPSAzMCkge1xuICAgIHRoaXMuX3dhaXRGb3JQcmljZVJlc29sdmVzW3N5bWJvbF0gPSB0aGlzLl93YWl0Rm9yUHJpY2VSZXNvbHZlc1tzeW1ib2xdIHx8IFtdO1xuICAgIGlmICghdGhpcy5wcmljZShzeW1ib2wpKSB7XG4gICAgICBhd2FpdCBQcm9taXNlLnJhY2UoW1xuICAgICAgICBuZXcgUHJvbWlzZShyZXMgPT4gdGhpcy5fd2FpdEZvclByaWNlUmVzb2x2ZXNbc3ltYm9sXS5wdXNoKHJlcykpLFxuICAgICAgICBuZXcgUHJvbWlzZShyZXMgPT4gc2V0VGltZW91dChyZXMsIHRpbWVvdXRJblNlY29uZHMgKiAxMDAwKSlcbiAgICAgIF0pO1xuICAgIH1cbiAgICByZXR1cm4gdGhpcy5wcmljZShzeW1ib2wpO1xuICB9XG5cbiAgLyoqXG4gICAqIEludm9rZWQgd2hlbiBjb25uZWN0aW9uIHRvIE1ldGFUcmFkZXIgdGVybWluYWwgZXN0YWJsaXNoZWRcbiAgICogQHBhcmFtIHtTdHJpbmd9IGluc3RhbmNlSW5kZXggaW5kZXggb2YgYW4gYWNjb3VudCBpbnN0YW5jZSBjb25uZWN0ZWRcbiAgICovXG4gIG9uQ29ubmVjdGVkKGluc3RhbmNlSW5kZXgpIHtcbiAgICB0aGlzLl9nZXRTdGF0ZShpbnN0YW5jZUluZGV4KS5jb25uZWN0ZWQgPSB0cnVlO1xuICB9XG5cbiAgLyoqXG4gICAqIEludm9rZWQgd2hlbiBjb25uZWN0aW9uIHRvIE1ldGFUcmFkZXIgdGVybWluYWwgdGVybWluYXRlZFxuICAgKiBAcGFyYW0ge1N0cmluZ30gaW5zdGFuY2VJbmRleCBpbmRleCBvZiBhbiBhY2NvdW50IGluc3RhbmNlIGNvbm5lY3RlZF