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)

217 lines (196 loc) 7.1 kB
import randomstring from 'randomstring'; import * as lrap from '../long-running-async-process'; import StickySocketConnection from './stickySocketConnection'; import * as errors from '../../clients/errorHandler'; import {destroyInternalClientSocket} from './common.utils'; import {msToSeconds} from '../../helpers/convert/time'; import EventEmitter from '../../tools/eventEmitter'; import LoggerManager from '../../logger'; import Queue from '../../tools/queue'; /** * Client sticky socket */ class ClientStickySocket extends EventEmitter<ClientStickySocket.Events> { private _logger = LoggerManager.getLogger('ClientStickySocket'); private _pool: lrap.RootPool<StickySocketConnection>; private _stickyConnectionWaitTimeout?: NodeJS.Timeout; private _id = randomstring.generate(16); private _label: string; private _stopped = false; private _connected = false; /** * Constructs instance * @param url URL * @param options options */ constructor(url: string, options?: ClientStickySocket.Options) { super(); this._label = `${options?.label || 'connection'}:${this._id}`; const stickyConnectionTtlInMs = 1000 * (options?.stickyConnectionTtlInSeconds ?? 10); let internalEvents = new EventEmitter<StickySocketConnection.SharedEvents>(); internalEvents.once('connect', () => { this._connected = true; this.emit('connect'); }); internalEvents.on('connect', () => clearTimeout(this._stickyConnectionWaitTimeout)); internalEvents.on('fail', err => { if (options?.useNativeSocketIoServer) { this.disconnect(err); return; } if (!this._stopped) { this._stickyConnectionWaitTimeout = setTimeout(() => { this._logger.warn(`${this._label}: failed to restore sticky connection`); this.disconnect(err); }, stickyConnectionTtlInMs); } }); this._pool = new lrap.RootPool(StickySocketConnection, { dependencies: [this, internalEvents], label: `clientStickySocket:${this._label}`, }); this._pool.scheduleProcess('connection', { args: [ url, this._id, { startCount: 0, emitHistory: new Queue(), lastSentIndex: -1, lastReceivedIndex: -1 }, { label: this._label, connection: options?.connection, useNativeSocketIoServer: options?.useNativeSocketIoServer, emitHistoryTtlInSeconds: options?.emitHistoryTtlInSeconds ?? msToSeconds(stickyConnectionTtlInMs) } ], failoverThrottleDelay: { mode: 'exponential', resetDelayInMs: 0, minDelayInMs: options?.reconnectionDelayInMs ?? 1000, maxDelayInMs: options?.reconnectionDelayMaxInMs ?? 10000, randomizationFactor: options?.reconnectionRandomizationFactor ?? 0.5 } }); } /** * Returns socket ID * @returns socket ID */ get id(): string { return this._id; } /** * Returns current native socket * @returns socket.io socket * @internal intended for tests only */ get socket(): SocketIOClient.Socket { return this._pool.getProcess('connection')?.socket; } /** * Returns current transport name, using socket.io internals * @returns transport name, e.g. "websocket" * @internal intended for tests only */ get transportName(): string { return this._pool.getProcess('connection')?.transportName; } /** * Returns first present packet index in last emit history * @returns first history index * @internal intended for tests only */ get firstHistoryIndex(): number | undefined { return this._pool.getProcess('connection')?.firstHistoryIndex; } /** * Returns whether socket connected * @returns is connected */ get connected(): boolean { return this._connected; } /** * @inheritdoc * @remarks Socket events `ClientStickySockets.INTERNAL_EVENTS` cannot be subscribed to * @throws `ValidationError` if attempting to subscribe to an internal event */ on<U extends EventEmitter.Event<ClientStickySocket.Events>>(event: U, callback: ClientStickySocket.Events[U]) { if (ClientStickySocket.INTERNAL_EVENTS.includes(event)) { throw new errors.ValidationError('Cannot subscribe to an internal event'); } super.on(event, callback); } /** * Emits a data event. All events are buffered if no connection * @param event The event that we're emitting * @param args Optional arguments to send with the event */ send(event: string, ...args: any[]) { this._pool.getProcess('connection')?.send(event, ...args); } /** * Disconnects socket * @param err error if disconnecting with error * @returns promise resolving when disconnected */ async disconnect(err?: Error) { clearTimeout(this._stickyConnectionWaitTimeout); let wasStopped = this._stopped; this._stopped = true; await this._pool.stop(); if (!wasStopped) { this._connected = false; this.emit('disconnect', err); } } /** * Destroys underlying socket, using socket.io internals. The socket must have `websocket` transport * @param err optional error * @internal intended for tests only, e.g. to test connection loss */ destroyInternalSocket(err?: Error) { destroyInternalClientSocket(this._pool.getProcess('connection').socket, err); } /** * Removes expired emit history * @internal intended for tests only */ removeExpiredEmitHistory() { this._pool.getProcess('connection')?.removeExpiredEmitHistory(); } } namespace ClientStickySocket { /** Constructing options */ export type Options = Pick<StickySocketConnection.Options, 'connection' | 'label' | 'useNativeSocketIoServer'> & { /** * Emit history buffer TTL. Defaults to `stickyConnectionTtlInSeconds` + `reconnectThrottleDelayInMs` * (converted to seconds) */ emitHistoryTtlInSeconds?: number; /** Timeout within of which a connection can be restored as previous one. Defaults to `10` */ stickyConnectionTtlInSeconds?: number; /** Reconnection delay. The delay will be increased by 2x each failed connection attempt. Defaults to `1000` */ reconnectionDelayInMs?: number; /** Max reconnection delay. Defaults to `10000` */ reconnectionDelayMaxInMs?: number; /** Reconnection delay randomization factor in range [0; 1]. Defaults to `0.5` */ reconnectionRandomizationFactor?: number; }; /** Event listeners */ export type Events = { /** Fired only once upon first connection */ connect: () => void; /** Fired only once upon the last disconnection. Will be fired anyway, even if was never connected */ disconnect: (err?: Error) => void; /** Custom subscription events */ [event: string]: (...args: any[]) => any; }; /** Connection events */ export const CONNECTION_EVENTS = StickySocketConnection.CONNECTION_EVENTS; /** Internal events */ export const INTERNAL_EVENTS = StickySocketConnection.INTERNAL_EVENTS; } export default ClientStickySocket;