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