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)

291 lines (274 loc) 9.36 kB
'use strict'; 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); }