UNPKG

binance

Version:

Professional Node.js & JavaScript SDK for Binance REST APIs & WebSockets, with TypeScript & end-to-end tests.

255 lines 15.4 kB
"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