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
text/typescript
'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');
}
}
}