binance
Version:
Professional Node.js & JavaScript SDK for Binance REST APIs & WebSockets, with TypeScript & end-to-end tests.
255 lines • 15.4 kB
JavaScript
"use strict";
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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserDataStreamManager = void 0;
const typeGuards_1 = require("../typeGuards");
const enum_1 = require("./enum");
const listen_key_state_cache_1 = require("./listen-key-state-cache");
const websocket_util_1 = require("./websocket-util");
const WsStore_types_1 = require("./WsStore.types");
/**
* A minimal attempt at separating all the user data stream handling workflows from
* the rest of the WS Client logic.
*
* This abstraction handles:
*
* - spawning the ws connection with the listen key
* - tracking metadata for a connection & the embedded listen key
* - handling listen key keep-alive triggers
* - handling ws respawn with listen key checks
*/
class UserDataStreamManager {
constructor(config) {
this.logger = config.logger;
this.wsStore = config.wsStore;
this.restClientCache = config.restClientCache;
this.respawnUserDataStream = config.respawnUserDataFn;
this.closeWsFn = config.closeWsFn;
this.connectFn = config.connectFn;
this.getWsUrlFn = config.getWsUrlFn;
this.getRestClientOptionsFn = config.getRestClientOptionsFn;
this.getWsClientOptionsfn = config.getWsClientOptionsfn;
this.listenKeyStateCache = new listen_key_state_cache_1.ListenKeyStateCache(this.logger);
// this.wsClient = config.wsClient;
}
// getWSClient(): WebsocketClient {
// return this.wsClient;
// }
getWsStore() {
return this.wsStore;
}
subscribeGeneralUserDataStreamWithListenKey(wsKey, market, listenKey, forceNewConnection, miscState) {
return __awaiter(this, void 0, void 0, function* () {
const streamName = 'userData';
const symbol = miscState === null || miscState === void 0 ? void 0 : miscState.symbol;
this.logger.trace('subscribeGeneralUserDataStreamWithListenKey(): ', {
wsKey,
market,
listenKey,
forceNewConnection,
miscState,
});
const derivedWsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, symbol, listenKey, wsKey);
const wsState = this.getWsStore().get(derivedWsKey, true);
if (!forceNewConnection &&
this.getWsStore().isConnectionAttemptInProgress(derivedWsKey)) {
const stateLastChangedAt = wsState === null || wsState === void 0 ? void 0 : wsState.connectionStateChangedAt;
const timestamp = stateLastChangedAt === null || stateLastChangedAt === void 0 ? void 0 : stateLastChangedAt.getTime();
const timestampNow = new Date().getTime();
const stateChangedTimeAgo = timestampNow - (timestamp || NaN);
this.logger.trace(`Existing ${wsKey} user data connection in progress for listen key. Avoiding duplicate`, { stateLastChangedAt, stateChangedTimeAgo, wsKey, derivedWsKey });
return this.getWsStore().getWs(derivedWsKey);
}
// Prepare the WS state for awareness whether this is a reconnect or fresh connect
if (miscState === null || miscState === void 0 ? void 0 : miscState.isReconnecting) {
this.getWsStore().setConnectionState(derivedWsKey, WsStore_types_1.WsConnectionStateEnum.RECONNECTING);
}
// Begin the connection process with the active listen key
try {
const wsBaseUrl = yield this.getWsUrlFn(wsKey, 'userData');
const wsURL = wsBaseUrl + `/${listenKey}`;
const throwOnConnectionError = true;
const connectResult = yield this.connectFn(derivedWsKey, wsURL, throwOnConnectionError);
if (!connectResult) {
this.logger.error('Exception in user data manager, connection error? ', { wsBaseUrl, wsURL, connectResult });
throw new Error('userDataManager->subscribeGeneral()-> connection error?');
}
// Start & store timer to keep alive listen key (and handle expiration)
this.setKeepAliveListenKeyTimer(listenKey, market, connectResult.ws, derivedWsKey);
return connectResult.ws;
}
catch (e) {
this.logger.error('Exception in subscribeGeneralUserDataStreamWithListenKey()', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, derivedWsKey, e: (e === null || e === void 0 ? void 0 : e.stack) || e }));
// In case any timers already exist, pre-wipe
this.listenKeyStateCache.clearAllListenKeyState(listenKey);
// So the next attempt doesn't think an attempt is already in progress
this.getWsStore().setConnectionState(derivedWsKey, WsStore_types_1.WsConnectionStateEnum.ERROR);
throw e;
}
});
}
setKeepAliveListenKeyTimer(listenKey, market, ws, wsKey, symbol, isTestnet) {
// This MUST happen before fetching listenKey state,
// since the getListenKeyState() creates new state,
// while clearAllListenKeyState DELETES it
this.listenKeyStateCache.clearAllListenKeyState(listenKey);
const listenKeyState = this.listenKeyStateCache.getListenKeyState(listenKey, market);
// this.logger.trace(
// `----> setKeepAliveListenKeyTimer() -> CREATED NEW keepAliveListenKey INTERVAL timer for ${listenKey}`,
// );
// Set timer to keep WS alive every 50 minutes
const minutes50 = 1000 * 60 * 50;
listenKeyState.keepAliveTimer = setInterval(() => this.checkKeepAliveListenKey(listenKey, market, ws, wsKey, symbol, isTestnet), minutes50);
}
checkKeepAliveListenKey(listenKey, market, ws, derivedWsKey, symbol, isTestnet) {
return __awaiter(this, void 0, void 0, function* () {
const listenKeyState = this.listenKeyStateCache.getListenKeyState(listenKey, market);
const wsKey = (0, websocket_util_1.getRealWsKeyFromDerivedWsKey)(derivedWsKey);
try {
if (listenKeyState.keepAliveRetryTimer) {
clearTimeout(listenKeyState.keepAliveRetryTimer);
listenKeyState.keepAliveRetryTimer = undefined;
this.logger.trace(`checkKeepAliveListenKey() -> CLEARED old one-off keepAliveRetryTimer timer for ${listenKey}`);
}
yield this.sendKeepAliveForMarket(listenKey, market, ws, wsKey, derivedWsKey, symbol);
listenKeyState.lastKeepAlive = Date.now();
listenKeyState.keepAliveFailures = 0;
this.logger.info(`Completed keep alive cycle for listenKey(${listenKey}) in market(${market})`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { listenKey }));
}
catch (e) {
listenKeyState.keepAliveFailures++;
// Reconnect follows a less automatic workflow since this is tied to a listen key (which may need a new one).
// - Kill connection first, with instruction NOT to reconnect automatically
// - Then respawn a connection with a potentially new listen key (since the old one may be invalid now)
const shouldReconnectAfterClose = false;
const errorCode = e === null || e === void 0 ? void 0 : e.code;
if (errorCode === enum_1.WS_ERROR_CODE.LISTEN_KEY_NOT_FOUND) {
this.logger.error('FATAL: Failed to keep WS alive for listen key - listen key expired/invalid. Respawning with fresh listen key...', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { listenKey, error: e, errorCode, errorMsg: e === null || e === void 0 ? void 0 : e.message }));
this.closeWsFn(derivedWsKey, shouldReconnectAfterClose);
this.respawnUserDataStream(wsKey, market, { symbol, isTestnet });
return;
}
// If max failurees reached, tear down and respawn if allowed
if (listenKeyState.keepAliveFailures >= 3) {
this.logger.error('FATAL: Failed to keep WS alive for listen key after 3 attempts', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { listenKey, error: e }));
this.closeWsFn(derivedWsKey, shouldReconnectAfterClose);
this.respawnUserDataStream(wsKey, market, { symbol, isTestnet });
return;
}
const reconnectDelaySeconds = 1000 * 15;
this.logger.info(`Userdata keep alive request failed due to error, trying again with short delay (${reconnectDelaySeconds} seconds)`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { listenKey, error: e, keepAliveAttempts: listenKeyState.keepAliveFailures }));
this.logger.trace(`checkKeepAliveListenKey() -> CREATED NEW one-off keepAliveRetryTimer timer for ${listenKey}`);
listenKeyState.keepAliveRetryTimer = setTimeout(() => this.checkKeepAliveListenKey(listenKey, market, ws, derivedWsKey, symbol), reconnectDelaySeconds);
}
});
}
teardownUserDataListenKey(listenKey, ws) {
if (listenKey) {
this.listenKeyStateCache.clearAllListenKeyState(listenKey);
(0, websocket_util_1.safeTerminateWs)(ws, true);
}
}
triggerUserDataReconnectionWorkflow(legacyWsKey) {
return __awaiter(this, void 0, void 0, function* () {
this.logger.trace(`triggerCustomReconnectionWorkflow(${legacyWsKey})`);
if (legacyWsKey.includes('userData')) {
const legacyWsKeyContext = (0, websocket_util_1.getLegacyWsKeyContext)(legacyWsKey);
if (!legacyWsKeyContext) {
throw new Error(`triggerCustomReconnectionWorkflow(): no context found in supplied wsKey: "${legacyWsKey}"`);
}
const { market, symbol, isTestnet, listenKey, wsKey } = legacyWsKeyContext;
this.logger.info('Preparing to reconnect userData stream...', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { legacyWsKey,
market,
symbol,
isTestnet,
listenKey,
wsKey }));
if (listenKey) {
this.logger.trace('Checking if old listenKey has active timers...', {
listenKey,
});
this.listenKeyStateCache.clearAllListenKeyState(listenKey);
}
else {
throw new Error('No listenKey stashed in legacyWsKey context???');
}
this.logger.info('Reconnecting to user data stream...', Object.assign(Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), legacyWsKeyContext), { wsKey,
market,
symbol }));
// We'll set a new one once the new stream respawns, it might have a diff listenKey in the wsKey
this.getWsStore().delete(legacyWsKey);
if (!wsKey) {
const errorMessage = 'triggerCustomReconnectionWorkflow(): missing real "wsKey" from legacy context';
this.logger.error(errorMessage, {
legacyWsKeyContext,
legacyWsKey,
});
throw new Error(errorMessage);
}
this.respawnUserDataStream(wsKey, market, {
symbol,
isTestnet,
});
return;
}
});
}
sendKeepAliveForMarket(listenKey, market, ws, wsKey, deriedWsKey, symbol) {
switch (market) {
case 'spot':
return this.restClientCache
.getSpotRestClient(this.getRestClientOptionsFn(), this.getWsClientOptionsfn().requestOptions)
.keepAliveSpotUserDataListenKey(listenKey);
case 'spotTestnet':
return this.restClientCache
.getSpotRestClient(this.getRestClientOptionsFn(), this.getWsClientOptionsfn().requestOptions)
.keepAliveSpotUserDataListenKey(listenKey);
case 'crossMargin':
return this.restClientCache
.getSpotRestClient(this.getRestClientOptionsFn(), this.getWsClientOptionsfn().requestOptions)
.keepAliveMarginUserDataListenKey(listenKey);
case 'riskDataMargin':
return this.restClientCache
.getSpotRestClient(this.getRestClientOptionsFn(), this.getWsClientOptionsfn().requestOptions)
.keepAliveMarginRiskUserDataListenKey(listenKey);
case 'isolatedMargin':
return this.restClientCache
.getSpotRestClient(this.getRestClientOptionsFn(), this.getWsClientOptionsfn().requestOptions)
.keepAliveIsolatedMarginUserDataListenKey({
listenKey,
symbol: symbol,
});
case 'coinm':
case 'options':
case 'optionsTestnet':
case 'usdm':
return this.restClientCache
.getUSDMRestClient(this.getRestClientOptionsFn(), this.getWsClientOptionsfn().requestOptions)
.keepAliveFuturesUserDataListenKey();
case 'usdmTestnet':
return this.restClientCache
.getUSDMRestClient(this.getRestClientOptionsFn(), this.getWsClientOptionsfn().requestOptions)
.keepAliveFuturesUserDataListenKey();
case 'coinmTestnet':
return this.restClientCache
.getCOINMRestClient(this.getRestClientOptionsFn(), this.getWsClientOptionsfn().requestOptions)
.keepAliveFuturesUserDataListenKey();
case 'portfoliom':
return this.restClientCache
.getPortfolioClient(this.getRestClientOptionsFn(), this.getWsClientOptionsfn().requestOptions)
.keepAlivePMUserDataListenKey();
default:
throw (0, typeGuards_1.neverGuard)(market, `Failed to send keep alive for user data stream in unhandled market ${market}`);
}
}
}
exports.UserDataStreamManager = UserDataStreamManager;
//# sourceMappingURL=user-data-stream-manager.js.map