UNPKG

bitget-api

Version:

Complete Node.js & JavaScript SDK for Bitget V1-V3 REST APIs & WebSockets, with TypeScript & end-to-end tests.

764 lines 34.3 kB
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ import EventEmitter from 'events'; import WebSocket from 'isomorphic-ws'; import { isMessageEvent, } from '../types/websockets/ws-events.js'; import { DefaultLogger } from './logger.js'; import { checkWebCryptoAPISupported } from './webCryptoAPI.js'; import { getNormalisedTopicRequests, safeTerminateWs, WS_LOGGER_CATEGORY, } from './websocket-util.js'; import WsStore from './WsStore.js'; import { WsConnectionStateEnum } from './WsStore.types.js'; /** * 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 { ...emittable.event, wsKey: wsKey, isWSAPIResponse: !!isWSAPIResponse, }; } return { ...emittable, wsKey: wsKey, isWSAPIResponse: !!isWSAPIResponse, }; } export class BaseWebsocketClient extends EventEmitter { /** * State store to track a list of topics (topic requests) we are expected to be subscribed to if reconnected */ wsStore; logger; options; wsApiRequestId = 0; timeOffsetMs = 0; /** * { [wsKey]: { [requestId]: request } } */ midflightRequestCache = {}; constructor(options, logger) { super(); this.logger = logger || DefaultLogger; this.wsStore = new WsStore(this.logger); this.options = { demoTrading: false, pongTimeout: 1000, pingInterval: 10000, reconnectTimeout: 500, recvWindow: 0, // 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, }; // Check Web Crypto API support when credentials are provided and no custom sign function is used if (this.options.apiKey && this.options.apiSecret && this.options.apiPass && !this.options.customSignMessageFn) { // 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 */ async subscribeTopicsForWsKey(wsTopicRequests, wsKey) { const normalisedTopicRequests = 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, 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.', { ...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 = this.wsStore.get(wsKey)?.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); } async unsubscribeTopicsForWsKey(wsTopicRequests, wsKey) { const normalisedTopicRequests = 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, 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 = this.wsStore.get(wsKey)?.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', { ...WS_LOGGER_CATEGORY, wsKey }); this.setWsState(wsKey, WsConnectionStateEnum.CLOSING); this.clearTimers(wsKey); const ws = this.getWs(wsKey); ws?.close(); if (force) { 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, WsConnectionStateEnum.CONNECTED); } /** * Request connection to a specific websocket, instead of waiting for automatic connection. */ async connect(wsKey, customUrl, throwOnError) { try { if (this.wsStore.isWsOpen(wsKey)) { this.logger.error('Refused to connect to ws with existing active connection', { ...WS_LOGGER_CATEGORY, wsKey }); return { wsKey, ws: this.wsStore.getWs(wsKey) }; } if (this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING)) { this.logger.error('Refused to connect to ws, connection attempt already active', { ...WS_LOGGER_CATEGORY, wsKey }); return this.wsStore.getConnectionInProgressPromise(wsKey)?.promise; } if (!this.wsStore.getConnectionState(wsKey) || this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.INITIAL)) { this.setWsState(wsKey, WsConnectionStateEnum.CONNECTING); } if (!this.wsStore.getConnectionInProgressPromise(wsKey)) { this.wsStore.createConnectionInProgressPromise(wsKey, false); } const url = customUrl || (await 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 this.wsStore.getConnectionInProgressPromise(wsKey)?.promise; } connectToWsUrl(url, wsKey) { this.logger.trace(`Opening WS connection to URL: ${url}`, { ...WS_LOGGER_CATEGORY, wsKey, }); const { protocols = [], ...wsOptions } = this.options.wsOptions || {}; const ws = new WebSocket(url, protocols, wsOptions); 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); // Native ws ping/pong frames are not in use for bitget // 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; } parseWsError(context, error, wsKey) { if (!error.message) { this.logger.error(`${context} due to unexpected error: `, error); this.emit('response', { ...error, wsKey }); this.emit('exception', { ...error, wsKey }); return; } switch (error.message) { case 'Unexpected server response: 401': this.logger.error(`${context} due to 401 authorization failure.`, { ...WS_LOGGER_CATEGORY, wsKey, }); break; default: this.logger.error(`${context} due to unexpected response error: "${error?.msg || error?.message || error}"`, { ...WS_LOGGER_CATEGORY, wsKey, error }); break; } this.emit('response', { ...error, wsKey }); this.emit('exception', { ...error, wsKey }); } /** Get a signature, build the auth request and send it */ async sendAuthRequest(wsKey) { try { this.logger.trace('Sending auth request...', { ...WS_LOGGER_CATEGORY, wsKey, }); await this.assertIsConnected(wsKey); if (!this.wsStore.getAuthenticationInProgressPromise(wsKey)) { this.wsStore.createAuthenticationInProgressPromise(wsKey, false); } const request = await this.getWsAuthRequestEvent(wsKey); // console.log('ws auth req', request); this.tryWsSend(wsKey, JSON.stringify(request)); return this.wsStore.getAuthenticationInProgressPromise(wsKey)?.promise; } catch (e) { this.logger.trace(e, { ...WS_LOGGER_CATEGORY, wsKey }); } } reconnectWithDelay(wsKey, connectionDelayMs) { this.clearTimers(wsKey); if (this.wsStore.getConnectionState(wsKey) !== WsConnectionStateEnum.CONNECTING) { this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING); } this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => { this.logger.info('Reconnecting to websocket', { ...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', { ...WS_LOGGER_CATEGORY, wsKey }); this.tryWsSend(wsKey, 'ping'); this.wsStore.get(wsKey, true).activePongTimer = setTimeout(() => { this.logger.info('Pong timeout - closing socket to reconnect', { ...WS_LOGGER_CATEGORY, wsKey, }); safeTerminateWs(this.getWs(wsKey), true); delete this.wsStore.get(wsKey, true).activePongTimer; }, this.options.pongTimeout); } clearTimers(wsKey) { this.clearPingTimer(wsKey); this.clearPongTimer(wsKey); const wsState = this.wsStore.get(wsKey); if (wsState?.activeReconnectTimer) { clearTimeout(wsState.activeReconnectTimer); } } // Send a ping at intervals clearPingTimer(wsKey) { const wsState = this.wsStore.get(wsKey); if (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?.activePongTimer) { clearTimeout(wsState.activePongTimer); wsState.activePongTimer = 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. */ async getWsOperationEventsForTopics(topics, wsKey, operation) { 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 = await this.getWsRequestEvents(operation, batch, wsKey); requestEvents.push(...subscribeRequestEvents); } return requestEvents; } const subscribeRequestEvents = await this.getWsRequestEvents(operation, topics, wsKey); return subscribeRequestEvents; } /** * @private Use the `subscribe(topics)` method to subscribe to topics. Send WS message to subscribe to topics. */ async requestSubscribeTopics(wsKey, wsTopicRequests) { if (!wsTopicRequests.length) { return; } // Automatically splits requests into smaller batches, if needed const subscribeWsMessages = await 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}"`, `Sending batch via message: "${JSON.stringify(wsMessage)}"`); 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]; } } } /** * @private Use the `unsubscribe(topics)` method to unsubscribe from topics. Send WS message to unsubscribe from topics. */ async requestUnsubscribeTopics(wsKey, wsTopicRequests) { if (!wsTopicRequests.length) { return; } const subscribeWsMessages = await 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.`); // const wsMessage = JSON.stringify({ // op: 'unsubscribe', // args: wsTopicRequests, // }); } getCachedMidFlightRequest(wsKey, requestKey) { if (!this.midflightRequestCache[wsKey]) { this.midflightRequestCache[wsKey] = {}; } return this.midflightRequestCache[wsKey][requestKey]; } // Not in use for Bitget. If desired, call from resolveEmittableEvents() for WS API responses. // See binance SDK for reference removeCachedMidFlightRequest(wsKey, requestKey) { if (this.getCachedMidFlightRequest(wsKey, requestKey)) { delete this.midflightRequestCache[wsKey][requestKey]; } } tryWsSend(wsKey, wsMessage, throwExceptions) { try { this.logger.trace('Sending upstream ws message: ', { ...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', { ...WS_LOGGER_CATEGORY, wsMessage, wsKey, exception: e, }); if (throwExceptions) { throw e; } } } async onWsOpen(event, wsKey, url, ws) { const isFreshConnectionAttempt = this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING); const isReconnectionAttempt = this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.RECONNECTING); if (isFreshConnectionAttempt) { this.logger.info('Websocket connected', { ...WS_LOGGER_CATEGORY, wsKey, }); this.emit('open', { wsKey, event, wsUrl: url, ws }); } else if (isReconnectionAttempt) { this.logger.info('Websocket reconnected', { ...WS_LOGGER_CATEGORY, wsKey, }); this.emit('reconnected', { wsKey, event, wsUrl: url, ws }); } this.setWsState(wsKey, WsConnectionStateEnum.CONNECTED); if (!this.options.disableHeartbeat) { this.logger.trace('Enabled ping timer', { ...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?.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) { await 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 "auth attempt in progress" promise try { const inProgressPromise = this.wsStore.getAuthenticationInProgressPromise(wsKey); if (inProgressPromise?.resolve) { inProgressPromise.resolve({ wsKey, event, ws: wsState.ws, }); } } catch (e) { this.logger.error('Exception trying to resolve "authenticationInProgress" 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); } } } /** * Raw incoming event handler. Parsing happens in integration layer via resolveEmittableEvents(). */ 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', { ...WS_LOGGER_CATEGORY, wsKey, event: event?.data, }); return; } if (this.isWsPing(event)) { this.logger.trace('Received ping', { ...WS_LOGGER_CATEGORY, wsKey, event, }); this.sendPongEvent(wsKey, ws); return; } if (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', { ...WS_LOGGER_CATEGORY, message: data || 'no message', dataType, event, wsKey, }); return this.emit('update', { ...event, wsKey }); } for (const emittable of emittableEvents) { if (this.isWsPong(emittable)) { this.logger.trace('Received pong2', { ...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', { ...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', { ...WS_LOGGER_CATEGORY, message: event || 'no message', event, wsKey, }); } catch (e) { this.logger.error('Failed to parse ws event message', { ...WS_LOGGER_CATEGORY, error: e, event, wsKey, }); } } onWsClose(event, wsKey) { this.logger.info('Websocket connection closed', { ...WS_LOGGER_CATEGORY, wsKey, }); const wsState = this.wsStore.get(wsKey, true); wsState.isAuthenticated = false; if (this.wsStore.getConnectionState(wsKey) !== 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, WsConnectionStateEnum.INITIAL); this.reconnectWithDelay(wsKey, this.options.reconnectTimeout); this.emit('reconnect', { 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, 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) */ async assertIsConnected(wsKey) { const isConnected = this.getWsStore().isConnectionState(wsKey, 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...'); await 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...'); await 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) */ async assertIsAuthenticated(wsKey) { const isConnected = this.getWsStore().isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED); if (!isConnected) { this.logger.trace('assertIsAuthenticated(): connecting...'); await this.assertIsConnected(wsKey); } const inProgressPromise = this.getWsStore().getAuthenticationInProgressPromise(wsKey); // Already in progress? Await shared promise and retry if (inProgressPromise) { this.logger.trace('assertIsAuthenticated(): awaiting...'); await inProgressPromise.promise; this.logger.trace('assertIsAuthenticated(): authenticated!'); return; } const isAuthenticated = this.wsStore.get(wsKey)?.isAuthenticated; if (isAuthenticated) { // this.logger.trace('assertIsAuthenticated(): ok'); return; } // Start authentication, it should automatically store/return a promise. this.logger.trace('assertIsAuthenticated(): authenticating...'); await this.sendAuthRequest(wsKey); this.logger.trace('assertIsAuthenticated(): newly authenticated!'); } } //# sourceMappingURL=BaseWSClient.js.map