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)
291 lines (274 loc) • 9.36 kB
text/typescript
;
import TimeoutError from '../clients/timeoutError';
import LoggerManager from '../logger';
import {PromiseOrNot} from '../types/util';
import _ from 'lodash';
const realSetTimeout = setTimeout;
const realDateNow = Date.now.bind(Date);
/** An extended promise with additional properties and methods */
export interface HandlePromise<T> extends Promise<T> {
/** Whether the promise is resolved or rejected */
completed: boolean,
/** Whether the promise is resolved */
resolved?: boolean,
/** Whether the promise is rejected */
rejected?: boolean,
/** Result value the promise resolved with */
result?: T,
/** Error the promise rejected with */
error?: Error,
/**
* Resolves the promise with specified value
* @param result Value to resolve the promise with
*/
resolve(result?: T): void,
/**
* Rejects the promise with specified error
* @param err Error to reject the promise with
*/
reject(err: Error): void,
/**
* Adds a timeout to reject the promise with `TimeoutError`
* @param milliseconds timeout in milliseconds
* @param errorMessage error message
* @returns self
*/
timeout(milliseconds: number, errorMessage: string): HandlePromise<T>
}
/**
* Creates a promise that can be used as a handle. It will not raise errors when rejected until it is explicitly
* awaited or catch is set
* @returns modified handle promise
*/
export function createHandlePromise<T>(): HandlePromise<T> {
let resolve, reject;
let promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
}) as HandlePromise<T>;
promise.completed = false;
promise.resolve = (result) => {
if (!promise.completed) {
promise.completed = true;
promise.resolved = true;
promise.result = result;
resolve(result);
}
};
promise.reject = (err) => {
if (!promise.completed) {
promise.completed = true;
promise.rejected = true;
promise.error = err;
reject(err);
}
};
promise.timeout = (milliseconds, errorMessage) => {
if (!promise.completed) {
let timeout = setTimeout(() => promise.reject(new TimeoutError(errorMessage)), milliseconds);
promise.finally(() => clearTimeout(timeout)).catch(() => {});
}
return promise;
};
promise.catch(() => {});
return promise;
}
/**
* Wraps a promise into a handle promise
* @param promise native promise
* @returns handle promise
*/
export function wrapHandlePromise<T>(promise: Promise<T>): HandlePromise<T> {
let result = createHandlePromise<T>();
promise.then(result.resolve).catch(() => {});
promise.catch(result.reject);
return result;
}
/**
* This function ensures that a promise is returned
* @param call call
* @returns promise
*/
export async function ensurePromise<T>(call: () => PromiseOrNot<T>): Promise<T> {
return call();
}
/** Additional delay options */
export type DelayOptions = {
/** Whether to delay real time, if a stubbed frozen `sinon` clock is used */
ignoreSinonClock?: boolean
};
/**
* Waits specified delay
* @param ms Milliseconds to wait
* @param options Additional options
* @return promise resolving when the delay has ended
*/
export function delay(ms: number, options?: DelayOptions): DelayPromise {
let resolve: () => void;
let timeout: NodeJS.Timeout;
let canceled = false;
let result = new Promise<void>(res => {
timeout = options?.ignoreSinonClock ? realSetTimeout(res, ms) : setTimeout(res, ms);
resolve = res;
}) as DelayPromise;
Object.defineProperty(result, 'canceled', {
get: () => canceled,
enumerable: true,
configurable: true
});
result.cancel = () => {
canceled = true;
clearTimeout(timeout);
resolve();
};
return result;
}
/**
* Delay promise
*/
export interface DelayPromise extends Promise<void> {
/**
* Returns whether the promise is canceled
* @returns whether canceled
*/
get canceled(): boolean;
/**
* Cancels waiting and resolves the promise immediately
*/
cancel(): void;
}
/**
* Assembles log4js config from logging level map
* @param {Object} [config] log4js config
* @param {String} [config.defaultLevel = 'INFO'] Default logging level
* @param {Object} [config.levels] Logging levels
* @return {Object} Log4js config
*/
export function assembleLog4jsConfig(config: any = {}) {
let appenders = {console: {type: 'console'}};
let categories = {
default: {
appenders: Object.keys(appenders),
level: config.defaultLevel || 'INFO'
}
};
Object.keys(config.levels || {}).forEach((category) => {
categories[category] = {
appenders: Object.keys(appenders),
level: config.levels[category]
};
});
return {appenders, categories};
}
/** Options for `wait*` functions */
export type WaitOptions = {
/** Wait timeout in milliseconds. Defaults to `30000` */
timeoutInMs?: number
};
/**
* Waits untill specified callable will pass successfully and return true. Uses log4js logger named `helpers.wait`
* @param {() => boolean|Promise<boolean>} callable Callable to call until it returns true
* @param {Number} [intervalInMs = 25] Interval in milliseconds between the checks
* @param {WaitOptions & DelayOptions} [options] Additional wait options
* @return {Promise} Promise resolving with callable return value when waited
* @throws {Error|TimeoutError} Error from the callable or timeout error when timed out
*/
// eslint-disable-next-line complexity
export async function wait(callable, intervalInMs = 25, options?: WaitOptions & DelayOptions) {
const logger = LoggerManager.getLogger('helpers.wait');
if (typeof intervalInMs === 'object') {
// for backward compatibility
options = intervalInMs;
intervalInMs = _.defaultTo((options as any).intervalInMs, 1000);
}
const dateNow = options?.ignoreSinonClock ? realDateNow : () => Date.now();
let result = false, lastError;
let timesAt = dateNow() + _.defaultTo(options?.timeoutInMs, 30000);
while (!result && dateNow() < timesAt) {
try {
result = await callable();
} catch (err) {
lastError = err;
logger.debug('The executor failed', err);
if (dateNow() >= timesAt) {
throw err;
}
} finally {
if (!result) {
logger.debug('Waiting because the result is', result);
await delay(intervalInMs, options);
}
}
}
if (dateNow() >= timesAt) {
if (lastError) {
throw lastError;
}
throw new TimeoutError('Timed out till specified callable returns true');
}
return result;
}
/**
* Waits untill specified callable will pass successfully and return true. Uses log4js logger named `helpers.wait`
* @param {() => boolean|Promise<boolean>} callable Callable to call until it returns true
* @param {Number} [intervalInMs = 25] Interval in milliseconds between the checks
* @param {WaitOptions & DelayOptions} [options] Additional wait options
* @return {Promise} Promise resolving with callable return value when waited
* @throws {Error|TimeoutError} Error from the callable or timeout error when timed out
*/
export function waitTrue(callable, intervalInMs = 25, options?: WaitOptions & DelayOptions) {
return wait(callable, intervalInMs, options);
}
/**
* Waits untill specified callable will pass successfully. Uses log4js logger named `helpers.wait`
* @param {() => boolean|Promise<boolean>} callable Callable to call
* @param {Number} [intervalInMs = 25] Interval in milliseconds between the checks
* @param {WaitOptions & DelayOptions} [options] Additional wait options
* @return {Promise} Promise resolving with callable return value when waited
* @throws {Error|TimeoutError} Error from the callable or timeout error when timed out
*/
export async function waitPass<T = void>(
callable: () => PromiseOrNot<T>,
intervalInMs = 25,
options?: WaitOptions & DelayOptions
): Promise<T> {
let result;
await wait(async () => {
result = await callable();
return true;
}, intervalInMs, options);
return result;
}
/**
* Waits untill specified callable successfully returns any non-undefined value. Uses log4js logger named `helpers.wait`
* @param callable Callable to call
* @param intervalInMs Interval in milliseconds between the checks
* @param options Additional wait options
* @return Promise resolving with callable return value when waited
* @throws {Error|TimeoutError} Error from the callable or timeout error when timed out
*/
export async function waitAny<T>(
callable: () => PromiseOrNot<T>, intervalInMs = 25, options?: WaitOptions
): Promise<T> {
let result: T;
await waitTrue(async () => {
result = await callable();
if (result !== undefined) {
return true;
}
}, intervalInMs, options);
return result;
}
/**
* Calculates exponential backoff delay. At the initial iteration, there is no delay. At the next iteration, the delay
* is `startDelay`. Further, at the every next iteration the previous delay multiplies to 2
* @param iteration current iteration, where 0 is initial iteration without delaying
* @param startDelay start delay
* @param maxDelay maximum delay
*/
export function expBackoffDelay(iteration: number, startDelay: number, maxDelay: number) {
if (iteration === 0) {
return 0;
}
return Math.min(startDelay * Math.pow(2, iteration - 1), maxDelay);
}