UNPKG

bybit-api

Version:

Complete & robust Node.js SDK for Bybit's REST APIs and WebSockets, with TypeScript & strong end to end tests.

765 lines 40.1 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()); }); }; 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 logger_1 = require("./logger"); const types_1 = require("../types"); const WsStore_1 = require("./websockets/WsStore"); const websockets_1 = require("./websockets"); /** * 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) { super(); this.wsApiRequestId = 0; this.timeOffsetMs = 0; /** * A nested wsKey->request key store. * pendingTopicSubscriptionRequests[wsKey][requestKey] = WsKeyPendingTopicSubscriptions<TWSRequestEvent> */ this.pendingTopicSubscriptionRequests = {}; this.logger = logger || logger_1.DefaultLogger; this.wsStore = new WsStore_1.WsStore(this.logger); this.options = Object.assign({ // Some defaults: testnet: false, demoTrading: false, // Connect to V5 by default, if not defined by the user market: 'v5', pongTimeout: 1000, pingInterval: 10000, reconnectTimeout: 500, recvWindow: 5000, // Calls to subscribeV5() are wrapped in a promise, allowing you to await a subscription request. // Note: due to internal complexity, it's only recommended if you connect before subscribing. promiseSubscribeRequests: false, // 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); } 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; } getWsKeyPendingSubscriptionStore(wsKey) { if (!this.pendingTopicSubscriptionRequests[wsKey]) { this.pendingTopicSubscriptionRequests[wsKey] = {}; } return this.pendingTopicSubscriptionRequests[wsKey]; } upsertPendingTopicSubscribeRequests(wsKey, requestData) { // a unique identifier for this subscription request (e.g. csv of topics, or request id, etc) const requestKey = requestData.requestKey; // Should not be possible to see a requestKey collision in the current design, since the req ID increments automatically with every request, so this should never be true, but just in case a future mistake happens... const pendingSubReqs = this.getWsKeyPendingSubscriptionStore(wsKey); if (pendingSubReqs[requestKey]) { throw new Error('Implementation error: attempted to upsert pending topics with duplicate request ID!'); } return new Promise((resolver, rejector) => { const pendingSubReqs = this.getWsKeyPendingSubscriptionStore(wsKey); pendingSubReqs[requestKey] = { requestData: requestData.requestEvent, resolver, rejector, }; }); } removeTopicPendingSubscription(wsKey, requestKey) { const pendingSubReqs = this.getWsKeyPendingSubscriptionStore(wsKey); delete pendingSubReqs[requestKey]; } clearTopicsPendingSubscriptions(wsKey, rejectAll, rejectReason) { if (rejectAll) { const pendingSubReqs = this.getWsKeyPendingSubscriptionStore(wsKey); for (const requestKey in pendingSubReqs) { const request = pendingSubReqs[requestKey]; this.logger.trace(`clearTopicsPendingSubscriptions(${wsKey}, ${rejectAll}, ${rejectReason}, ${requestKey}): rejecting promise for: ${JSON.stringify((request === null || request === void 0 ? void 0 : request.requestData) || {})}`); request === null || request === void 0 ? void 0 : request.rejector(request.requestData, rejectReason); } } this.pendingTopicSubscriptionRequests[wsKey] = {}; } /** * Resolve/reject the promise for a midflight request. * * This will typically execute before the event is emitted. */ updatePendingTopicSubscriptionStatus(wsKey, requestKey, msg, isTopicSubscriptionSuccessEvent) { const wsKeyPendingRequests = this.getWsKeyPendingSubscriptionStore(wsKey); if (!wsKeyPendingRequests) { return; } const pendingSubscriptionRequest = wsKeyPendingRequests[requestKey]; if (!pendingSubscriptionRequest) { return; } if (isTopicSubscriptionSuccessEvent) { pendingSubscriptionRequest.resolver(pendingSubscriptionRequest.requestData); } else { this.logger.trace(`updatePendingTopicSubscriptionStatus.reject(${wsKey}, ${requestKey}, ${msg}, ${isTopicSubscriptionSuccessEvent}): `, msg); try { pendingSubscriptionRequest.rejector(pendingSubscriptionRequest.requestData, msg); } catch (e) { console.error('Exception rejecting promise: ', e); } } this.removeTopicPendingSubscription(wsKey, requestKey); } /** * 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, websockets_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, websockets_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({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey, wsTopicRequests })); 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 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, websockets_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, websockets_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({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey })); this.setWsState(wsKey, websockets_1.WsConnectionStateEnum.CLOSING); this.clearTimers(wsKey); const ws = this.getWs(wsKey); ws === null || ws === void 0 ? void 0 : ws.close(); if (force) { (0, websockets_1.safeTerminateWs)(ws, false); } } 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, websockets_1.WsConnectionStateEnum.CONNECTED); } /** * Request connection to a specific websocket, instead of waiting for automatic connection. */ connect(wsKey) { return __awaiter(this, void 0, void 0, function* () { var _a; try { if (this.wsStore.isWsOpen(wsKey)) { this.logger.error('Refused to connect to ws with existing active connection', Object.assign(Object.assign({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey })); return { wsKey }; } if (this.wsStore.isConnectionState(wsKey, websockets_1.WsConnectionStateEnum.CONNECTING)) { this.logger.error('Refused to connect to ws, connection attempt already active', Object.assign(Object.assign({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey })); return; } if (!this.wsStore.getConnectionState(wsKey) || this.wsStore.isConnectionState(wsKey, websockets_1.WsConnectionStateEnum.INITIAL)) { this.setWsState(wsKey, websockets_1.WsConnectionStateEnum.CONNECTING); } if (!this.wsStore.getConnectionInProgressPromise(wsKey)) { this.wsStore.createConnectionInProgressPromise(wsKey, false); } const url = yield this.getWsUrl(wsKey); const ws = this.connectToWsUrl(url, wsKey); this.wsStore.setWs(wsKey, ws); return (_a = this.wsStore.getConnectionInProgressPromise(wsKey)) === null || _a === void 0 ? void 0 : _a.promise; } catch (err) { this.parseWsError('Connection failed', err, wsKey); this.reconnectWithDelay(wsKey, this.options.reconnectTimeout); } }); } connectToWsUrl(url, wsKey) { var _a; this.logger.trace(`Opening WS connection to URL: ${url}`, Object.assign(Object.assign({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey })); const agent = (_a = this.options.requestOptions) === null || _a === void 0 ? void 0 : _a.agent; const ws = new isomorphic_ws_1.default(url, undefined, agent ? { agent } : undefined); ws.onopen = (event) => this.onWsOpen(event, wsKey); ws.onmessage = (event) => this.onWsMessage(event, wsKey, ws); ws.onerror = (event) => this.parseWsError('Websocket onWsError', event, wsKey); ws.onclose = (event) => this.onWsClose(event, wsKey); return ws; } 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({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey })); break; default: if (this.wsStore.getConnectionState(wsKey) !== websockets_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({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey, error })); this.executeReconnectableClose(wsKey, 'unhandled onWsError'); } else { this.logger.info(`${wsKey} socket forcefully closed. Will not reconnect.`); } break; } this.logger.error(`parseWsError(${context}, ${error}, ${wsKey}) `, error); 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({}, websockets_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({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey })); } }); } reconnectWithDelay(wsKey, connectionDelayMs) { var _a; this.clearTimers(wsKey); if (!this.wsStore.isConnectionAttemptInProgress(wsKey)) { this.setWsState(wsKey, websockets_1.WsConnectionStateEnum.RECONNECTING); } 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.logger.info('Reconnecting to websocket', Object.assign(Object.assign({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey })); this.clearReconnectTimer(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({}, websockets_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({}, websockets_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, websockets_1.safeTerminateWs)(ws, false); } if (!wasOpen) { this.logger.info(`${reason} - socket already closed - trigger immediate reconnect`, Object.assign(Object.assign({}, websockets_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 market = 'all'; 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(market, operation, batch, wsKey); requestEvents.push(...subscribeRequestEvents); } return requestEvents; } const subscribeRequestEvents = yield this.getWsRequestEvents(market, operation, topics, wsKey); 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; } // 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)); const promises = []; for (const midflightRequest of subscribeWsMessages) { const wsMessage = midflightRequest.requestEvent; if (this.options.promiseSubscribeRequests) { promises.push(this.upsertPendingTopicSubscribeRequests(wsKey, midflightRequest)); } this.logger.trace(`Sending batch via message: "${JSON.stringify(wsMessage)}"`); this.tryWsSend(wsKey, JSON.stringify(wsMessage)); } this.logger.trace(`Finished subscribing to ${wsTopicRequests.length} "${wsKey}" topics in ${subscribeWsMessages.length} batches.`); return Promise.all(promises); }); } /** * 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)}"`); const promises = []; for (const midflightRequest of subscribeWsMessages) { const wsMessage = midflightRequest.requestEvent; if (this.options.promiseSubscribeRequests) { promises.push(this.upsertPendingTopicSubscribeRequests(wsKey, midflightRequest)); } this.logger.trace(`Sending batch via message: "${wsMessage}"`); this.tryWsSend(wsKey, JSON.stringify(wsMessage)); } this.logger.trace(`Finished unsubscribing to ${wsTopicRequests.length} "${wsKey}" topics in ${subscribeWsMessages.length} batches.`); return Promise.all(promises); }); } /** * 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({}, websockets_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({}, websockets_1.WS_LOGGER_CATEGORY), { wsMessage, wsKey, exception: e })); if (throwExceptions) { throw e; } } } onWsOpen(event, wsKey) { return __awaiter(this, void 0, void 0, function* () { const isFreshConnectionAttempt = this.wsStore.isConnectionState(wsKey, websockets_1.WsConnectionStateEnum.CONNECTING); const isReconnectionAttempt = this.wsStore.isConnectionState(wsKey, websockets_1.WsConnectionStateEnum.RECONNECTING); if (isFreshConnectionAttempt) { this.logger.info('Websocket connected', Object.assign(Object.assign({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey, testnet: this.options.testnet === true, market: this.options.market })); this.emit('open', { wsKey, event }); } else if (isReconnectionAttempt) { this.logger.info('Websocket reconnected', Object.assign(Object.assign({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey, testnet: this.options.testnet === true, market: this.options.market })); this.emit('reconnected', { wsKey, event }); } this.setWsState(wsKey, websockets_1.WsConnectionStateEnum.CONNECTED); this.logger.trace('Enabled ping timer', Object.assign(Object.assign({}, websockets_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, }); } } 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); // Reconnect to topics known before it connected const { privateReqs, publicReqs } = this.sortTopicRequestsIntoPublicPrivate([...this.wsStore.getTopics(wsKey)], wsKey); // Request sub to public topics, if any try { yield this.requestSubscribeTopics(wsKey, publicReqs); } catch (e) { this.logger.error(`onWsOpen(): exception in public requestSubscribeTopics(${wsKey}): `, publicReqs, e); } // 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) { try { this.requestSubscribeTopics(wsKey, privateReqs); } catch (e) { this.logger.error(`onWsOpen(): exception in private requestSubscribeTopics(${wsKey}: `, privateReqs, e); } } // Some websockets require an auth packet to be sent after opening the connection if (this.isAuthOnConnectWsKey(wsKey) && this.options.authPrivateConnectionsOnConnect) { yield this.sendAuthRequest(wsKey); } }); } /** * 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, }); } } 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({}, websockets_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({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey, event })); this.sendPongEvent(wsKey, ws); return; } if ((0, types_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({}, websockets_1.WS_LOGGER_CATEGORY), { message: data || 'no message', dataType, event, wsKey })); return this.emit('update', Object.assign(Object.assign({}, event), { wsKey })); } for (const emittable of emittableEvents) { if (this.isWsPong(emittable)) { this.logger.trace('Received pong2', Object.assign(Object.assign({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey, data })); continue; } const emittableFinalEvent = Object.assign(Object.assign({}, emittable.event), { wsKey, isWSAPIResponse: emittable.isWSAPIResponse }); if (emittable.eventType === 'authenticated') { this.logger.trace('Successfully authenticated', Object.assign(Object.assign({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey, emittable })); this.emit(emittable.eventType, emittableFinalEvent); this.onWsAuthenticated(wsKey, emittable.event); continue; } // 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({}, websockets_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({}, websockets_1.WS_LOGGER_CATEGORY), { error: e, event, wsKey })); } } onWsClose(event, wsKey) { this.logger.info('Websocket connection closed', Object.assign(Object.assign({}, websockets_1.WS_LOGGER_CATEGORY), { wsKey })); const wsState = this.wsStore.get(wsKey, true); wsState.isAuthenticated = false; if (this.wsStore.getConnectionState(wsKey) !== websockets_1.WsConnectionStateEnum.CLOSING) { 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.clearTopicsPendingSubscriptions(wsKey, true, 'WS Closed'); this.setWsState(wsKey, websockets_1.WsConnectionStateEnum.INITIAL); this.reconnectWithDelay(wsKey, this.options.reconnectTimeout); this.emit('reconnect', { wsKey, event }); } else { // 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, websockets_1.WsConnectionStateEnum.INITIAL); 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, websockets_1.WsConnectionStateEnum.CONNECTED); if (!isConnected) { 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(): 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(): 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, websockets_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