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)
203 lines (188 loc) • 7.77 kB
text/typescript
'use strict';
/**
* Class which orders the synchronization packets
*/
class PacketOrderer {
private _outOfOrderListener: any;
private _orderingTimeoutInSeconds: any;
private _isOutOfOrderEmitted: {};
private _waitListSizeLimit: number;
private _sequenceNumberByInstance: {};
private _lastSessionStartTimestamp: {};
private _packetsByInstance: {};
private _outOfOrderInterval: any;
/**
* Constructs the class
* @param {Function} outOfOrderListener function which will receive out of order packet events
* @param {Number} orderingTimeoutInSeconds packet ordering timeout
*/
constructor(outOfOrderListener, orderingTimeoutInSeconds) {
this._outOfOrderListener = outOfOrderListener;
this._orderingTimeoutInSeconds = orderingTimeoutInSeconds;
this._isOutOfOrderEmitted = {};
this._waitListSizeLimit = 100;
this._sequenceNumberByInstance = {};
this._lastSessionStartTimestamp = {};
this._packetsByInstance = {};
}
/**
* Initializes the packet orderer
*/
start() {
this._sequenceNumberByInstance = {};
this._lastSessionStartTimestamp = {};
this._packetsByInstance = {};
if (!this._outOfOrderInterval) {
this._outOfOrderInterval = setInterval(() => this._emitOutOfOrderEvents(), 1000);
}
}
/**
* Deinitialized the packet orderer
*/
stop() {
clearInterval(this._outOfOrderInterval);
}
/**
* Processes the packet and resolves in the order of packet sequence number
* @param {Object} packet packet to process
* @return {Array<Object>} ordered packets when the packets are ready to be processed in order
*/
// eslint-disable-next-line complexity
restoreOrder<T extends PacketOrderer.Packet>(packet: T): PacketOrderer.Packet[] {
let instanceId = packet.accountId + ':' + (packet.instanceIndex || 0) + ':' + (packet.host || 0);
if (packet.sequenceNumber === undefined) {
return [packet];
}
if (packet.type === 'synchronizationStarted' && packet.synchronizationId &&
(!this._lastSessionStartTimestamp[instanceId] || this._lastSessionStartTimestamp[instanceId] <
packet.sequenceTimestamp)) {
// synchronization packet sequence just started
this._isOutOfOrderEmitted[instanceId] = false;
this._sequenceNumberByInstance[instanceId] = packet.sequenceNumber;
this._lastSessionStartTimestamp[instanceId] = packet.sequenceTimestamp;
this._packetsByInstance[instanceId] = (this._packetsByInstance[instanceId] || [])
.filter(waitPacket => waitPacket.packet.sequenceTimestamp >= packet.sequenceTimestamp);
return [packet].concat(this._findNextPacketsFromWaitList(instanceId));
} else if (packet.sequenceTimestamp < this._lastSessionStartTimestamp[instanceId]) {
// filter out previous packets
return [];
} else if (packet.sequenceNumber === this._sequenceNumberByInstance[instanceId]) {
// let the duplicate s/n packet to pass through
return [packet];
} else if (packet.sequenceNumber === this._sequenceNumberByInstance[instanceId] + 1) {
// in-order packet was received
this._sequenceNumberByInstance[instanceId]++;
this._lastSessionStartTimestamp[instanceId] = packet.sequenceTimestamp ||
this._lastSessionStartTimestamp[instanceId];
return [packet].concat(this._findNextPacketsFromWaitList(instanceId));
} else {
// out-of-order packet was received, add it to the wait list
this._packetsByInstance[instanceId] = this._packetsByInstance[instanceId] || [];
let waitList = this._packetsByInstance[instanceId];
waitList.push({
instanceId,
accountId: packet.accountId,
instanceIndex: packet.instanceIndex || 0,
sequenceNumber: packet.sequenceNumber,
packet: packet,
receivedAt: new Date()
});
waitList.sort((e1, e2) => e1.sequenceNumber - e2.sequenceNumber);
while (waitList.length > this._waitListSizeLimit) {
waitList.shift();
}
return [];
}
}
/**
* Resets state for instance id
* @param {String} instanceId instance id to reset state for
*/
onStreamClosed(instanceId) {
delete this._packetsByInstance[instanceId];
delete this._lastSessionStartTimestamp[instanceId];
delete this._sequenceNumberByInstance[instanceId];
}
/**
* Resets state for specified accounts on reconnect
* @param {String[]} reconnectAccountIds reconnected account ids
*/
onReconnected(reconnectAccountIds) {
Object.keys(this._packetsByInstance).forEach(instanceId => {
if(reconnectAccountIds.includes(this._getAccountIdFromInstance(instanceId))) {
delete this._packetsByInstance[instanceId];
}
});
Object.keys(this._lastSessionStartTimestamp).forEach(instanceId => {
if(reconnectAccountIds.includes(this._getAccountIdFromInstance(instanceId))) {
delete this._lastSessionStartTimestamp[instanceId];
}
});
Object.keys(this._sequenceNumberByInstance).forEach(instanceId => {
if(reconnectAccountIds.includes(this._getAccountIdFromInstance(instanceId))) {
delete this._sequenceNumberByInstance[instanceId];
}
});
}
_getAccountIdFromInstance(instanceId) {
return instanceId.split(':')[0];
}
// eslint-disable-next-line complexity
_findNextPacketsFromWaitList(instanceId) {
let result = [];
let waitList = this._packetsByInstance[instanceId] || [];
while (waitList.length && ([this._sequenceNumberByInstance[instanceId],
this._sequenceNumberByInstance[instanceId] + 1].includes(waitList[0].sequenceNumber) ||
waitList[0].packet.sequenceTimestamp < this._lastSessionStartTimestamp[instanceId])) {
if (waitList[0].packet.sequenceTimestamp >= this._lastSessionStartTimestamp[instanceId]) {
result.push(waitList[0].packet);
if (waitList[0].packet.sequenceNumber === this._sequenceNumberByInstance[instanceId] + 1) {
this._sequenceNumberByInstance[instanceId]++;
this._lastSessionStartTimestamp[instanceId] = waitList[0].packet.sequenceTimestamp ||
this._lastSessionStartTimestamp[instanceId];
}
}
waitList.splice(0, 1);
}
if (!waitList.length) {
delete this._packetsByInstance[instanceId];
}
return result;
}
_emitOutOfOrderEvents() {
for (let waitList of Object.values<any>(this._packetsByInstance)) {
if (waitList.length && waitList[0].receivedAt.getTime() + this._orderingTimeoutInSeconds * 1000 < Date.now()) {
const instanceId = waitList[0].instanceId;
if(!this._isOutOfOrderEmitted[instanceId]) {
this._isOutOfOrderEmitted[instanceId] = true;
// Do not emit onOutOfOrderPacket for packets that come before synchronizationStarted
if (this._sequenceNumberByInstance[instanceId] !== undefined) {
this._outOfOrderListener.onOutOfOrderPacket(waitList[0].accountId, waitList[0].instanceIndex,
this._sequenceNumberByInstance[instanceId] + 1, waitList[0].sequenceNumber, waitList[0].packet,
waitList[0].receivedAt);
}
}
}
}
}
}
namespace PacketOrderer {
/** Packet to order. Can be extended, the same input packet object reference will be returned */
export type Packet = {
/** Account ID */
accountId?: string;
/** Instance index. Defaults to `0` */
instanceIndex?: number;
/** Source server host. Defaults to `0` */
host?: string | number;
/** Packet type */
type?: string;
/** Sequence number */
sequenceNumber?: number;
/** Synchronization ID */
synchronizationId?: string;
/** Sequence timestamp */
sequenceTimestamp?: number;
};
}
export default PacketOrderer;