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)

459 lines (458 loc) 75.8 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 {NotFoundError} from '../../../clients/errorHandler'; import LoggerManager from '../../../logger'; let PeriodStatisticsStreamManager = class PeriodStatisticsStreamManager { /** * Returns listeners for a tracker * @param {string} accountId account id to return listeners for * @param {string} trackerId tracker id to return listeners for * @returns {{[listenerId: string]: PeriodStatisticsListener}} dictionary of period statistics listeners */ getTrackerListeners(accountId, trackerId) { if (!this._periodStatisticsListeners[accountId] || !this._periodStatisticsListeners[accountId][trackerId]) { return {}; } else { return this._periodStatisticsListeners[accountId][trackerId]; } } /** * Adds a period statistics event listener * @param {PeriodStatisticsListener} listener period statistics event listener * @param {String} accountId account id * @param {String} trackerId tracker id * @returns {String} listener id */ // eslint-disable-next-line complexity, max-statements addPeriodStatisticsListener(listener, accountId, trackerId) { var _this = this; return _async_to_generator(function*() { let newTracker = false; if (!_this._periodStatisticsCaches[accountId]) { _this._periodStatisticsCaches[accountId] = {}; } if (!_this._periodStatisticsCaches[accountId][trackerId]) { newTracker = true; _this._periodStatisticsCaches[accountId][trackerId] = { trackerData: {}, record: {}, lastPeriod: {}, equityAdjustments: {} }; } const cache = _this._periodStatisticsCaches[accountId][trackerId]; let connection = null; let retryIntervalInSeconds = _this._retryIntervalInSeconds; const equityTrackingClient = _this._equityTrackingClient; const listenerId = randomstring.generate(10); const removePeriodStatisticsListener = _this.removePeriodStatisticsListener; const getTrackerListeners = ()=>_this.getTrackerListeners(accountId, trackerId); const pendingInitalizationResolves = _this._pendingInitalizationResolves; const synchronizationFlags = _this._accountSynchronizationFlags; let PeriodStatisticsStreamListener = class PeriodStatisticsStreamListener extends SynchronizationListener { onDealsSynchronized(instanceIndex, synchronizationId) { var _this = this; return _async_to_generator(function*() { try { if (!synchronizationFlags[accountId]) { synchronizationFlags[accountId] = true; Object.values(getTrackerListeners()).forEach((accountListener)=>{ accountListener.onConnected(); }); if (pendingInitalizationResolves[accountId]) { pendingInitalizationResolves[accountId].forEach((resolve)=>resolve()); delete pendingInitalizationResolves[accountId]; } } } catch (err) { listener.onError(err); _this._logger.error('Error processing onDealsSynchronized event for ' + `equity chart listener for account ${accountId}`, err); } })(); } onDisconnected(instanceIndex) { var _this = this; return _async_to_generator(function*() { try { if (synchronizationFlags[accountId] && !connection.healthMonitor.healthStatus.synchronized) { synchronizationFlags[accountId] = false; Object.values(getTrackerListeners()).forEach((trackerListener)=>{ trackerListener.onDisconnected(); }); } } catch (err) { Object.values(getTrackerListeners()).forEach((trackerListener)=>{ trackerListener.onError(err); }); _this._logger.error('Error processing onDisconnected event for ' + `equity chart listener for account ${accountId}`, err); } })(); } // eslint-disable-next-line complexity, max-statements onSymbolPriceUpdated(instanceIndex, price) { var _this = this; return _async_to_generator(function*() { try { if (pendingInitalizationResolves[accountId]) { pendingInitalizationResolves[accountId].forEach((resolve)=>resolve()); delete pendingInitalizationResolves[accountId]; } if (!cache.lastPeriod) { return; } /** * Process brokerTime: * - smaller than tracker startBrokerTime -> ignore * - bigger than tracker endBrokerTime -> send onTrackerCompleted, close connection * - bigger than period endBrokerTime -> send onPeriodStatisticsCompleted * - normal -> compare to previous data, if different -> send onPeriodStatisticsUpdated */ const equity = price.equity - Object.values(cache.equityAdjustments).reduce((a, b)=>a + b, 0); const brokerTime = price.brokerTime; if (brokerTime > cache.lastPeriod.endBrokerTime) { Object.values(getTrackerListeners()).forEach((trackerListener)=>{ trackerListener.onPeriodStatisticsCompleted(); }); cache.equityAdjustments = {}; const startBrokerTime = cache.lastPeriod.startBrokerTime; cache.lastPeriod = null; // eslint-disable-next-line no-constant-condition while(true){ let periods = yield equityTrackingClient.getTrackingStatistics(accountId, trackerId, undefined, 2, true); if (periods[0].startBrokerTime === startBrokerTime) { yield new Promise((res)=>setTimeout(res, 10000)); } else { cache.lastPeriod = periods[0]; periods.reverse(); Object.values(getTrackerListeners()).forEach((trackerListener)=>{ trackerListener.onPeriodStatisticsUpdated(periods); }); break; } } } else { if (cache.trackerData.startBrokerTime && brokerTime < cache.trackerData.startBrokerTime) { return; } if (cache.trackerData.endBrokerTime && brokerTime > cache.trackerData.endBrokerTime) { Object.values(getTrackerListeners()).forEach((trackerListener)=>{ trackerListener.onTrackerCompleted(); }); cache.equityAdjustments = {}; Object.keys(getTrackerListeners()).forEach((trackerListenerId)=>{ removePeriodStatisticsListener(trackerListenerId); }); } let absoluteDrawdown = Math.max(0, cache.lastPeriod.initialBalance - equity); let relativeDrawdown = absoluteDrawdown / cache.lastPeriod.initialBalance; let absoluteProfit = Math.max(0, equity - cache.lastPeriod.initialBalance); let relativeProfit = absoluteProfit / cache.lastPeriod.initialBalance; const previousRecord = JSON.stringify(cache.record); if (!cache.record.thresholdExceeded) { if (cache.record.maxAbsoluteDrawdown < absoluteDrawdown) { cache.record.maxAbsoluteDrawdown = absoluteDrawdown; cache.record.maxRelativeDrawdown = relativeDrawdown; cache.record.maxDrawdownTime = brokerTime; if (cache.trackerData.relativeDrawdownThreshold && cache.trackerData.relativeDrawdownThreshold < relativeDrawdown || cache.trackerData.absoluteDrawdownThreshold && cache.trackerData.absoluteDrawdownThreshold < absoluteDrawdown) { cache.record.thresholdExceeded = true; cache.record.exceededThresholdType = 'drawdown'; } } if (cache.record.maxAbsoluteProfit < absoluteProfit) { cache.record.maxAbsoluteProfit = absoluteProfit; cache.record.maxRelativeProfit = relativeProfit; cache.record.maxProfitTime = brokerTime; if (cache.trackerData.relativeProfitThreshold && cache.trackerData.relativeProfitThreshold < relativeProfit || cache.trackerData.absoluteProfitThreshold && cache.trackerData.absoluteProfitThreshold < absoluteProfit) { cache.record.thresholdExceeded = true; cache.record.exceededThresholdType = 'profit'; } } if (JSON.stringify(cache.record) !== previousRecord) { Object.values(getTrackerListeners()).forEach((trackerListener)=>{ trackerListener.onPeriodStatisticsUpdated([ { startBrokerTime: cache.lastPeriod.startBrokerTime, endBrokerTime: cache.lastPeriod.endBrokerTime, initialBalance: cache.lastPeriod.initialBalance, maxAbsoluteDrawdown: cache.record.maxAbsoluteDrawdown, maxAbsoluteProfit: cache.record.maxAbsoluteProfit, maxDrawdownTime: cache.record.maxDrawdownTime, maxProfitTime: cache.record.maxProfitTime, maxRelativeDrawdown: cache.record.maxRelativeDrawdown, maxRelativeProfit: cache.record.maxRelativeProfit, period: cache.lastPeriod.period, exceededThresholdType: cache.record.exceededThresholdType, thresholdExceeded: cache.record.thresholdExceeded, tradeDayCount: cache.record.tradeDayCount } ]); }); } } } } catch (err) { Object.values(getTrackerListeners()).forEach((trackerListener)=>{ trackerListener.onError(err); }); _this._logger.error('Error processing onSymbolPriceUpdated event for ' + `period statistics listener for account ${accountId}`, err); } })(); } onDealAdded(instanceIndex, deal) { var _this = this; return _async_to_generator(function*() { try { if (!cache.lastPeriod || !Object.keys(cache.lastPeriod).length) { return; } if (deal.type === 'DEAL_TYPE_BALANCE') { cache.equityAdjustments[deal.id] = deal.profit; } const ignoredDealTypes = [ 'DEAL_TYPE_BALANCE', 'DEAL_TYPE_CREDIT' ]; if (!ignoredDealTypes.includes(deal.type)) { const timeDiff = new Date(deal.time).getTime() - new Date(deal.brokerTime).getTime(); const startSearchDate = new Date(new Date(cache.lastPeriod.startBrokerTime).getTime() + timeDiff); const deals = connection.historyStorage.getDealsByTimeRange(startSearchDate, new Date(8640000000000000)).filter((dealItem)=>!ignoredDealTypes.includes(dealItem.type)); deals.push(deal); const tradedDays = {}; deals.forEach((dealItem)=>{ tradedDays[dealItem.brokerTime.slice(0, 10)] = true; }); const tradeDayCount = Object.keys(tradedDays).length; if (cache.record.tradeDayCount !== tradeDayCount) { cache.record.tradeDayCount = tradeDayCount; Object.values(getTrackerListeners()).forEach((trackerListener)=>{ trackerListener.onPeriodStatisticsUpdated([ { startBrokerTime: cache.lastPeriod.startBrokerTime, endBrokerTime: cache.lastPeriod.endBrokerTime, initialBalance: cache.lastPeriod.initialBalance, maxAbsoluteDrawdown: cache.record.maxAbsoluteDrawdown, maxAbsoluteProfit: cache.record.maxAbsoluteProfit, maxDrawdownTime: cache.record.maxDrawdownTime, maxProfitTime: cache.record.maxProfitTime, maxRelativeDrawdown: cache.record.maxRelativeDrawdown, maxRelativeProfit: cache.record.maxRelativeProfit, period: cache.lastPeriod.period, exceededThresholdType: cache.record.exceededThresholdType, thresholdExceeded: cache.record.thresholdExceeded, tradeDayCount: cache.record.tradeDayCount } ]); }); } } } catch (err) { Object.values(getTrackerListeners()).forEach((trackerListener)=>{ trackerListener.onError(err); }); _this._logger.error('Error processing onDealAdded event for ' + `period statistics listener for account ${accountId}`, err); } })(); } }; const account = yield _this._metaApi.metatraderAccountApi.getAccount(accountId); const tracker = yield equityTrackingClient.getTracker(accountId, trackerId); cache.trackerData = tracker; if (!_this._periodStatisticsListeners[accountId]) { _this._periodStatisticsListeners[accountId] = {}; } if (!_this._periodStatisticsListeners[accountId][trackerId]) { _this._periodStatisticsListeners[accountId][trackerId] = {}; } const accountListeners = _this._periodStatisticsListeners[accountId][trackerId]; accountListeners[listenerId] = listener; _this._accountsByListenerId[listenerId] = accountId; _this._trackersByListenerId[listenerId] = trackerId; let isDeployed = false; while(!isDeployed){ try { yield account.waitDeployed(); isDeployed = true; } catch (err) { listener.onError(err); _this._logger.error(`Error wait for account ${accountId} to deploy, retrying`, err); yield new Promise((res)=>setTimeout(res, retryIntervalInSeconds * 1000)); retryIntervalInSeconds = Math.min(retryIntervalInSeconds * 2, 300); } } if (!_this._periodStatisticsConnections[accountId]) { retryIntervalInSeconds = _this._retryIntervalInSeconds; connection = account.getStreamingConnection(); const syncListener = new PeriodStatisticsStreamListener(); connection.addSynchronizationListener(syncListener); _this._periodStatisticsConnections[accountId] = connection; _this._syncListeners[trackerId] = syncListener; let isSynchronized = false; while(!isSynchronized){ try { yield connection.connect(); yield connection.waitSynchronized(); isSynchronized = true; } catch (err) { listener.onError(err); _this._logger.error('Error configuring period statistics stream listener for ' + `account ${accountId}, retrying`, err); yield new Promise((res)=>setTimeout(res, retryIntervalInSeconds * 1000)); retryIntervalInSeconds = Math.min(retryIntervalInSeconds * 2, 300); } } retryIntervalInSeconds = _this._retryIntervalInSeconds; } else { connection = _this._periodStatisticsConnections[accountId]; if (newTracker) { const syncListener = new PeriodStatisticsStreamListener(); connection.addSynchronizationListener(syncListener); _this._syncListeners[trackerId] = syncListener; } if (!connection.healthMonitor.healthStatus.synchronized) { if (!_this._pendingInitalizationResolves[accountId]) { _this._pendingInitalizationResolves[accountId] = []; } let resolveInitialize; let initializePromise = new Promise((res, rej)=>{ resolveInitialize = res; }); _this._pendingInitalizationResolves[accountId].push(resolveInitialize); yield initializePromise; } } let initialData = []; const fetchInitialData = function() { var _ref = _async_to_generator(function*() { try { initialData = yield equityTrackingClient.getTrackingStatistics(accountId, trackerId, undefined, undefined, true); if (initialData.length) { const lastItem = initialData[0]; if (_this._fetchInitialDataIntervalId[listenerId]) { clearInterval(_this._fetchInitialDataIntervalId[listenerId]); delete _this._fetchInitialDataIntervalId[listenerId]; } listener.onPeriodStatisticsUpdated(initialData); cache.lastPeriod = { startBrokerTime: lastItem.startBrokerTime, endBrokerTime: lastItem.endBrokerTime, period: lastItem.period, initialBalance: lastItem.initialBalance, maxDrawdownTime: lastItem.maxDrawdownTime, maxAbsoluteDrawdown: lastItem.maxAbsoluteDrawdown, maxRelativeDrawdown: lastItem.maxRelativeDrawdown, maxProfitTime: lastItem.maxProfitTime, maxAbsoluteProfit: lastItem.maxAbsoluteProfit, maxRelativeProfit: lastItem.maxRelativeProfit, thresholdExceeded: lastItem.thresholdExceeded, exceededThresholdType: lastItem.exceededThresholdType, tradeDayCount: lastItem.tradeDayCount }; cache.record = cache.lastPeriod; } } catch (err) { listener.onError(err); _this._logger.error(`Failed to initialize tracking statistics data for account ${accountId}`, err); yield new Promise((res)=>setTimeout(res, retryIntervalInSeconds * 1000)); retryIntervalInSeconds = Math.min(retryIntervalInSeconds * 2, 300); } }); return function fetchInitialData() { return _ref.apply(this, arguments); }; }(); retryIntervalInSeconds = _this._retryIntervalInSeconds; _this._fetchInitialDataIntervalId[listenerId] = setInterval(fetchInitialData, retryIntervalInSeconds * 1000 * 2 * 60); fetchInitialData(); return listenerId; })(); } /** * Removes period statistics event listener by id * @param {String} listenerId listener id */ // eslint-disable-next-line complexity removePeriodStatisticsListener(listenerId) { if (this._accountsByListenerId[listenerId] && this._trackersByListenerId[listenerId]) { if (this._fetchInitialDataIntervalId[listenerId]) { clearInterval(this._fetchInitialDataIntervalId[listenerId]); delete this._fetchInitialDataIntervalId[listenerId]; } const accountId = this._accountsByListenerId[listenerId]; const trackerId = this._trackersByListenerId[listenerId]; delete this._accountsByListenerId[listenerId]; delete this._trackersByListenerId[listenerId]; if (this._periodStatisticsListeners[accountId]) { if (this._periodStatisticsListeners[accountId][trackerId]) { delete this._periodStatisticsListeners[accountId][trackerId][listenerId]; if (!Object.keys(this._periodStatisticsListeners[accountId][trackerId]).length) { delete this._periodStatisticsListeners[accountId][trackerId]; if (this._periodStatisticsConnections[accountId] && this._syncListeners[trackerId]) { this._periodStatisticsConnections[accountId].removeSynchronizationListener(this._syncListeners[trackerId]); delete this._syncListeners[trackerId]; } } } if (!Object.keys(this._periodStatisticsListeners[accountId]).length) { delete this._periodStatisticsListeners[accountId]; } } if (this._periodStatisticsConnections[accountId] && !this._periodStatisticsListeners[accountId]) { delete this._accountSynchronizationFlags[accountId]; this._periodStatisticsConnections[accountId].close(); delete this._periodStatisticsConnections[accountId]; } } } /** * Constructs period statistics event listener manager instance * @param {DomainClient} domainClient domain client * @param {EquityTrackingClient} equityTrackingClient equity tracking client * @param {MetaApi} metaApi metaApi SDK instance */ constructor(domainClient, equityTrackingClient, metaApi){ this._domainClient = domainClient; this._equityTrackingClient = equityTrackingClient; this._metaApi = metaApi; this._periodStatisticsListeners = {}; this._accountsByListenerId = {}; this._trackersByListenerId = {}; this._trackerSyncListeners = {}; this._periodStatisticsConnections = {}; this._periodStatisticsCaches = {}; this._accountSynchronizationFlags = {}; this._pendingInitalizationResolves = {}; this._syncListeners = {}; this._retryIntervalInSeconds = 1; this._fetchInitialDataIntervalId = {}; this.removePeriodStatisticsListener = this.removePeriodStatisticsListener.bind(this); this._logger = LoggerManager.getLogger('PeriodStatisticsStreamManager'); } }; /** * Manager for handling period statistics event listeners */ export { PeriodStatisticsStreamManager as default }; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIjxhbm9uPiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbmltcG9ydCByYW5kb21zdHJpbmcgZnJvbSAncmFuZG9tc3RyaW5nJztcbmltcG9ydCBTeW5jaHJvbml6YXRpb25MaXN0ZW5lciBmcm9tICcuLi8uLi8uLi9jbGllbnRzL21ldGFBcGkvc3luY2hyb25pemF0aW9uTGlzdGVuZXInO1xuLy8gaW1wb3J0IHtOb3RGb3VuZEVycm9yfSBmcm9tICcuLi8uLi8uLi9jbGllbnRzL2Vycm9ySGFuZGxlcic7XG5pbXBvcnQgTG9nZ2VyTWFuYWdlciBmcm9tICcuLi8uLi8uLi9sb2dnZXInO1xuXG4vKipcbiAqIE1hbmFnZXIgZm9yIGhhbmRsaW5nIHBlcmlvZCBzdGF0aXN0aWNzIGV2ZW50IGxpc3RlbmVyc1xuICovXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBQZXJpb2RTdGF0aXN0aWNzU3RyZWFtTWFuYWdlciB7XG5cbiAgLyoqXG4gICAqIENvbnN0cnVjdHMgcGVyaW9kIHN0YXRpc3RpY3MgZXZlbnQgbGlzdGVuZXIgbWFuYWdlciBpbnN0YW5jZVxuICAgKiBAcGFyYW0ge0RvbWFpbkNsaWVudH0gZG9tYWluQ2xpZW50IGRvbWFpbiBjbGllbnRcbiAgICogQHBhcmFtIHtFcXVpdHlUcmFja2luZ0NsaWVudH0gZXF1aXR5VHJhY2tpbmdDbGllbnQgZXF1aXR5IHRyYWNraW5nIGNsaWVudFxuICAgKiBAcGFyYW0ge01ldGFBcGl9IG1ldGFBcGkgbWV0YUFwaSBTREsgaW5zdGFuY2VcbiAgICovXG4gIGNvbnN0cnVjdG9yKGRvbWFpbkNsaWVudCwgZXF1aXR5VHJhY2tpbmdDbGllbnQsIG1ldGFBcGkpIHtcbiAgICB0aGlzLl9kb21haW5DbGllbnQgPSBkb21haW5DbGllbnQ7XG4gICAgdGhpcy5fZXF1aXR5VHJhY2tpbmdDbGllbnQgPSBlcXVpdHlUcmFja2luZ0NsaWVudDtcbiAgICB0aGlzLl9tZXRhQXBpID0gbWV0YUFwaTtcbiAgICB0aGlzLl9wZXJpb2RTdGF0aXN0aWNzTGlzdGVuZXJzID0ge307XG4gICAgdGhpcy5fYWNjb3VudHNCeUxpc3RlbmVySWQgPSB7fTtcbiAgICB0aGlzLl90cmFja2Vyc0J5TGlzdGVuZXJJZCA9IHt9O1xuICAgIHRoaXMuX3RyYWNrZXJTeW5jTGlzdGVuZXJzID0ge307XG4gICAgdGhpcy5fcGVyaW9kU3RhdGlzdGljc0Nvbm5lY3Rpb25zID0ge307XG4gICAgdGhpcy5fcGVyaW9kU3RhdGlzdGljc0NhY2hlcyA9IHt9O1xuICAgIHRoaXMuX2FjY291bnRTeW5jaHJvbml6YXRpb25GbGFncyA9IHt9O1xuICAgIHRoaXMuX3BlbmRpbmdJbml0YWxpemF0aW9uUmVzb2x2ZXMgPSB7fTtcbiAgICB0aGlzLl9zeW5jTGlzdGVuZXJzID0ge307XG4gICAgdGhpcy5fcmV0cnlJbnRlcnZhbEluU2Vjb25kcyA9IDE7XG4gICAgdGhpcy5fZmV0Y2hJbml0aWFsRGF0YUludGVydmFsSWQgPSB7fTtcbiAgICB0aGlzLnJlbW92ZVBlcmlvZFN0YXRpc3RpY3NMaXN0ZW5lciA9IHRoaXMucmVtb3ZlUGVyaW9kU3RhdGlzdGljc0xpc3RlbmVyLmJpbmQodGhpcyk7XG4gICAgdGhpcy5fbG9nZ2VyID0gTG9nZ2VyTWFuYWdlci5nZXRMb2dnZXIoJ1BlcmlvZFN0YXRpc3RpY3NTdHJlYW1NYW5hZ2VyJyk7XG4gIH1cblxuICAvKipcbiAgICogUmV0dXJucyBsaXN0ZW5lcnMgZm9yIGEgdHJhY2tlclxuICAgKiBAcGFyYW0ge3N0cmluZ30gYWNjb3VudElkIGFjY291bnQgaWQgdG8gcmV0dXJuIGxpc3RlbmVycyBmb3JcbiAgICogQHBhcmFtIHtzdHJpbmd9IHRyYWNrZXJJZCB0cmFja2VyIGlkIHRvIHJldHVybiBsaXN0ZW5lcnMgZm9yXG4gICAqIEByZXR1cm5zIHt7W2xpc3RlbmVySWQ6IHN0cmluZ106IFBlcmlvZFN0YXRpc3RpY3NMaXN0ZW5lcn19IGRpY3Rpb25hcnkgb2YgcGVyaW9kIHN0YXRpc3RpY3MgbGlzdGVuZXJzXG4gICAqL1xuICBnZXRUcmFja2VyTGlzdGVuZXJzKGFjY291bnRJZCwgdHJhY2tlcklkKSB7XG4gICAgaWYoIXRoaXMuX3BlcmlvZFN0YXRpc3RpY3NMaXN0ZW5lcnNbYWNjb3VudElkXSB8fCAhdGhpcy5fcGVyaW9kU3RhdGlzdGljc0xpc3RlbmVyc1thY2NvdW50SWRdW3RyYWNrZXJJZF0pIHtcbiAgICAgIHJldHVybiB7fTtcbiAgICB9IGVsc2Uge1xuICAgICAgcmV0dXJuIHRoaXMuX3BlcmlvZFN0YXRpc3RpY3NMaXN0ZW5lcnNbYWNjb3VudElkXVt0cmFja2VySWRdO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBBZGRzIGEgcGVyaW9kIHN0YXRpc3RpY3MgZXZlbnQgbGlzdGVuZXJcbiAgICogQHBhcmFtIHtQZXJpb2RTdGF0aXN0aWNzTGlzdGVuZXJ9IGxpc3RlbmVyIHBlcmlvZCBzdGF0aXN0aWNzIGV2ZW50IGxpc3RlbmVyXG4gICAqIEBwYXJhbSB7U3RyaW5nfSBhY2NvdW50SWQgYWNjb3VudCBpZFxuICAgKiBAcGFyYW0ge1N0cmluZ30gdHJhY2tlcklkIHRyYWNrZXIgaWRcbiAgICogQHJldHVybnMge1N0cmluZ30gbGlzdGVuZXIgaWRcbiAgICovXG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBjb21wbGV4aXR5LCBtYXgtc3RhdGVtZW50c1xuICBhc3luYyBhZGRQZXJpb2RTdGF0aXN0aWNzTGlzdGVuZXIobGlzdGVuZXIsIGFjY291bnRJZCwgdHJhY2tlcklkKSB7XG4gICAgbGV0IG5ld1RyYWNrZXIgPSBmYWxzZTtcbiAgICBpZighdGhpcy5fcGVyaW9kU3RhdGlzdGljc0NhY2hlc1thY2NvdW50SWRdKSB7XG4gICAgICB0aGlzLl9wZXJpb2RTdGF0aXN0aWNzQ2FjaGVzW2FjY291bnRJZF0gPSB7fTtcbiAgICB9XG4gICAgaWYoIXRoaXMuX3BlcmlvZFN0YXRpc3RpY3NDYWNoZXNbYWNjb3VudElkXVt0cmFja2VySWRdKSB7XG4gICAgICBuZXdUcmFja2VyID0gdHJ1ZTtcbiAgICAgIHRoaXMuX3BlcmlvZFN0YXRpc3RpY3NDYWNoZXNbYWNjb3VudElkXVt0cmFja2VySWRdID0ge1xuICAgICAgICB0cmFja2VyRGF0YToge30sXG4gICAgICAgIHJlY29yZDoge30sXG4gICAgICAgIGxhc3RQZXJpb2Q6IHt9LFxuICAgICAgICBlcXVpdHlBZGp1c3RtZW50czoge31cbiAgICAgIH07XG4gICAgfVxuICAgIGNvbnN0IGNhY2hlID0gdGhpcy5fcGVyaW9kU3RhdGlzdGljc0NhY2hlc1thY2NvdW50SWRdW3RyYWNrZXJJZF07XG4gICAgbGV0IGNvbm5lY3Rpb24gPSBudWxsO1xuICAgIGxldCByZXRyeUludGVydmFsSW5TZWNvbmRzID0gdGhpcy5fcmV0cnlJbnRlcnZhbEluU2Vjb25kcztcbiAgICBjb25zdCBlcXVpdHlUcmFja2luZ0NsaWVudCA9IHRoaXMuX2VxdWl0eVRyYWNraW5nQ2xpZW50O1xuICAgIGNvbnN0IGxpc3RlbmVySWQgPSByYW5kb21zdHJpbmcuZ2VuZXJhdGUoMTApO1xuICAgIGNvbnN0IHJlbW92ZVBlcmlvZFN0YXRpc3RpY3NMaXN0ZW5lciA9IHRoaXMucmVtb3ZlUGVyaW9kU3RhdGlzdGljc0xpc3RlbmVyO1xuICAgIGNvbnN0IGdldFRyYWNrZXJMaXN0ZW5lcnMgPSAoKSA9PiB0aGlzLmdldFRyYWNrZXJMaXN0ZW5lcnMoYWNjb3VudElkLCB0cmFja2VySWQpO1xuICAgIGNvbnN0IHBlbmRpbmdJbml0YWxpemF0aW9uUmVzb2x2ZXMgPSB0aGlzLl9wZW5kaW5nSW5pdGFsaXphdGlvblJlc29sdmVzO1xuICAgIGNvbnN0IHN5bmNocm9uaXphdGlvbkZsYWdzID0gdGhpcy5fYWNjb3VudFN5bmNocm9uaXphdGlvbkZsYWdzO1xuXG4gICAgY2xhc3MgUGVyaW9kU3RhdGlzdGljc1N0cmVhbUxpc3RlbmVyIGV4dGVuZHMgU3luY2hyb25pemF0aW9uTGlzdGVuZXIge1xuXG4gICAgICBhc3luYyBvbkRlYWxzU3luY2hyb25pemVkKGluc3RhbmNlSW5kZXgsIHN5bmNocm9uaXphdGlvbklkKSB7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgaWYoIXN5bmNocm9uaXphdGlvbkZsYWdzW2FjY291bnRJZF0pIHtcbiAgICAgICAgICAgIHN5bmNocm9uaXphdGlvbkZsYWdzW2FjY291bnRJZF0gPSB0cnVlO1xuICAgICAgICAgICAgT2JqZWN0LnZhbHVlcyhnZXRUcmFja2VyTGlzdGVuZXJzKCkpLmZvckVhY2goYWNjb3VudExpc3RlbmVyID0+IHtcbiAgICAgICAgICAgICAgYWNjb3VudExpc3RlbmVyLm9uQ29ubmVjdGVkKCk7XG4gICAgICAgICAgICB9KTtcbiAgICAgICAgICAgIGlmKHBlbmRpbmdJbml0YWxpemF0aW9uUmVzb2x2ZXNbYWNjb3VudElkXSkge1xuICAgICAgICAgICAgICBwZW5kaW5nSW5pdGFsaXphdGlvblJlc29sdmVzW2FjY291bnRJZF0uZm9yRWFjaChyZXNvbHZlID0+IHJlc29sdmUoKSk7XG4gICAgICAgICAgICAgIGRlbGV0ZSBwZW5kaW5nSW5pdGFsaXphdGlvblJlc29sdmVzW2FjY291bnRJZF07XG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgICBsaXN0ZW5lci5vbkVycm9yKGVycik7XG4gICAgICAgICAgdGhpcy5fbG9nZ2VyLmVycm9yKCdFcnJvciBwcm9jZXNzaW5nIG9uRGVhbHNTeW5jaHJvbml6ZWQgZXZlbnQgZm9yICcgK1xuICAgICAgICAgIGBlcXVpdHkgY2hhcnQgbGlzdGVuZXIgZm9yIGFjY291bnQgJHthY2NvdW50SWR9YCwgZXJyKTtcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICBhc3luYyBvbkRpc2Nvbm5lY3RlZChpbnN0YW5jZUluZGV4KSB7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgaWYoc3luY2hyb25pemF0aW9uRmxhZ3NbYWNjb3VudElkXSAmJiAhY29ubmVjdGlvbi5oZWFsdGhNb25pdG9yLmhlYWx0aFN0YXR1cy5zeW5jaHJvbml6ZWQpIHtcbiAgICAgICAgICAgIHN5bmNocm9uaXphdGlvbkZsYWdzW2FjY291bnRJZF0gPSBmYWxzZTtcbiAgICAgICAgICAgIE9iamVjdC52YWx1ZXMoZ2V0VHJhY2tlckxpc3RlbmVycygpKS5mb3JFYWNoKHRyYWNrZXJMaXN0ZW5lciA9PiB7XG4gICAgICAgICAgICAgIHRyYWNrZXJMaXN0ZW5lci5vbkRpc2Nvbm5lY3RlZCgpO1xuICAgICAgICAgICAgfSk7XG4gICAgICAgICAgfVxuICAgICAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgICBPYmplY3QudmFsdWVzKGdldFRyYWNrZXJMaXN0ZW5lcnMoKSkuZm9yRWFjaCh0cmFja2VyTGlzdGVuZXIgPT4ge1xuICAgICAgICAgICAgdHJhY2tlckxpc3RlbmVyLm9uRXJyb3IoZXJyKTtcbiAgICAgICAgICB9KTtcbiAgICAgICAgICB0aGlzLl9sb2dnZXIuZXJyb3IoJ0Vycm9yIHByb2Nlc3Npbmcgb25EaXNjb25uZWN0ZWQgZXZlbnQgZm9yICcgK1xuICAgICAgICAgIGBlcXVpdHkgY2hhcnQgbGlzdGVuZXIgZm9yIGFjY291bnQgJHthY2NvdW50SWR9YCwgZXJyKTtcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgY29tcGxleGl0eSwgbWF4LXN0YXRlbWVudHNcbiAgICAgIGFzeW5jIG9uU3ltYm9sUHJpY2VVcGRhdGVkKGluc3RhbmNlSW5kZXgsIHByaWNlKSB7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgaWYocGVuZGluZ0luaXRhbGl6YXRpb25SZXNvbHZlc1thY2NvdW50SWRdKSB7XG4gICAgICAgICAgICBwZW5kaW5nSW5pdGFsaXphdGlvblJlc29sdmVzW2FjY291bnRJZF0uZm9yRWFjaChyZXNvbHZlID0+IHJlc29sdmUoKSk7XG4gICAgICAgICAgICBkZWxldGUgcGVuZGluZ0luaXRhbGl6YXRpb25SZXNvbHZlc1thY2NvdW50SWRdO1xuICAgICAgICAgIH1cbiAgXG4gICAgICAgICAgaWYoIWNhY2hlLmxhc3RQZXJpb2QpIHtcbiAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgICB9XG4gIFxuICAgICAgICAgIC8qKlxuICAgICAgICAgICAqIFByb2Nlc3MgYnJva2VyVGltZTpcbiAgICAgICAgICAgKiAtIHNtYWxsZXIgdGhhbiB0cmFja2VyIHN0YXJ0QnJva2VyVGltZSAtPiBpZ25vcmVcbiAgICAgICAgICAgKiAtIGJpZ2dlciB0aGFuIHRyYWNrZXIgZW5kQnJva2VyVGltZSAtPiBzZW5kIG9uVHJhY2tlckNvbXBsZXRlZCwgY2xvc2UgY29ubmVjdGlvblxuICAgICAgICAgICAqIC0gYmlnZ2VyIHRoYW4gcGVyaW9kIGVuZEJyb2tlclRpbWUgLT4gc2VuZCBvblBlcmlvZFN0YXRpc3RpY3NDb21wbGV0ZWRcbiAgICAgICAgICAgKiAtIG5vcm1hbCAtPiBjb21wYXJlIHRvIHByZXZpb3VzIGRhdGEsIGlmIGRpZmZlcmVudCAtPiBzZW5kIG9uUGVyaW9kU3RhdGlzdGljc1VwZGF0ZWRcbiAgICAgICAgICAgKi9cbiAgICAgICAgICBjb25zdCBlcXVpdHkgPSBwcmljZS5lcXVpdHkgLSBPYmplY3QudmFsdWVzKGNhY2hlLmVxdWl0eUFkanVzdG1lbnRzKVxuICAgICAgICAgICAgLnJlZHVjZSgoYSwgYikgPT4gYSArIGIsIDApO1xuICAgICAgICAgIGNvbnN0IGJyb2tlclRpbWUgPSBwcmljZS5icm9rZXJUaW1lO1xuICAgICAgICAgIGlmKGJyb2tlclRpbWUgPiBjYWNoZS5sYXN0UGVyaW9kLmVuZEJyb2tlclRpbWUpIHtcbiAgICAgICAgICAgIE9iamVjdC52YWx1ZXMoZ2V0VHJhY2tlckxpc3RlbmVycygpKS5mb3JFYWNoKHRyYWNrZXJMaXN0ZW5lciA9PiB7XG4gICAgICAgICAgICAgIHRyYWNrZXJMaXN0ZW5lci5vblBlcmlvZFN0YXRpc3RpY3NDb21wbGV0ZWQoKTtcbiAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgY2FjaGUuZXF1aXR5QWRqdXN0bWVudHMgPSB7fTtcbiAgICAgICAgICAgIGNvbnN0IHN0YXJ0QnJva2VyVGltZSA9IGNhY2hlLmxhc3RQZXJpb2Quc3RhcnRCcm9rZXJUaW1lO1xuICAgICAgICAgICAgY2FjaGUubGFzdFBlcmlvZCA9IG51bGw7XG4gICAgICAgICAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tY29uc3RhbnQtY29uZGl0aW9uXG4gICAgICAgICAgICB3aGlsZSh0cnVlKSB7XG4gICAgICAgICAgICAgIGxldCBwZXJpb2RzID0gYXdhaXQgZXF1aXR5VHJhY2tpbmdDbGllbnQuZ2V0VHJhY2tpbmdTdGF0aXN0aWNzKGFjY291bnRJZCwgdHJhY2tlcklkLCB1bmRlZmluZWQsIDIsIHRydWUpO1xuICAgICAgICAgICAgICBpZihwZXJpb2RzWzBdLnN0YXJ0QnJva2VyVGltZSA9PT0gc3RhcnRCcm9rZXJUaW1lKSB7XG4gICAgICAgICAgICAgICAgYXdhaXQgbmV3IFByb21pc2UocmVzID0+IHNldFRpbWVvdXQocmVzLCAxMDAwMCkpO1xuICAgICAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgICAgIGNhY2hlLmxhc3RQZXJpb2QgPSBwZXJpb2RzWzBdO1xuICAgICAgICAgICAgICAgIHBlcmlvZHMucmV2ZXJzZSgpO1xuICAgICAgICAgICAgICAgIE9iamVjdC52YWx1ZXMoZ2V0VHJhY2tlckxpc3RlbmVycygpKS5mb3JFYWNoKHRyYWNrZXJMaXN0ZW5lciA9PiB7XG4gICAgICAgICAgICAgICAgICB0cmFja2VyTGlzdGVuZXIub25QZXJpb2RTdGF0aXN0aWNzVXBkYXRlZChwZXJpb2RzKTtcbiAgICAgICAgICAgICAgICB9KTtcbiAgICAgICAgICAgICAgICBicmVhaztcbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICBpZihjYWNoZS50cmFja2VyRGF0YS5zdGFydEJyb2tlclRpbWUgJiYgYnJva2VyVGltZSA8IGNhY2hlLnRyYWNrZXJEYXRhLnN0YXJ0QnJva2VyVGltZSkge1xuICAgICAgICAgICAgICByZXR1cm47XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBpZihjYWNoZS50cmFja2VyRGF0YS5lbmRCcm9rZXJUaW1lICYmIGJyb2tlclRpbWUgPiBjYWNoZS50cmFja2VyRGF0YS5lbmRCcm9rZXJUaW1lKSB7XG4gICAgICAgICAgICAgIE9iamVjdC52YWx1ZXMoZ2V0VHJhY2tlckxpc3RlbmVycygpKS5mb3JFYWNoKHRyYWNrZXJMaXN0ZW5lciA9PiB7XG4gICAgICAgICAgICAgICAgdHJhY2tlckxpc3RlbmVyLm9uVHJhY2tlckNvbXBsZXRlZCgpO1xuICAgICAgICAgICAgICB9KTtcbiAgICAgICAgICAgICAgY2FjaGUuZXF1aXR5QWRqdXN0bWVudHMgPSB7fTtcbiAgICAgICAgICAgICAgT2JqZWN0LmtleXMoZ2V0VHJhY2tlckxpc3RlbmVycygpKS5mb3JFYWNoKHRyYWNrZXJMaXN0ZW5lcklkID0+IHtcbiAgICAgICAgICAgICAgICByZW1vdmVQZXJpb2RTdGF0aXN0aWNzTGlzdGVuZXIodHJhY2tlckxpc3RlbmVySWQpO1xuICAgICAgICAgICAgICB9KTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIFxuICAgICAgICAgICAgbGV0IGFic29sdXRlRHJhd2Rvd24gPSBNYXRoLm1heCgwLCBjYWNoZS5sYXN0UGVyaW9kLmluaXRpYWxCYWxhbmNlIC0gZXF1aXR5KTtcbiAgICAgICAgICAgIGxldCByZWxhdGl2ZURyYXdkb3duID0gYWJzb2x1dGVEcmF3ZG93biAvIGNhY2hlLmxhc3RQZXJpb2QuaW5pdGlhbEJhbGFuY2U7XG4gICAgICAgICAgICBsZXQgYWJzb2x1dGVQcm9maXQgPSBNYXRoLm1heCgwLCBlcXVpdHkgLSBjYWNoZS5sYXN0UGVyaW9kLmluaXRpYWxCYWxhbmNlKTtcbiAgICAgICAgICAgIGxldCByZWxhdGl2ZVByb2ZpdCA9IGFic29sdXRlUHJvZml0IC8gY2FjaGUubGFzdFBlcmlvZC5pbml0aWFsQmFsYW5jZTtcbiAgICAgICAgICAgIGNvbnN0IHByZXZpb3VzUmVjb3JkID0gSlNPTi5zdHJpbmdpZnkoY2FjaGUucmVjb3JkKTtcbiAgICAgICAgICAgIGlmKCFjYWNoZS5yZWNvcmQudGhyZXNob2xkRXhjZWVkZWQpIHtcbiAgICAgICAgICAgICAgaWYoY2FjaGUucmVjb3JkLm1heEFic29sdXRlRHJhd2Rvd24gPCBhYnNvbHV0ZURyYXdkb3duKSB7XG4gICAgICAgICAgICAgICAgY2FjaGUucmVjb3JkLm1heEFic29sdXRlRHJhd2Rvd24gPSBhYnNvbHV0ZURyYXdkb3duO1xuICAgICAgICAgICAgICAgIGNhY2hlLnJlY29yZC5tYXhSZWxhdGl2ZURyYXdkb3duID0gcmVsYXRpdmVEcmF3ZG93bjtcbiAgICAgICAgICAgICAgICBjYWNoZS5yZWNvcmQubWF4RHJhd2Rvd25UaW1lID0gYnJva2VyVGltZTtcbiAgICAgICAgICAgICAgICBpZigoY2FjaGUudHJhY2tlckRhdGEucmVsYXRpdmVEcmF3ZG93blRocmVzaG9sZCAmJiBcbiAgICAgICAgICAgICAgICAgIGNhY2hlLnRyYWNrZXJEYXRhLnJlbGF0aXZlRHJhd2Rvd25UaHJlc2hvbGQgPCByZWxhdGl2ZURyYXdkb3duKSB8fCBcbiAgICAgICAgICAgICAgICAgIChjYWNoZS50cmFja2VyRGF0YS5hYnNvbHV0ZURyYXdkb3duVGhyZXNob2xkICYmXG4gICAgICAgICAgICAgICAgICAgIGNhY2hlLnRyYWNrZXJEYXRhLmFic29sdXRlRHJhd2Rvd25UaHJlc2hvbGQgPCBhYnNvbHV0ZURyYXdkb3duKSkge1xuICAgICAgICAgICAgICAgICAgY2FjaGUucmVjb3JkLnRocmVzaG9sZEV4Y2VlZGVkID0gdHJ1ZTtcbiAgICAgICAgICAgICAgICAgIGNhY2hlLnJlY29yZC5leGNlZWRlZFRocmVzaG9sZFR5cGUgPSAnZHJhd2Rvd24nO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICBpZihjYWNoZS5yZWNvcmQubWF4QWJzb2x1dGVQcm9maXQgPCBhYnNvbHV0ZVByb2ZpdCkge1xuICAgICAgICAgICAgICAgIGNhY2hlLnJlY29yZC5tYXhBYnNvbHV0ZVByb2ZpdCA9IGFic29sdXRlUHJvZml0O1xuICAgICAgICAgICAgICAgIGNhY2hlLnJlY29yZC5tYXhSZWxhdGl2ZVByb2ZpdCA9IHJlbGF0aXZlUHJvZml0O1xuICAgICAgICAgICAgICAgIGNhY2hlLnJlY29yZC5tYXhQcm9maXRUaW1lID0gYnJva2VyVGltZTtcbiAgICAgICAgICAgICAgICBpZigoY2FjaGUudHJhY2tlckRhdGEucmVsYXRpdmVQcm9maXRUaHJlc2hvbGQgJiYgXG4gICAgICAgICAgICAgICAgICBjYWNoZS50cmFja2VyRGF0YS5yZWxhdGl2ZVByb2ZpdFRocmVzaG9sZCA8IHJlbGF0aXZlUHJvZml0KSB8fFxuICAgICAgICAgICAgICAgICAgKGNhY2hlLnRyYWNrZXJEYXRhLmFic29sdXRlUHJvZml0VGhyZXNob2xkICYmXG4gICAgICAgICAgICAgICAgICAgIGNhY2hlLnRyYWNrZXJEYXRhLmFic29sdXRlUHJvZml0VGhyZXNob2xkIDwgYWJzb2x1dGVQcm9maXQpKSB7XG4gICAgICAgICAgICAgICAgICBjYWNoZS5yZWNvcmQudGhyZXNob2xkRXhjZWVkZWQgPSB0cnVlO1xuICAgICAgICAgICAgICAgICAgY2FjaGUucmVjb3JkLmV4Y2VlZGVkVGhyZXNob2xkVHlwZSA9ICdwcm9maXQnO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICBpZihKU09OLnN0cmluZ2lmeShjYWNoZS5yZWNvcmQpICE9PSBwcmV2aW91c1JlY29yZCkge1xuICAgICAgICAgICAgICAgIE9iamVjdC52YWx1ZXMoZ2V0VHJhY2tlckxpc3RlbmVycygpKS5mb3JFYWNoKHRyYWNrZXJMaXN0ZW5lciA9PiB7XG4gICAgICAgICAgICAgICAgICB0cmFja2VyTGlzdGVuZXIub25QZXJpb2RTdGF0aXN0aWNzVXBkYXRlZChbe1xuICAgICAgICAgICAgICAgICAgICBzdGFydEJyb2tlclRpbWU6IGNhY2hlLmxhc3RQZXJpb2Quc3RhcnRCcm9rZXJUaW1lLFxuICAgICAgICAgICAgICAgICAgICBlbmRCcm9rZXJUaW1lOiBjYWNoZS5sYXN0UGVyaW9kLmVuZEJyb2tlclRpbWUsXG4gICAgICAgICAgICAgICAgICAgIGluaXRpYWxCYWxhbmNlOiBjYWNoZS5sYXN0UGVyaW9kLmluaXRpYWxCYWxhbmNlLFxuICAgICAgICAgICAgICAgICAgICBtYXhBYnNvbHV0ZURyYXdkb3duOiBjYWNoZS5yZWNvcmQubWF4QWJzb2x1dGVEcmF3ZG93bixcbiAgICAgICAgICAgICAgICAgICAgbWF4QWJzb2x1dGVQcm9maXQ6IGNhY2hlLnJlY29yZC5tYXhBYnNvbHV0ZVByb2ZpdCxcbiAgICAgICAgICAgICAgICAgICAgbWF4RHJhd2Rvd25UaW1lOiBjYWNoZS5yZWNvcmQubWF4RHJhd2Rvd25UaW1lLFxuICAgICAgICAgICAgICAgICAgICBtYXhQcm9maXRUaW1lOiBjYWNoZS5yZWNvcmQubWF4UHJvZml0VGltZSxcbiAgICAgICAgICAgICAgICAgICAgbWF4UmVsYXRpdmVEcmF3ZG93bjogY2FjaGUucmVjb3JkLm1heFJlbGF0aXZlRHJhd2Rvd24sXG4gICAgICAgICAgICAgICAgICAgIG1heFJlbGF0aXZlUHJvZml0OiBjYWNoZS5yZWNvcmQubWF4UmVsYXRpdmVQcm9maXQsXG4gICAgICAgICAgICAgICAgICAgIHBlcmlvZDogY2FjaGUubGFzdFBlcmlvZC5wZXJpb2QsXG4gICAgICAgICAgICAgICAgICAgIGV4Y2VlZGVkVGhyZXNob2xkVHlwZTogY2FjaGUucmVjb3JkLmV4Y2VlZGVkVGhyZXNob2xkVHlwZSxcbiAgICAgICAgICAgICAgICAgICAgdGhyZXNob2xkRXhjZWVkZWQ6IGNhY2hlLnJlY29yZC50aHJlc2hvbGRFeGNlZWRlZCxcbiAgICAgICAgICAgICAgICAgICAgdHJhZGVEYXlDb3VudDogY2FjaGUucmVjb3JkLnRyYWRlRGF5Q291bnRcbiAgICAgICAgICAgICAgICAgIH1dKTtcbiAgICAgICAgICAgICAgICB9KTtcbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSBjYXRjaCAoZXJyKSB7XG4gICAgICAgICAgT2JqZWN0LnZhbHVlcyhnZXRUcmFja2VyTGlzdGVuZXJzKCkpLmZvckVhY2godHJhY2tlckxpc3RlbmVyID0+IHtcbiAgICAgICAgICAgIHRyYWNrZXJMaXN0ZW5lci5vbkVycm9yKGVycik7XG4gICAgICAgICAgfSk7XG4gICAgICAgICAgdGhpcy5fbG9nZ2VyLmVycm9yKCdFcnJvciBwcm9jZXNzaW5nIG9uU3ltYm9sUHJpY2VVcGRhdGVkIGV2ZW50IGZvciAnICtcbiAgICAgICAgICBgcGVyaW9kIHN0YXRpc3RpY3MgbGlzdGVuZXIgZm9yIGFjY291bnQgJHthY2NvdW50SWR9YCwgZXJyKTtcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICBhc3luYyBvbkRlYWxBZGRlZChpbnN0YW5jZUluZGV4LCBkZWFsKSB7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgaWYoIWNhY2hlLmxhc3RQZXJpb2QgfHwgIU9iamVjdC5rZXlzKGNhY2hlLmxhc3RQZXJpb2QpLmxlbmd0aCkge1xuICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICAgIH1cbiAgICAgICAgICBpZihkZWFsLnR5cGUgPT09ICdERUFMX1RZUEVfQkFMQU5DRScpIHtcbiAgICAgICAgICAgIGNhY2hlLmVxdWl0eUFkanVzdG1lbnRzW2RlYWwuaWRdID0gZGVhbC5wcm9maXQ7XG4gICAgICAgICAgfVxuICAgICAgICAgIGNvbnN0IGlnbm9yZWREZWFsVHlwZXMgPSBbJ0RFQUxfVFlQRV9CQUxBTkNFJywgJ0RFQUxfVFlQRV9DUkVESVQnXTtcbiAgICAgICAgICBpZighaWdub3JlZERlYWxUeXBlcy5pbmNsdWRlcyhkZWFsLnR5cGUpKSB7XG4gICAgICAgICAgICBjb25zdCB0aW1lRGlmZiA9IG5ldyBEYXRlKGRlYWwudGltZSkuZ2V0VGltZSgpIC0gbmV3IERhdGUoZGVhbC5icm9rZXJUaW1lKS5nZXRUaW1lKCk7XG4gICAgICAgICAgICBjb25zdCBzdGFydFNlYXJjaERhdGUgPSBuZXcgRGF0ZShuZXcgRGF0ZShjYWNoZS5sYXN0UGVyaW9kLnN0YXJ0QnJva2VyVGltZSkuZ2V0VGltZSgpICsgdGltZURpZmYpO1xuICAgICAgICAgICAgY29uc3QgZGVhbHMgPSBjb25uZWN0aW9uLmhpc3RvcnlTdG9yYWdlLmdldERlYWxzQnlUaW1lUmFuZ2Uoc3RhcnRTZWFyY2hEYXRlLCBuZXcgRGF0ZSg4NjQwMDAwMDAwMDAwMDAwKSlcbiAgICAgICAgICAgICAgLmZpbHRlcihkZWFsSXRlbSA9PiAhaWdub3JlZERlYWxUeXBlcy5pbmNsdWRlcyhkZWFsSXRlbS50eXBlKSk7XG4gICAgICAgICAgICBkZWFscy5wdXNoKGRlYWwpO1xuICAgICAgICAgICAgY29uc3QgdHJhZGVkRGF5cyA9IHt9O1xuICAgICAgICAgICAgZGVhbHMuZm9yRWFjaChkZWFsSXRlbSA9PiB7XG4gICAgICAgICAgICAgIHRyYWRlZERheXNbZGVhbEl0ZW0uYnJva2VyVGltZS5zbGljZSgwLCAxMCldID0gdHJ1ZTtcbiAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgY29uc3QgdHJhZGVEYXlDb3VudCA9IE9iamVjdC5rZXlzKHRyYWRlZERheXMpLmxlbmd0aDtcbiAgICAgICAgICAgIGlmKGNhY2hlLnJlY29yZC50cmFkZURheUNvdW50ICE9PSB0cmFkZURheUNvdW50KSB7XG4gICAgICAgICAgICAgIGNhY2hlLnJlY29yZC50cmFkZURheUNvdW50ID0gdHJhZGVEYXlDb3VudDtcbiAgICAgICAgICAgICAgT2JqZWN0LnZhbHVlcyhnZXRUcmFja2VyTGlzdGVuZXJzKCkpLmZvckVhY2godHJhY2tlckxpc3RlbmVyID0+IHtcbiAgICAgICAgICAgICAgICB0cmFja2VyTGlzdGVuZXIub25QZXJpb2RTdGF0aXN0aWNzVXBkYXRlZChbe1xuICAgICAgICAgICAgICAgICAgc3RhcnRCcm9rZXJUaW1lOiBjYWNoZS5sYXN0UGVyaW9kLnN0YXJ0QnJva2VyVGltZSxcbiAgICAgICAgICAgICAgICAgIGVuZEJyb2tlclRpbWU6IGNhY2hlLmxhc3RQZXJpb2QuZW5kQnJva2VyVGltZSxcbiAgICAgICAgICAgICAgICAgIGluaXRpYWxCYWxhbmNlOiBjYWNoZS5sYXN0UGVyaW9kLmluaXRpYWxCYWxhbmNlLFxuICAgICAgICAgICAgICAgICAgbWF4QWJzb2x1dGVEcmF3ZG93bjogY2FjaGUucmVjb3JkLm1heEFic29sdXRlRHJhd2Rvd24sXG4gICAgICAgICAgICAgICAgICBtYXhBYnNvbHV0ZVByb2ZpdDogY2FjaGUucmVjb3JkLm1heEFic29sdXRlUHJvZml0LFxuICAgICAgICAgICAgICAgICAgbWF4RHJhd2Rvd25UaW1lOiBjYWNoZS5yZWNvcmQubWF4RHJhd2Rvd25UaW1lLFxuICAgICAgICAgICAgICAgICAgbWF4UHJvZml0VGltZTogY2FjaGUucmVjb3JkLm1heFByb2ZpdFRpbWUsXG4gICAgICAgICAgICAgICAgICBtYXhSZWxhdGl2ZURyYXdkb3duOiBjYWNoZS5yZWNvcmQubWF4UmVsYXRpdmVEcmF3ZG93bixcbiAgICAgICAgICAgICAgICAgIG1heFJlbGF0aXZlUHJvZml0OiBjYWNoZS5yZWNvcmQubWF4UmVsYXRpdmVQcm9maXQsXG4gICAgICAgICAgICAgICAgICBwZXJpb2Q6IGNhY2hlLmxhc3RQZXJpb2QucGVyaW9kLFxuICAgICAgICAgICAgICAgICAgZXhjZWVkZWRUaHJlc2hvbGRUeXBlOiBjYWNoZS5yZWNvcmQuZXhjZWVkZWRUaHJlc2hvbGRUeXBlLFxuICAgICAgICAgICAgICAgICAgdGhyZXNob2xkRXhjZWVkZWQ6IGNhY2hlLnJlY29yZC50aHJlc2hvbGRFeGNlZWRlZCxcbiAgICAgICAgICAgICAgICAgIHRyYWRlRGF5Q291bnQ6IGNhY2hlLnJlY29yZC50cmFkZURheUNvdW50XG4gICAgICAgICAgICAgICAgfV0pO1xuICAgICAgICAgICAgICB9KTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgICAgIE9iamVjdC52YWx1ZXMoZ2V0VHJhY2tlckxpc3RlbmVycygpKS5mb3JFYWNoKHRyYWNrZXJMaXN0ZW5lciA9PiB7XG4gICAgICAgICAgICB0cmFja2VyTGlzdGVuZXIub25FcnJvcihlcnIpO1xuICAgICAgICAgIH0pO1xuICAgICAgICAgIHRoaXMuX2xvZ2dlci5lcnJvcignRXJyb3IgcHJvY2Vzc2luZyBvbkRlYWxBZGRlZCBldmVudCBmb3IgJyArXG4gICAgICAgICAgYHBlcmlvZCBzdGF0aXN0aWNzIGxpc3RlbmVyIGZvciBhY2NvdW50ICR7YWNjb3VudElkfWAsIGVycik7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICBjb25zdCBhY2NvdW50ID0gYXdhaXQgdGhpcy5fbWV0YUFwaS5tZXRhdHJhZGVyQWNjb3VudEFwaS5nZXRBY2NvdW50KGFjY291bnRJZCk7XG4gICAgY29uc3QgdHJhY2tlciA9IGF3YWl0IGVxdWl0eVRyYWNraW5nQ2xpZW50LmdldFRyYWNrZXIoYWNjb3VudElkLCB0cmFja2VySWQpO1xuICAgIGNhY2hlLnRyYWNrZXJEYXRhID0gdHJhY2tlcjtcbiAgICBpZighdGhpcy5fcGVyaW9kU3RhdGlzdGljc0xpc3RlbmVyc1thY2NvdW50SWRdKSB7XG4gICAgICB0aGlzLl9wZXJpb2RTdGF0aXN0aWNzTGlzdGVuZXJzW2FjY291bnRJZF0gPSB7fTtcbiAgICB9XG4gICAgaWYoIXRoaXMuX3BlcmlvZFN0YXRpc3RpY3NMaXN0ZW5lcnNbYWNjb3VudElkXVt0cmFja2VySWRdKSB7XG4gICAgICB0aGlzLl9wZXJpb2RTdGF0aXN0aWNzTGlzdGVuZXJzW2FjY291bnRJZF1bdHJhY2tlcklkXSA9IHt9O1xuICAgIH1cbiAgICBjb25zdCBhY2NvdW50TGlzdGVuZXJzID0gdGhpcy5fcGVyaW9kU3RhdGlzdGljc0xpc3RlbmVyc1thY2NvdW50SWRdW3RyYWNrZXJJZF07XG4gICAgYWNjb3VudExpc3RlbmVyc1tsaXN0ZW5lcklkXSA9IGxpc3RlbmVyO1xuICAgIHRoaXMuX2FjY291bnRzQnlMaXN0ZW5lcklkW2xpc3RlbmVySWRdID0gYWNjb3VudElkO1xuICAgIHRoaXMuX3RyYWNrZXJzQnlMaXN0ZW5lcklkW2xpc3RlbmVySWRdID0gdHJhY2tlcklkO1xuICAgIGxldCBpc0RlcGxveWVkID0gZmFsc2U7XG4gICAgd2hpbGUoIWlzRGVwbG95ZWQpIHtcbiAgICAgIHRyeSB7XG4gICAgICAgIGF3YWl0IGFjY291bnQud2FpdERlcGxveWVkKCk7XG4gICAgICAgIGlzRGVwbG95ZWQgPSB0cnVlOyAgXG4gICAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgbGlzdGVuZXIub25FcnJvcihlcnIpO1xuICAgICAgICB0aGlzLl9sb2dnZXIuZXJyb3IoYEVycm9yIHdhaXQgZm9yIGFjY291bnQgJHthY2NvdW50SWR9IHRvIGRlcGxveSwgcmV0cnlpbmdgLCBlcnIpO1xuICAgICAgICBhd2FpdCBuZXcgUHJvbWlzZShyZXMgPT4gc2V0VGltZW91dChyZXMsIHJldHJ5SW50ZXJ2YWxJblNlY29uZHMgKiAxMDAwKSk7IFxuICAgICAgICByZXRyeUludGVydmFsSW5TZWNvbmRzID0gTWF0aC5taW4ocmV0cnlJbnRlcnZhbEluU2Vjb25kcyAqIDIsIDMwMCk7XG4gICAgICB9XG4gICAgfVxuICAgIGlmKCF0aGlzLl9wZXJpb2RTdGF0aXN0aWNzQ29ubmVjdGlvbnNbYWNjb3VudElkXSkge1xuICAgICAgcmV0cnlJbnRlcnZhbEluU2Vjb25kcyA9IHRoaXMuX3JldHJ5SW50ZXJ2YWxJblNlY29uZHM7XG4gICAgICBjb25uZWN0aW9uID0gYWNjb3VudC5nZXRTdHJlYW1pbmdDb25uZWN0aW9uKCk7XG4gICAgICBjb25zdCBzeW5jTGlzdGVuZXIgPSBuZXcgUGVyaW9kU3RhdGlzdGljc1N0cmVhbUxpc3RlbmVyKCk7XG4gICAgICBjb25uZWN0aW9uLmFkZFN5bmNocm9uaXphdGlvbkxpc3RlbmVyKHN5bmNMaXN0ZW5lcik7XG4gICAgICB0aGlzLl9wZXJpb2RTdGF0aXN0aWNzQ29ubmVjdGlvbnNbYWNjb3VudElkXSA9IGNvbm5lY3Rpb247XG4gICAgICB0aGlzLl9zeW5jTGlzdGVuZXJzW3RyYWNrZXJJZF0gPSBzeW5jTGlzdGVuZXI7XG4gICAgICBcbiAgICAgIGxldCBpc1N5bmNocm9uaXplZCA9IGZhbHNlO1xuICAgICAgd2hpbGUoIWlzU3luY2hyb25pemVkKSB7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgYXdhaXQgY29ubmVjdGlvbi5jb25uZWN0KCk7XG4gICAgICAgICAgYXdhaXQgY29ubmVjdGlvbi53YWl0U3luY2hyb25pemVkKCk7XG4gICAgICAgICAgaXNTeW5jaHJvbml6ZWQgPSB0cnVlO1xuICAgICAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgICBsaXN0ZW5lci5vbkVycm9yKGVycik7XG4gICAgICAgICAgdGhpcy5fbG9nZ2VyLmVycm9yKCdFcnJvciBjb25maWd1cmluZyBwZXJpb2Qgc3RhdGlzdGljcyBzdHJlYW0gbGlzdGVuZXIgZm9yICcgK1xuICAgICAgICAgIGBhY2NvdW50ICR7YWNjb3VudElkfSwgcmV0cnlpbmdgLCBlcnIpO1xuICAgICAgICAgIGF3YWl0IG5ldyBQcm9taXNlKHJlcyA9PiBzZXRUaW1lb3V0KHJlcywgcmV0cnlJbnRlcnZhbEluU2Vjb25kcyAqIDEwMDApKTsgXG4gICAgICAgICAgcmV0cnlJbnRlcnZhbEluU2Vjb25kcyA9IE1hdGgubWluKHJldHJ5SW50ZXJ2YWxJblNlY29uZHMgKiAyLCAzMDApO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgICByZXRyeUludGVydmFsSW5TZWNvbmRzID0gdGhpcy5fcmV0cnlJbnRlcnZhbEluU2Vjb25kcztcbiAgICB9IGVsc2Uge1xuICAgICAgY29ubmVjdGlvbiA9IHRoaXMuX3BlcmlvZFN0YXRpc3RpY3NDb25uZWN0aW9uc1thY2NvdW50SWRdO1xuICAgICAgaWYobmV3VHJhY2tlcikge1xuICAgICAgICBjb25zdCBzeW5jTGlzdGVuZXIgPSBuZXcgUGVyaW9kU3RhdGlzdGljc1N0cmVhbUxpc3RlbmVyKCk7XG4gICAgICAgIGNvbm5lY3Rpb24uYWRkU3luY2hyb25pemF0aW9uTGlzdGVuZXIoc3luY0xpc3RlbmVyKTtcbiAgICAgICAgdGhpcy5fc3luY0xpc3RlbmVyc1t0cmFja2VySWRdID0gc3luY0xpc3RlbmVyO1xuICAgICAgfVxuICAgICAgaWYoIWNvbm5lY3Rpb24uaGVhbHRoTW9uaXRvci5oZWFsdGhTdGF0dXMuc3luY2hyb25pemVkKSB7XG4gICAgICAgIGlmKCF0aGlzLl9wZW5kaW5nSW5pdGFsaXphdGlvblJlc29sdmVzW2FjY291bnRJZF0pI