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)

207 lines (188 loc) 7.15 kB
'use strict'; import MetaApiWebsocketClient from '../clients/metaApi/metaApiWebsocket.client'; import SynchronizationListener from '../clients/metaApi/synchronizationListener'; import LoggerManager, {Logger} from '../logger'; import MetatraderAccount from './metatraderAccount'; /** * Exposes MetaApi MetaTrader API connection to consumers */ export default class MetaApiConnection extends SynchronizationListener { protected _options: any; protected _websocketClient: MetaApiWebsocketClient; protected _latencyService: any; protected _account: MetatraderAccount; protected _logger: Logger; protected _application: any; protected _refreshTasks: {}; protected _connectionRegistry: any; protected _closed: any; protected _stateByInstanceIndex: any; protected _opened: any; /** * @typedef Config MetaApi options for connections * @property {Options} [connections] MetaApi connections options. Only for tests. Will be ignored when set in SDK */ /** * @typedef Options MetaApiConnection options * @property {number} [refreshReplicasMaxDelayInMs = 6 * 60 * 60 * 1000] max delay before refreshing replicas delay */ /** * Constructs MetaApi MetaTrader Api connection * @param {MetaApiOpts & Config} options MetaApi options * @param {MetaApiWebsocketClient} websocketClient MetaApi websocket client * @param {MetatraderAccount} account MetaTrader account id to connect to * @param {String} [application] application to use */ constructor(options, websocketClient, account, application?) { super(); this._options = options; this._websocketClient = websocketClient; this._latencyService = websocketClient.latencyService; this._account = account; this._logger = LoggerManager.getLogger('MetaApiConnection'); this._application = application; this._refreshReplicas = this._refreshReplicas.bind(this); this._refreshTasks = {}; } /** * 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) {} /** * 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) {} /** * Returns MetaApi account * @return {MetatraderAccount} MetaApi account */ get account() { return this._account; } /** * Returns connection application * @return {String} connection application */ get application() { return this._application; } /** * Schedules the refresh task * @param {string} region replica region */ scheduleRefresh(region) { if (!this._refreshTasks[region]) { const delay = Math.random() * (this._options.connections?.refreshReplicasMaxDelayInMs ?? 6 * 60 * 60 * 1000); this._refreshTasks[region] = setTimeout(this._refreshReplicas, delay); } } /** * Cancels the scheduled refresh task * @param {string} region replica region */ cancelRefresh(region) { clearTimeout(this._refreshTasks[region]); delete this._refreshTasks[region]; } /** * Refreshes account replicas */ async _refreshReplicas() { Object.values<any>(this._refreshTasks).forEach(task => clearTimeout(task)); this._refreshTasks = {}; const oldReplicas: MetatraderAccount.AccountsByRegion = {}; this._account.replicas.forEach(replica => oldReplicas[replica.region] = replica.id); const newReplicas: MetatraderAccount.AccountsByRegion = {}; let isAccountUpdated = false; try { await this._account.reload(); isAccountUpdated = true; this._account.replicas.forEach(replica => newReplicas[replica.region] = replica.id); } catch (error) { if (error.name === 'NotFoundError') { if (this._connectionRegistry) { this._connectionRegistry.closeAllInstances(this._account.id); } } } if (isAccountUpdated) { const deletedReplicas = {}; const addedReplicas = {}; Object.keys(oldReplicas).forEach(key => { if (newReplicas[key] !== oldReplicas[key]) { deletedReplicas[key] = oldReplicas[key]; } }); Object.keys(newReplicas).forEach(key => { if (newReplicas[key] !== oldReplicas[key]) { addedReplicas[key] = newReplicas[key]; } }); if (Object.keys(deletedReplicas).length) { Object.values(deletedReplicas).forEach(replicaId => this._websocketClient.onAccountDeleted(replicaId)); } if (Object.keys(deletedReplicas).length || Object.keys(addedReplicas).length) { newReplicas[this._account.region] = this._account.id; this._websocketClient.updateAccountCache(this._account.id, newReplicas); Object.entries(this._account.accountRegions).forEach(([region, instance]) => { if (!this._options.region || this._options.region === region) { this._websocketClient.ensureSubscribe(instance, 0); this._websocketClient.ensureSubscribe(instance, 1); } }); } } } async _ensureSynchronized(instanceIndex, key) { let state = this._getState(instanceIndex); if (state && !this._closed) { try { const synchronizationResult = await this.synchronize(instanceIndex); if (synchronizationResult) { state.synchronized = true; state.synchronizationRetryIntervalInSeconds = 1; } } 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) { setTimeout(this._ensureSynchronized.bind(this, instanceIndex, key), state.synchronizationRetryIntervalInSeconds * 1000); state.synchronizationRetryIntervalInSeconds = Math.min(state.synchronizationRetryIntervalInSeconds * 2, 300); } } } } synchronize(instanceIndex: any) { return undefined; } _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]; } _checkIsConnectionActive() { if (!this._opened) { throw new Error('This connection has not been initialized yet, please invoke await connection.connect()'); } if (this._closed) { throw new Error('This connection has been closed, please create a new connection'); } } }