binance
Version:
Professional Node.js & JavaScript SDK for Binance REST APIs & WebSockets, with TypeScript & end-to-end tests.
798 lines • 42.7 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseWebsocketClient = void 0;
/* eslint-disable max-len */
/* eslint-disable @typescript-eslint/no-explicit-any */
const events_1 = __importDefault(require("events"));
const isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
const ws_general_1 = require("../types/websockets/ws-general");
const logger_1 = require("./logger");
//import { checkWebCryptoAPISupported } from './webCryptoAPI';
const websocket_util_1 = require("./websockets/websocket-util");
const WsStore_1 = require("./websockets/WsStore");
const WsStore_types_1 = require("./websockets/WsStore.types");
/**
* Appends wsKey and isWSAPIResponse to all events.
* Some events are arrays, this handles that nested scenario too.
*/
function getFinalEmittable(emittable, wsKey, isWSAPIResponse) {
if (Array.isArray(emittable)) {
return emittable.map((subEvent) => getFinalEmittable(subEvent, wsKey, isWSAPIResponse));
}
if (Array.isArray(emittable.event)) {
// Some topics just emit an array.
// This is consistent with how it was before the WS API upgrade:
return emittable.event.map((subEvent) => getFinalEmittable(subEvent, wsKey, isWSAPIResponse));
// const { event, ...others } = emittable;
// return {
// ...others,
// event: event.map((subEvent) =>
// getFinalEmittable(subEvent, wsKey, isWSAPIResponse),
// ),
// };
}
if (emittable.event) {
return Object.assign(Object.assign({}, emittable.event), { wsKey: wsKey, isWSAPIResponse: !!isWSAPIResponse });
}
return Object.assign(Object.assign({}, emittable), { wsKey: wsKey, isWSAPIResponse: !!isWSAPIResponse });
}
/**
* Base WebSocket abstraction layer. Handles connections, tracking each connection as a unique "WS Key"
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
class BaseWebsocketClient extends events_1.default {
constructor(options, logger) {
var _a, _b;
super();
this.wsApiRequestId = 0;
this.timeOffsetMs = 0;
/**
* { [wsKey]: { [requestId]: request } }
*/
this.midflightRequestCache = {};
this.logger = logger || logger_1.DefaultLogger;
this.wsStore = new WsStore_1.WsStore(this.logger);
this.options = Object.assign(Object.assign({ pongTimeout: 5000, pingInterval: 10000, reconnectTimeout: 500, recvWindow: 5000,
// Automatically send an authentication op/request after a connection opens, for private connections.
authPrivateConnectionsOnConnect: true,
// Individual requests do not require a signature, so this is disabled.
authPrivateRequests: false }, options), { api_key: (_a = options === null || options === void 0 ? void 0 : options.api_key) === null || _a === void 0 ? void 0 : _a.replace(/\\n/g, '\n'), api_secret: (_b = options === null || options === void 0 ? void 0 : options.api_secret) === null || _b === void 0 ? void 0 : _b.replace(/\\n/g, '\n') });
// WebCryptoAPI feature
/* if (this.options.api_key && this.options.api_secret) {
// Provide a user friendly error message if the user is using an outdated Node.js version (where Web Crypto API is not available).
// A few users have been caught out by using the end-of-life Node.js v18 release.
checkWebCryptoAPISupported();
} */
}
isPrivateWsKey(wsKey) {
return this.getPrivateWSKeys().includes(wsKey);
}
/** Returns auto-incrementing request ID, used to track promise references for async requests */
getNewRequestId() {
return ++this.wsApiRequestId;
}
getTimeOffsetMs() {
return this.timeOffsetMs;
}
setTimeOffsetMs(newOffset) {
this.timeOffsetMs = newOffset;
}
/**
* Don't call directly! Use subscribe() instead!
*
* Subscribe to one or more topics on a WS connection (identified by WS Key).
*
* - Topics are automatically cached
* - Connections are automatically opened, if not yet connected
* - Authentication is automatically handled
* - Topics are automatically resubscribed to, if something happens to the connection, unless you call unsubsribeTopicsForWsKey(topics, key).
*
* @param wsRequests array of topics to subscribe to
* @param wsKey ws key referring to the ws connection these topics should be subscribed on
*/
subscribeTopicsForWsKey(wsTopicRequests, wsKey) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const normalisedTopicRequests = (0, websocket_util_1.getNormalisedTopicRequests)(wsTopicRequests);
// Store topics, so future automation (post-auth, post-reconnect) has everything needed to resubscribe automatically
for (const topic of normalisedTopicRequests) {
this.wsStore.addTopic(wsKey, topic);
}
const isConnected = this.wsStore.isConnectionState(wsKey, WsStore_types_1.WsConnectionStateEnum.CONNECTED);
const isConnectionInProgress = this.wsStore.isConnectionAttemptInProgress(wsKey);
// start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect
if (!isConnected && !isConnectionInProgress) {
return this.connect(wsKey);
}
// Subscribe should happen automatically once connected, nothing to do here after topics are added to wsStore.
if (!isConnected) {
/**
* Are we in the process of connection? Nothing to send yet.
*/
this.logger.trace('WS not connected - requests queued for retry once connected.', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey,
wsTopicRequests }));
return isConnectionInProgress;
}
// We're connected. Check if auth is needed and if already authenticated
const isPrivateConnection = this.isPrivateWsKey(wsKey);
const isAuthenticated = (_a = this.wsStore.get(wsKey)) === null || _a === void 0 ? void 0 : _a.isAuthenticated;
if (isPrivateConnection && !isAuthenticated) {
/**
* If not authenticated yet and auth is required, don't request topics yet.
*
* Auth should already automatically be in progress, so no action needed from here. Topics will automatically subscribe post-auth success.
*/
return false;
}
// Finally, request subscription to topics if the connection is healthy and ready
return this.requestSubscribeTopics(wsKey, normalisedTopicRequests);
});
}
unsubscribeTopicsForWsKey(wsTopicRequests, wsKey) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const normalisedTopicRequests = (0, websocket_util_1.getNormalisedTopicRequests)(wsTopicRequests);
// Store topics, so future automation (post-auth, post-reconnect) has everything needed to resubscribe automatically
for (const topic of normalisedTopicRequests) {
this.wsStore.deleteTopic(wsKey, topic);
}
const isConnected = this.wsStore.isConnectionState(wsKey, WsStore_types_1.WsConnectionStateEnum.CONNECTED);
// If not connected, don't need to do anything.
// Removing the topic from the store is enough to stop it from being resubscribed to on reconnect.
if (!isConnected) {
return;
}
// We're connected. Check if auth is needed and if already authenticated
const isPrivateConnection = this.isPrivateWsKey(wsKey);
const isAuthenticated = (_a = this.wsStore.get(wsKey)) === null || _a === void 0 ? void 0 : _a.isAuthenticated;
if (isPrivateConnection && !isAuthenticated) {
/**
* If not authenticated yet and auth is required, don't need to do anything.
* We don't subscribe to topics until auth is complete anyway.
*/
return;
}
// Finally, request subscription to topics if the connection is healthy and ready
return this.requestUnsubscribeTopics(wsKey, normalisedTopicRequests);
});
}
/**
* Splits topic requests into two groups, public & private topic requests
*/
sortTopicRequestsIntoPublicPrivate(wsTopicRequests, wsKey) {
const publicTopicRequests = [];
const privateTopicRequests = [];
for (const topic of wsTopicRequests) {
if (this.isPrivateTopicRequest(topic, wsKey)) {
privateTopicRequests.push(topic);
}
else {
publicTopicRequests.push(topic);
}
}
return {
publicReqs: publicTopicRequests,
privateReqs: privateTopicRequests,
};
}
/** Get the WsStore that tracks websockets & topics */
getWsStore() {
return this.wsStore;
}
close(wsKey, force) {
this.logger.info('Closing connection', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey }));
this.setWsState(wsKey, WsStore_types_1.WsConnectionStateEnum.CLOSING);
this.clearTimers(wsKey);
const ws = this.getWs(wsKey);
ws === null || ws === void 0 ? void 0 : ws.close();
if (force) {
(0, websocket_util_1.safeTerminateWs)(ws);
}
}
closeAll(force) {
const keys = this.wsStore.getKeys();
this.logger.info(`Closing all ws connections: ${keys}`);
keys.forEach((key) => {
this.close(key, force);
});
}
isConnected(wsKey) {
return this.wsStore.isConnectionState(wsKey, WsStore_types_1.WsConnectionStateEnum.CONNECTED);
}
/**
* Request connection to a specific websocket, instead of waiting for automatic connection.
*/
connect(wsKey, customUrl, throwOnError) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
try {
if (this.wsStore.isWsOpen(wsKey)) {
this.logger.error('Refused to connect to ws with existing active connection', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey }));
return { wsKey, ws: this.wsStore.getWs(wsKey) };
}
// Don't check for reconnecting? Conflicts a bit with user data reconnect workflow...
if (this.wsStore.isConnectionState(wsKey, WsStore_types_1.WsConnectionStateEnum.CONNECTING)) {
this.logger.error('Refused to connect to ws, connection attempt already active', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey }));
return (_a = this.wsStore.getConnectionInProgressPromise(wsKey)) === null || _a === void 0 ? void 0 : _a.promise;
}
if (!this.wsStore.getConnectionState(wsKey) ||
this.wsStore.isConnectionState(wsKey, WsStore_types_1.WsConnectionStateEnum.INITIAL)) {
this.setWsState(wsKey, WsStore_types_1.WsConnectionStateEnum.CONNECTING);
}
if (!this.wsStore.getConnectionInProgressPromise(wsKey)) {
this.wsStore.createConnectionInProgressPromise(wsKey, false);
}
const url = customUrl || (yield this.getWsUrl(wsKey));
const ws = this.connectToWsUrl(url, wsKey);
this.wsStore.setWs(wsKey, ws);
}
catch (err) {
this.parseWsError('Connection failed', err, wsKey);
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
if (throwOnError) {
throw err;
}
}
return (_b = this.wsStore.getConnectionInProgressPromise(wsKey)) === null || _b === void 0 ? void 0 : _b.promise;
});
}
connectToWsUrl(url, wsKey) {
this.logger.trace(`Opening WS connection to URL: ${url}`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey }));
const _a = this.options.wsOptions || {}, { protocols = [] } = _a, wsOptions = __rest(_a, ["protocols"]);
const ws = new isomorphic_ws_1.default(url, protocols, wsOptions);
// this.wsUrlKeyMap[url] = wsRefKey;
ws.onopen = (event) => this.onWsOpen(event, wsKey, url, ws);
ws.onmessage = (event) => this.onWsMessage(event, wsKey, ws);
ws.onerror = (event) => this.parseWsError('Websocket onWsError', event, wsKey);
ws.onclose = (event) => this.onWsClose(event, wsKey);
if (typeof ws.on === 'function') {
ws.on('ping', (event) => this.onWsPing(event, wsKey, ws, 'event'));
ws.on('pong', (event) => this.onWsPong(event, wsKey, 'event'));
}
// Not sure these work in the browser, the traditional event listeners are required for ping/pong frames in node
ws.onping = (event) => this.onWsPing(event, wsKey, ws, 'function');
ws.onpong = (event) => this.onWsPong(event, wsKey, 'function');
ws.wsKey = wsKey;
return ws;
}
onWsPing(event, wsKey, ws, source) {
this.logger.trace('Received ping frame, sending pong frame', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey,
source }));
this.sendPongEvent(wsKey, ws);
}
onWsPong(_event, wsKey, source) {
this.logger.trace('Received pong frame, clearing pong timer', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey,
source }));
this.clearPongTimer(wsKey);
}
parseWsError(context, error, wsKey) {
if (!error.message) {
this.logger.error(`${context} due to unexpected error: `, error);
this.emit('response', Object.assign(Object.assign({}, error), { wsKey }));
this.emit('exception', Object.assign(Object.assign({}, error), { wsKey }));
return;
}
switch (error.message) {
case 'Unexpected server response: 401':
this.logger.error(`${context} due to 401 authorization failure.`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey }));
break;
default:
if (this.wsStore.getConnectionState(wsKey) !==
WsStore_types_1.WsConnectionStateEnum.CLOSING) {
this.logger.error(`${context} due to unexpected response error: "${(error === null || error === void 0 ? void 0 : error.msg) || (error === null || error === void 0 ? void 0 : error.message) || error}"`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, error }));
this.executeReconnectableClose(wsKey, 'unhandled onWsError');
}
else {
this.logger.info(`${wsKey} socket forcefully closed. Will not reconnect.`);
}
break;
}
this.emit('response', Object.assign(Object.assign({}, error), { wsKey }));
this.emit('exception', Object.assign(Object.assign({}, error), { wsKey }));
}
/** Get a signature, build the auth request and send it */
sendAuthRequest(wsKey) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
try {
this.logger.trace('Sending auth request...', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey }));
yield this.assertIsConnected(wsKey);
if (!this.wsStore.getAuthenticationInProgressPromise(wsKey)) {
this.wsStore.createAuthenticationInProgressPromise(wsKey, false);
}
const request = yield this.getWsAuthRequestEvent(wsKey);
// console.log('ws auth req', request);
this.tryWsSend(wsKey, JSON.stringify(request));
return (_a = this.wsStore.getAuthenticationInProgressPromise(wsKey)) === null || _a === void 0 ? void 0 : _a.promise;
}
catch (e) {
this.logger.trace(e, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey }));
}
});
}
reconnectWithDelay(wsKey, connectionDelayMs) {
var _a;
this.clearTimers(wsKey);
if (!this.wsStore.isConnectionAttemptInProgress(wsKey)) {
this.setWsState(wsKey, WsStore_types_1.WsConnectionStateEnum.RECONNECTING);
}
this.logger.info('Reconnecting to websocket with delay...', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey,
connectionDelayMs }));
if ((_a = this.wsStore.get(wsKey)) === null || _a === void 0 ? void 0 : _a.activeReconnectTimer) {
this.clearReconnectTimer(wsKey);
}
this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => {
this.clearReconnectTimer(wsKey);
// Some streams need a specialist reconnection workflow.
// E.g. the user data stream can't just be reconnected as is.
if (this.isCustomReconnectionNeeded(wsKey)) {
this.wsStore.delete(wsKey);
return this.triggerCustomReconnectionWorkflow(wsKey);
}
this.logger.info('Reconnecting to websocket now', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey }));
this.connect(wsKey);
}, connectionDelayMs);
}
ping(wsKey) {
if (this.wsStore.get(wsKey, true).activePongTimer) {
return;
}
this.clearPongTimer(wsKey);
this.logger.trace('Sending ping', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey }));
const ws = this.wsStore.get(wsKey, true).ws;
if (!ws) {
this.logger.error(`Unable to send ping for wsKey "${wsKey}" - no connection found`);
return;
}
this.sendPingEvent(wsKey, ws);
this.wsStore.get(wsKey, true).activePongTimer = setTimeout(() => this.executeReconnectableClose(wsKey, 'Pong timeout'), this.options.pongTimeout);
}
/**
* Closes a connection, if it's even open. If open, this will trigger a reconnect asynchronously.
* If closed, trigger a reconnect immediately
*/
executeReconnectableClose(wsKey, reason) {
this.logger.info(`${reason} - closing socket to reconnect`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey,
reason }));
const wasOpen = this.wsStore.isWsOpen(wsKey);
this.clearPingTimer(wsKey);
this.clearPongTimer(wsKey);
const ws = this.getWs(wsKey);
if (ws) {
ws.close();
(0, websocket_util_1.safeTerminateWs)(ws);
}
if (!wasOpen) {
this.logger.info(`${reason} - socket already closed - trigger immediate reconnect`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey,
reason }));
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
}
}
clearTimers(wsKey) {
this.clearPingTimer(wsKey);
this.clearPongTimer(wsKey);
this.clearReconnectTimer(wsKey);
}
// Send a ping at intervals
clearPingTimer(wsKey) {
const wsState = this.wsStore.get(wsKey);
if (wsState === null || wsState === void 0 ? void 0 : wsState.activePingTimer) {
clearInterval(wsState.activePingTimer);
wsState.activePingTimer = undefined;
}
}
// Expect a pong within a time limit
clearPongTimer(wsKey) {
const wsState = this.wsStore.get(wsKey);
if (wsState === null || wsState === void 0 ? void 0 : wsState.activePongTimer) {
clearTimeout(wsState.activePongTimer);
wsState.activePongTimer = undefined;
// this.logger.trace(`Cleared pong timeout for "${wsKey}"`);
}
else {
// this.logger.trace(`No active pong timer for "${wsKey}"`);
}
}
clearReconnectTimer(wsKey) {
const wsState = this.wsStore.get(wsKey);
if (wsState === null || wsState === void 0 ? void 0 : wsState.activeReconnectTimer) {
clearTimeout(wsState.activeReconnectTimer);
wsState.activeReconnectTimer = undefined;
}
}
/**
* Returns a list of string events that can be individually sent upstream to complete subscribing/unsubscribing/etc to these topics
*
* If events are an object, these should be stringified (`return JSON.stringify(event);`)
* Each event returned by this will be sent one at a time
*
* Events are automatically split into smaller batches, by this method, if needed.
*/
getWsOperationEventsForTopics(topics, wsKey, operation) {
return __awaiter(this, void 0, void 0, function* () {
if (!topics.length) {
return [];
}
// Events that are ready to send (usually stringified JSON)
const requestEvents = [];
const maxTopicsPerEvent = this.getMaxTopicsPerSubscribeEvent(wsKey);
if (maxTopicsPerEvent &&
maxTopicsPerEvent !== null &&
topics.length > maxTopicsPerEvent) {
for (let i = 0; i < topics.length; i += maxTopicsPerEvent) {
const batch = topics.slice(i, i + maxTopicsPerEvent);
const subscribeRequestEvents = yield this.getWsRequestEvents(wsKey, operation, batch);
requestEvents.push(...subscribeRequestEvents);
}
return requestEvents;
}
const subscribeRequestEvents = yield this.getWsRequestEvents(wsKey, operation, topics);
return subscribeRequestEvents;
});
}
/**
* Simply builds and sends subscribe events for a list of topics for a ws key
*
* @private Use the `subscribe(topics)` or `subscribeTopicsForWsKey(topics, wsKey)` method to subscribe to topics.
*/
requestSubscribeTopics(wsKey, wsTopicRequests) {
return __awaiter(this, void 0, void 0, function* () {
if (!wsTopicRequests.length) {
return;
}
// Warn if trying to subscribe to a large number of topics
if (wsTopicRequests.length > 200) {
this.logger.info(`High topic count warning: subscribing to ${wsTopicRequests.length} topics for wsKey "${wsKey}". You may encounter exchange rate limits and performance degradation. Consider reducing the number of subscriptions as Binance has "max topics per connection" limits. If you're repeatedly disconnected by Binance, split your topics per connection by making multiple WebsocketClient instances.`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, topicCount: wsTopicRequests.length }));
}
// Automatically splits requests into smaller batches, if needed
const subscribeWsMessages = yield this.getWsOperationEventsForTopics(wsTopicRequests, wsKey, 'SUBSCRIBE');
this.logger.trace(`Subscribing to ${wsTopicRequests.length} "${wsKey}" topics in ${subscribeWsMessages.length} batches.`);
// console.log(`batches: `, JSON.stringify(subscribeWsMessages, null, 2));
for (const midflightRequest of subscribeWsMessages) {
const wsMessage = midflightRequest.requestEvent;
if (!this.midflightRequestCache[wsKey]) {
this.midflightRequestCache[wsKey] = {};
}
// Cache the request for this call, so we can enrich the response with request info
this.midflightRequestCache[wsKey][midflightRequest.requestKey] =
midflightRequest.requestEvent;
this.logger.trace(`Sending batch via message: "${JSON.stringify(wsMessage)}", cached with key "${midflightRequest.requestKey}"`);
try {
this.tryWsSend(wsKey, JSON.stringify(wsMessage), true);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (e) {
delete this.midflightRequestCache[wsKey][midflightRequest.requestKey];
}
}
this.logger.trace(`Finished subscribing to ${wsTopicRequests.length} "${wsKey}" topics in ${subscribeWsMessages.length} batches.`);
});
}
/**
* Simply builds and sends unsubscribe events for a list of topics for a ws key
*
* @private Use the `unsubscribe(topics)` method to unsubscribe from topics. Send WS message to unsubscribe from topics.
*/
requestUnsubscribeTopics(wsKey, wsTopicRequests) {
return __awaiter(this, void 0, void 0, function* () {
if (!wsTopicRequests.length) {
return;
}
const subscribeWsMessages = yield this.getWsOperationEventsForTopics(wsTopicRequests, wsKey, 'UNSUBSCRIBE');
this.logger.trace(`Unsubscribing to ${wsTopicRequests.length} "${wsKey}" topics in ${subscribeWsMessages.length} batches. Events: "${JSON.stringify(wsTopicRequests)}"`);
for (const midflightRequest of subscribeWsMessages) {
const wsMessage = midflightRequest.requestEvent;
if (!this.midflightRequestCache[wsKey]) {
this.midflightRequestCache[wsKey] = {};
}
// Cache the request for this call, so we can enrich the response with request info
this.midflightRequestCache[wsKey][midflightRequest.requestKey] =
midflightRequest.requestEvent;
this.logger.trace(`Sending batch via message: "${wsMessage}"`);
try {
this.tryWsSend(wsKey, JSON.stringify(wsMessage));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (e) {
delete this.midflightRequestCache[wsKey][midflightRequest.requestKey];
}
}
this.logger.trace(`Finished unsubscribing to ${wsTopicRequests.length} "${wsKey}" topics in ${subscribeWsMessages.length} batches.`);
});
}
getCachedMidFlightRequest(wsKey, requestKey) {
if (!this.midflightRequestCache[wsKey]) {
this.midflightRequestCache[wsKey] = {};
}
return this.midflightRequestCache[wsKey][requestKey];
}
removeCachedMidFlightRequest(wsKey, requestKey) {
if (this.getCachedMidFlightRequest(wsKey, requestKey)) {
delete this.midflightRequestCache[wsKey][requestKey];
}
}
/**
* Try sending a string event on a WS connection (identified by the WS Key)
*/
tryWsSend(wsKey, wsMessage, throwExceptions) {
try {
this.logger.trace('Sending upstream ws message: ', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsMessage,
wsKey }));
if (!wsKey) {
throw new Error('Cannot send message due to no known websocket for this wsKey');
}
const ws = this.getWs(wsKey);
if (!ws) {
throw new Error(`${wsKey} socket not connected yet, call "connectAll()" first then try again when the "open" event arrives`);
}
ws.send(wsMessage);
}
catch (e) {
this.logger.error('Failed to send WS message', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsMessage,
wsKey, exception: e }));
if (throwExceptions) {
throw e;
}
}
}
onWsOpen(event, wsKey, url, ws) {
return __awaiter(this, void 0, void 0, function* () {
const isFreshConnectionAttempt = this.wsStore.isConnectionState(wsKey, WsStore_types_1.WsConnectionStateEnum.CONNECTING);
const isReconnectionAttempt = this.wsStore.isConnectionState(wsKey, WsStore_types_1.WsConnectionStateEnum.RECONNECTING);
if (isFreshConnectionAttempt) {
this.logger.info('Websocket connected', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey }));
this.emit('open', { wsKey, event, wsUrl: url, ws });
}
else if (isReconnectionAttempt) {
this.logger.info('Websocket reconnected', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey }));
this.emit('reconnected', { wsKey, event, wsUrl: url, ws });
}
this.setWsState(wsKey, WsStore_types_1.WsConnectionStateEnum.CONNECTED);
this.logger.trace('Enabled ping timer', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey }));
this.wsStore.get(wsKey, true).activePingTimer = setInterval(() => this.ping(wsKey), this.options.pingInterval);
// Resolve & cleanup deferred "connection attempt in progress" promise
try {
const connectionInProgressPromise = this.wsStore.getConnectionInProgressPromise(wsKey);
if (connectionInProgressPromise === null || connectionInProgressPromise === void 0 ? void 0 : connectionInProgressPromise.resolve) {
connectionInProgressPromise.resolve({
wsKey,
ws,
});
}
}
catch (e) {
this.logger.error('Exception trying to resolve "connectionInProgress" promise', e);
}
// Remove before continuing, in case there's more requests queued
this.wsStore.removeConnectingInProgressPromise(wsKey);
// Some websockets require an auth packet to be sent after opening the connection
if (this.isAuthOnConnectWsKey(wsKey) &&
this.options.authPrivateConnectionsOnConnect) {
yield this.assertIsAuthenticated(wsKey);
}
// Reconnect to topics known before it connected
const { privateReqs, publicReqs } = this.sortTopicRequestsIntoPublicPrivate([...this.wsStore.getTopics(wsKey)], wsKey);
// Request sub to public topics, if any
this.requestSubscribeTopics(wsKey, publicReqs);
// Request sub to private topics, if auth on connect isn't needed
// Else, this is automatic after authentication is successfully confirmed
if (!this.options.authPrivateConnectionsOnConnect) {
this.requestSubscribeTopics(wsKey, privateReqs);
}
});
}
/**
* Handle subscription to private topics _after_ authentication successfully completes asynchronously.
*
* Only used for exchanges that require auth before sending private topic subscription requests
*/
onWsAuthenticated(wsKey, event) {
const wsState = this.wsStore.get(wsKey, true);
wsState.isAuthenticated = true;
// Resolve & cleanup deferred "connection attempt in progress" promise
try {
const inProgressPromise = this.wsStore.getAuthenticationInProgressPromise(wsKey);
if (inProgressPromise === null || inProgressPromise === void 0 ? void 0 : inProgressPromise.resolve) {
inProgressPromise.resolve({
wsKey,
event,
ws: wsState.ws,
});
}
}
catch (e) {
this.logger.error('Exception trying to resolve "connectionInProgress" promise', e);
}
// Remove before continuing, in case there's more requests queued
this.wsStore.removeAuthenticationInProgressPromise(wsKey);
if (this.options.authPrivateConnectionsOnConnect) {
const topics = [...this.wsStore.getTopics(wsKey)];
const privateTopics = topics.filter((topic) => this.isPrivateTopicRequest(topic, wsKey));
if (privateTopics.length) {
this.subscribeTopicsForWsKey(privateTopics, wsKey);
}
}
}
onWsMessage(event, wsKey, ws) {
try {
// console.log('onMessageRaw: ', (event as any).data);
// any message can clear the pong timer - wouldn't get a message if the ws wasn't working
this.clearPongTimer(wsKey);
if (this.isWsPong(event)) {
this.logger.trace('Received pong', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, event: event === null || event === void 0 ? void 0 : event.data }));
return;
}
if (this.isWsPing(event)) {
this.logger.trace('Received ping', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey,
event }));
this.sendPongEvent(wsKey, ws);
return;
}
if ((0, ws_general_1.isMessageEvent)(event)) {
const data = event.data;
const dataType = event.type;
const emittableEvents = this.resolveEmittableEvents(wsKey, event);
if (!emittableEvents.length) {
// console.log(`raw event: `, { data, dataType, emittableEvents });
this.logger.error('Unhandled/unrecognised ws event message - returned no emittable data', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { message: data || 'no message', dataType,
event,
wsKey }));
return this.emit('message', Object.assign(Object.assign({}, event), { wsKey }));
}
for (const emittable of emittableEvents) {
if (this.isWsPong(emittable)) {
this.logger.trace('Received pong2', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey,
data }));
continue;
}
// this.logger.trace(
// 'getFinalEmittable()->pre(): ',
// JSON.stringify(emittable),
// );
const emittableFinalEvent = getFinalEmittable(emittable, wsKey, emittable.isWSAPIResponse);
// this.logger.trace(
// 'getFinalEmittable()->post(): ',
// JSON.stringify(emittable),
// );
if (emittable.eventType === 'authenticated') {
this.logger.trace('Successfully authenticated', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey,
emittable }));
this.emit(emittable.eventType, emittableFinalEvent);
this.onWsAuthenticated(wsKey, emittable.event);
continue;
}
// Other event types are automatically emitted here
// this.logger.trace(
// `onWsMessage().emit(${emittable.eventType})`,
// emittableFinalEvent,
// );
try {
this.emit(emittable.eventType, emittableFinalEvent);
}
catch (e) {
this.logger.error(`Exception in onWsMessage().emit(${emittable.eventType}) handler:`, e);
}
// this.logger.trace(
// `onWsMessage().emit(${emittable.eventType}).done()`,
// emittableFinalEvent,
// );
}
return;
}
this.logger.error('Unhandled/unrecognised ws event message - unexpected message format', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { message: event || 'no message', event,
wsKey }));
}
catch (e) {
this.logger.error('Failed to parse ws event message', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { error: e, event,
wsKey }));
}
}
onWsClose(event, wsKey) {
this.logger.info('Websocket connection closed', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey }));
const wsState = this.wsStore.get(wsKey, true);
wsState.isAuthenticated = false;
if (this.wsStore.getConnectionState(wsKey) !== WsStore_types_1.WsConnectionStateEnum.CLOSING) {
// unintentional close, attempt recovery
this.logger.trace(`onWsClose(${wsKey}): rejecting all deferred promises...`);
// clean up any pending promises for this connection
this.getWsStore().rejectAllDeferredPromises(wsKey, 'connection lost, reconnecting');
this.setWsState(wsKey, WsStore_types_1.WsConnectionStateEnum.INITIAL);
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
this.emit('reconnecting', { wsKey, event });
}
else {
// intentional close - clean up
// clean up any pending promises for this connection
this.logger.trace(`onWsClose(${wsKey}): rejecting all deferred promises...`);
this.getWsStore().rejectAllDeferredPromises(wsKey, 'disconnected');
this.setWsState(wsKey, WsStore_types_1.WsConnectionStateEnum.INITIAL);
// This was an intentional close, delete all state for this connection, as if it never existed:
this.wsStore.delete(wsKey);
this.emit('close', { wsKey, event });
}
}
getWs(wsKey) {
return this.wsStore.getWs(wsKey);
}
setWsState(wsKey, state) {
this.wsStore.setConnectionState(wsKey, state);
}
/**
* Promise-driven method to assert that a ws has successfully connected (will await until connection is open)
*/
assertIsConnected(wsKey) {
return __awaiter(this, void 0, void 0, function* () {
const isConnected = this.getWsStore().isConnectionState(wsKey, WsStore_types_1.WsConnectionStateEnum.CONNECTED);
if (isConnected) {
return true;
}
const inProgressPromise = this.getWsStore().getConnectionInProgressPromise(wsKey);
// Already in progress? Await shared promise and retry
if (inProgressPromise) {
this.logger.trace('assertIsConnected(): awaiting...');
yield inProgressPromise.promise;
this.logger.trace('assertIsConnected(): awaiting...connected!');
return inProgressPromise.promise;
}
// Start connection, it should automatically store/return a promise.
this.logger.trace('assertIsConnected(): connecting...');
yield this.connect(wsKey);
this.logger.trace('assertIsConnected(): connecting...newly connected!');
});
}
/**
* Promise-driven method to assert that a ws has been successfully authenticated (will await until auth is confirmed)
*/
assertIsAuthenticated(wsKey) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const isConnected = this.getWsStore().isConnectionState(wsKey, WsStore_types_1.WsConnectionStateEnum.CONNECTED);
if (!isConnected) {
this.logger.trace('assertIsAuthenticated(): connecting...');
yield this.assertIsConnected(wsKey);
}
const inProgressPromise = this.getWsStore().getAuthenticationInProgressPromise(wsKey);
// Already in progress? Await shared promise and retry
if (inProgressPromise) {
this.logger.trace('assertIsAuthenticated(): awaiting...');
yield inProgressPromise.promise;
this.logger.trace('assertIsAuthenticated(): authenticated!');
return;
}
const isAuthenticated = (_a = this.wsStore.get(wsKey)) === null || _a === void 0 ? void 0 : _a.isAuthenticated;
if (isAuthenticated) {
// this.logger.trace('assertIsAuthenticated(): ok');
return;
}
// Start authentication, it should automatically store/return a promise.
this.logger.trace('assertIsAuthenticated(): authenticating...');
yield this.sendAuthRequest(wsKey);
this.logger.trace('assertIsAuthenticated(): newly authenticated!');
});
}
}
exports.BaseWebsocketClient = BaseWebsocketClient;
//# sourceMappingURL=BaseWSClient.js.map