@hackape/tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
278 lines • 11.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PoolingClientBase = exports.MultiConnectionRealTimeFeedBase = exports.RealTimeFeedBase = void 0;
const debug_1 = __importDefault(require("debug"));
const ws_1 = __importDefault(require("ws"));
const stream_1 = require("stream");
const events_1 = require("events");
const handy_1 = require("../handy");
let connectionCounter = 1;
class RealTimeFeedBase {
constructor(_exchange, filters, _timeoutIntervalMS, _onError) {
this._exchange = _exchange;
this._timeoutIntervalMS = _timeoutIntervalMS;
this._onError = _onError;
this.throttleSubscribeMS = 0;
this.manualSnapshotsBuffer = [];
this._receivedMessagesCount = 0;
this._connectionId = connectionCounter++;
this._onConnectionEstabilished = async () => {
try {
const subscribeMessages = this.mapToSubscribeMessages(this._filters);
let symbolsCount = this._filters.reduce((prev, curr) => {
if (curr.symbols !== undefined) {
for (const symbol of curr.symbols) {
prev.add(symbol);
}
}
return prev;
}, new Set()).size;
for (const message of subscribeMessages) {
this.send(message);
if (this.throttleSubscribeMS > 0) {
await handy_1.wait(this.throttleSubscribeMS);
}
}
this.debug('(connection id: %d) estabilished connection', this._connectionId);
this.onConnected();
//wait before fetching snapshots until we're sure we've got proper connection estabilished (received some messages)
while (this._receivedMessagesCount < symbolsCount * 2) {
await handy_1.wait(100);
}
// wait a second just in case before starting fetching the snapshots
await handy_1.wait(1 * handy_1.ONE_SEC_IN_MS);
if (this._ws.readyState === ws_1.default.CLOSED) {
return;
}
await this.provideManualSnapshots(this._filters, () => this._ws.readyState === ws_1.default.CLOSED);
}
catch (e) {
this.debug('(connection id: %d) providing manual snapshots error: %o', this._connectionId, e);
this._ws.emit('error', e);
}
};
this._onConnectionClosed = () => {
this.debug('(connection id: %d) connection closed', this._connectionId);
};
this._filters = handy_1.optimizeFilters(filters);
this.debug = debug_1.default(`tardis-dev:realtime:${_exchange}`);
}
[Symbol.asyncIterator]() {
return this._stream();
}
async *_stream() {
let staleConnectionTimerId;
let pingTimerId;
let retries = 0;
while (true) {
try {
const subscribeMessages = this.mapToSubscribeMessages(this._filters);
this.debug('(connection id: %d) estabilishing connection to %s', this._connectionId, this.wssURL);
this.debug('(connection id: %d) provided filters: %o mapped to subscribe messages: %o', this._connectionId, this._filters, subscribeMessages);
this._ws = new ws_1.default(this.wssURL, { perMessageDeflate: false, handshakeTimeout: 10 * handy_1.ONE_SEC_IN_MS });
this._ws.onopen = this._onConnectionEstabilished;
this._ws.onclose = this._onConnectionClosed;
staleConnectionTimerId = this._monitorConnectionIfStale();
pingTimerId = this._sendPeriodicPing();
const realtimeMessagesStream = ws_1.default.createWebSocketStream(this._ws, {
readableObjectMode: true,
readableHighWaterMark: 8096 // since we're in object mode, let's increase hwm a little from default of 16 messages buffered
});
for await (let message of realtimeMessagesStream) {
if (this.decompress !== undefined) {
message = this.decompress(message);
}
const messageDeserialized = JSON.parse(message);
if (this.messageIsError(messageDeserialized)) {
throw new Error(`Received error message:${message.toString()}`);
}
// exclude heaartbeat messages from received messages counter
// connection could still be stale even if only heartbeats are provided without any data
if (this.messageIsHeartbeat(messageDeserialized) === false) {
this._receivedMessagesCount++;
}
this.onMessage(messageDeserialized);
yield messageDeserialized;
if (retries > 0) {
// reset retries counter as we've received correct message from the connection
retries = 0;
}
if (this.manualSnapshotsBuffer.length > 0) {
for (let snapshot of this.manualSnapshotsBuffer) {
yield snapshot;
}
this.manualSnapshotsBuffer.length = 0;
}
}
// clear monitoring connection timer and notify about disconnect
if (staleConnectionTimerId !== undefined) {
clearInterval(staleConnectionTimerId);
}
yield undefined;
}
catch (error) {
if (this._onError !== undefined) {
this._onError(error);
}
retries++;
const MAX_DELAY = 16 * 1000;
const isRateLimited = error.message.includes('429');
let delay;
if (isRateLimited) {
delay = MAX_DELAY * retries;
}
else {
delay = Math.pow(2, retries - 1) * 1000;
if (delay > MAX_DELAY) {
delay = MAX_DELAY;
}
}
this.debug('(connection id: %d) %s real-time feed connection error, retries count: %d, next retry delay: %dms, rate limited: %s error message: %o', this._connectionId, this._exchange, retries, delay, isRateLimited, error);
// clear monitoring connection timer and notify about disconnect
if (staleConnectionTimerId !== undefined) {
clearInterval(staleConnectionTimerId);
}
yield undefined;
await handy_1.wait(delay);
}
finally {
// stop timers
if (staleConnectionTimerId !== undefined) {
clearInterval(staleConnectionTimerId);
}
if (pingTimerId !== undefined) {
clearInterval(pingTimerId);
}
}
}
}
send(msg) {
if (this._ws === undefined) {
return;
}
if (this._ws.readyState !== ws_1.default.OPEN) {
return;
}
this._ws.send(JSON.stringify(msg));
}
messageIsHeartbeat(_msg) {
return false;
}
async provideManualSnapshots(_filters, _shouldCancel) { }
onMessage(_msg) { }
onConnected() { }
_monitorConnectionIfStale() {
if (this._timeoutIntervalMS === undefined || this._timeoutIntervalMS === 0) {
return;
}
// set up timer that checks against open, but stale connections that do not return any data
return setInterval(() => {
if (this._ws === undefined) {
return;
}
if (this._receivedMessagesCount === 0) {
this.debug('(connection id: %d) did not received any messages within %d ms timeout, terminating connection...', this._connectionId, this._timeoutIntervalMS);
this._ws.terminate();
}
this._receivedMessagesCount = 0;
}, this._timeoutIntervalMS);
}
_sendPeriodicPing() {
return setInterval(() => {
if (this._ws === undefined || this._ws.readyState !== ws_1.default.OPEN) {
return;
}
this._ws.ping();
}, 5 * handy_1.ONE_SEC_IN_MS);
}
}
exports.RealTimeFeedBase = RealTimeFeedBase;
class MultiConnectionRealTimeFeedBase {
constructor(_exchange, _filters, _timeoutIntervalMS, _onError) {
this._exchange = _exchange;
this._filters = _filters;
this._timeoutIntervalMS = _timeoutIntervalMS;
this._onError = _onError;
}
[Symbol.asyncIterator]() {
return this._stream();
}
async *_stream() {
const combinedStream = new stream_1.PassThrough({
objectMode: true,
highWaterMark: 8096
});
const realTimeFeeds = this._getRealTimeFeeds(this._exchange, this._filters, this._timeoutIntervalMS, this._onError);
for (const realTimeFeed of realTimeFeeds) {
// iterate over separate real-time feeds and write their messages into combined stream
;
(async function writeMessagesToCombinedStream() {
for await (const message of realTimeFeed) {
if (combinedStream.destroyed) {
return;
}
if (!combinedStream.write(message))
// Handle backpressure on write
await events_1.once(combinedStream, 'drain');
}
})();
}
for await (const message of combinedStream) {
yield message;
}
}
}
exports.MultiConnectionRealTimeFeedBase = MultiConnectionRealTimeFeedBase;
class PoolingClientBase {
constructor(exchange, _poolingIntervalSeconds) {
this._poolingIntervalSeconds = _poolingIntervalSeconds;
this._tid = undefined;
this.debug = debug_1.default(`tardis-dev:pooling-client:${exchange}`);
}
[Symbol.asyncIterator]() {
return this._stream();
}
async _startPooling(outputStream) {
const timeoutInterval = this._poolingIntervalSeconds * handy_1.ONE_SEC_IN_MS;
const pool = async () => {
try {
await this.poolDataToStream(outputStream);
}
catch (e) {
this.debug('pooling error %o', e);
}
};
const poolAndSchedule = () => {
pool().then(() => {
if (!outputStream.destroyed) {
this._tid = setTimeout(poolAndSchedule, timeoutInterval);
}
});
};
poolAndSchedule();
}
async *_stream() {
const stream = new stream_1.PassThrough({
objectMode: true,
highWaterMark: 1024
});
this._startPooling(stream);
this.debug('pooling started');
try {
for await (const message of stream) {
yield message;
}
}
finally {
if (this._tid !== undefined) {
clearInterval(this._tid);
}
this.debug('pooling finished');
}
}
}
exports.PoolingClientBase = PoolingClientBase;
//# sourceMappingURL=realtimefeed.js.map