"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
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.WebsocketClient = exports.parseRawWsMessage = void 0;
const events_1 = require("events");
const isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
const coinm_client_1 = require("./coinm-client");
const logger_1 = require("./logger");
const main_client_1 = require("./main-client");
const usdm_client_1 = require("./usdm-client");
const beautifier_1 = __importDefault(require("./util/beautifier"));
const requestUtils_1 = require("./util/requestUtils");
const ws_utils_1 = require("./util/ws-utils");
const WsStore_1 = __importStar(require("./util/WsStore"));
const wsBaseEndpoints = {
    spot: 'wss://stream.binance.com:9443',
    margin: 'wss://stream.binance.com:9443',
    isolatedMargin: 'wss://stream.binance.com:9443',
    usdm: 'wss://fstream.binance.com',
    usdmTestnet: 'wss://stream.binancefuture.com',
    coinm: 'wss://dstream.binance.com',
    coinmTestnet: 'wss://dstream.binancefuture.com',
    options: 'wss://vstream.binance.com',
    optionsTestnet: 'wss://testnetws.binanceops.com',
};
const loggerCategory = { category: 'binance-ws' };
function throwUnhandledSwitch(x, msg) {
    throw new Error(msg);
}
function parseEventTypeFromMessage(parsedMsg) {
    var _a;
    if (parsedMsg === null || parsedMsg === void 0 ? void 0 : parsedMsg.e) {
        return parsedMsg.e;
    }
    if (Array.isArray(parsedMsg) && parsedMsg.length) {
        return (_a = parsedMsg[0]) === null || _a === void 0 ? void 0 : _a.e;
    }
    return;
}
/**
 * Try to resolve event.data. Example circumstance: {"stream":"!forceOrder@arr","data":{"e":"forceOrder","E":1634653599186,"o":{"s":"IOTXUSDT","S":"SELL","o":"LIMIT","f":"IOC","q":"3661","p":"0.06606","ap":"0.06669","X":"FILLED","l":"962","z":"3661","T":1634653599180}}}
 */
function parseRawWsMessage(event) {
    if (typeof event === 'string') {
        const parsedEvent = JSON.parse(event);
        if (parsedEvent.data) {
            if (typeof parsedEvent.data === 'string') {
                return parseRawWsMessage(parsedEvent.data);
            }
            return parsedEvent.data;
        }
    }
    if (event === null || event === void 0 ? void 0 : event.data) {
        return JSON.parse(event.data);
    }
    return event;
}
exports.parseRawWsMessage = parseRawWsMessage;
class WebsocketClient extends events_1.EventEmitter {
    constructor(options, logger) {
        super();
        this.logger = logger || logger_1.DefaultLogger;
        this.wsStore = new WsStore_1.default(this.logger);
        this.beautifier = new beautifier_1.default();
        this.restClients = {};
        this.options = Object.assign({ pongTimeout: 7500, pingInterval: 10000, reconnectTimeout: 500 }, options);
        this.listenKeyStateStore = {};
        this.wsUrlKeyMap = {};
        // add default error handling so this doesn't crash node (if the user didn't set a handler)
        this.on('error', () => { });
    }
    getRestClientOptions() {
        return Object.assign(Object.assign(Object.assign({}, this.options), this.options.restOptions), { api_key: this.options.api_key, api_secret: this.options.api_secret });
    }
    connectToWsUrl(url, wsKey, forceNewConnection) {
        const wsRefKey = wsKey || url;
        const oldWs = this.wsStore.getWs(wsRefKey);
        if (oldWs && this.wsStore.isWsOpen(wsRefKey) && !forceNewConnection) {
            this.logger.silly('connectToWsUrl(): Returning existing open WS connection', Object.assign(Object.assign({}, loggerCategory), { wsRefKey }));
            return oldWs;
        }
        this.logger.silly(`connectToWsUrl(): Opening WS connection to URL: ${url}`, Object.assign(Object.assign({}, loggerCategory), { wsRefKey }));
        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;
        if (typeof ws.on === 'function') {
            ws.on('ping', (event) => this.onWsPing(event, wsRefKey, ws, 'event'));
            ws.on('pong', (event) => this.onWsPong(event, wsRefKey, 'event'));
        }
        ws.onopen = (event) => this.onWsOpen(event, wsRefKey, url);
        ws.onerror = (event) => this.parseWsError('WS Error Event', event, wsRefKey, url);
        ws.onclose = (event) => this.onWsClose(event, wsRefKey, ws, url);
        ws.onmessage = (event) => this.onWsMessage(event, wsRefKey, 'function');
        // 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, wsRefKey, ws, 'function');
        ws.onpong = (event) => this.onWsPong(event, wsRefKey, 'function');
        // Add ws connection with key to store
        this.wsStore.setWs(wsRefKey, ws);
        ws.wsKey = wsRefKey;
        return ws;
    }
    tryWsSend(wsKey, wsMessage) {
        try {
            this.logger.silly('Sending upstream ws message: ', Object.assign(Object.assign({}, loggerCategory), { wsMessage,
                wsKey }));
            if (!wsKey) {
                throw new Error('No wsKey provided');
            }
            const ws = this.getWs(wsKey);
            if (!ws) {
                throw new Error(`No active websocket connection exists for wsKey: ${wsKey}`);
            }
            ws.send(wsMessage);
        }
        catch (e) {
            this.logger.error('Failed to send WS message', Object.assign(Object.assign({}, loggerCategory), { wsMessage,
                wsKey, exception: e }));
        }
    }
    tryWsPing(wsKey) {
        try {
            // this.logger.silly(`Sending upstream ping: `, { ...loggerCategory, wsKey });
            if (!wsKey) {
                throw new Error('No wsKey provided');
            }
            const ws = this.getWs(wsKey);
            if (!ws) {
                throw new Error(`No active websocket connection exists for wsKey: ${wsKey}`);
            }
            // Binance allows unsolicited pongs, so we send both (though we expect a pong in response to our ping if the connection is still alive)
            if (ws.readyState === 1) {
                ws.ping();
                ws.pong();
            }
            else {
                this.logger.silly('WS ready state not open - refusing to send WS ping', Object.assign(Object.assign({}, loggerCategory), { wsKey, readyState: ws === null || ws === void 0 ? void 0 : ws.readyState }));
            }
        }
        catch (e) {
            this.logger.error('Failed to send WS ping', Object.assign(Object.assign({}, loggerCategory), { wsKey, exception: e }));
        }
    }
    onWsOpen(ws, wsKey, wsUrl) {
        this.logger.silly(`onWsOpen(): ${wsUrl} : ${wsKey}`);
        if (this.wsStore.isConnectionState(wsKey, WsStore_1.WsConnectionStateEnum.RECONNECTING)) {
            this.logger.info('Websocket reconnected', Object.assign(Object.assign({}, loggerCategory), { wsKey }));
            this.emit('reconnected', { wsKey, ws });
        }
        else {
            this.logger.info('Websocket connected', Object.assign(Object.assign({}, loggerCategory), { wsKey }));
            this.emit('open', { wsKey, ws });
        }
        this.setWsState(wsKey, WsStore_1.WsConnectionStateEnum.CONNECTED);
        const topics = [...this.wsStore.getTopics(wsKey)];
        if (topics.length) {
            this.requestSubscribeTopics(wsKey, topics);
        }
        if (!this.options.disableHeartbeat) {
            const wsState = this.wsStore.get(wsKey, true);
            if (wsState.activePingTimer) {
                clearInterval(wsState.activePingTimer);
            }
            wsState.activePingTimer = setInterval(() => this.sendPing(wsKey, wsUrl), this.options.pingInterval);
        }
    }
    onWsClose(event, wsKey, ws, wsUrl) {
        var _a;
        const wsConnectionState = this.wsStore.getConnectionState(wsKey);
        const { market, listenKey, isUserData } = (0, requestUtils_1.getContextFromWsKey)(wsKey);
        this.logger.info('Websocket connection closed', Object.assign(Object.assign({}, loggerCategory), { wsKey, eventCloseCode: (_a = event === null || event === void 0 ? void 0 : event.target) === null || _a === void 0 ? void 0 : _a._closeCode, wsConnectionState,
            isUserData,
            listenKey,
            market }));
        // Clear any timers before we initiate revival
        this.clearTimers(wsKey);
        // User data sockets include the listen key. To prevent accummulation in memory we should clean up old disconnected states
        if (isUserData) {
            this.wsStore.delete(wsKey);
            if (listenKey) {
                this.clearUserDataKeepAliveTimer(listenKey);
            }
        }
        if (wsConnectionState !== WsStore_1.WsConnectionStateEnum.CLOSING) {
            this.reconnectWithDelay(wsKey, this.options.reconnectTimeout, wsUrl);
            this.emit('reconnecting', { wsKey, event, ws });
        }
        else {
            this.setWsState(wsKey, WsStore_1.WsConnectionStateEnum.INITIAL);
            this.emit('close', { wsKey, event, ws });
        }
    }
    onWsMessage(event, wsKey, source) {
        try {
            this.clearPongTimer(wsKey);
            const msg = parseRawWsMessage(event);
            // Edge case where raw event does not include event type, detect using wsKey and mutate msg.e
            (0, requestUtils_1.appendEventIfMissing)(msg, wsKey);
            (0, requestUtils_1.appendEventMarket)(msg, wsKey);
            const eventType = parseEventTypeFromMessage(msg);
            if (eventType) {
                this.emit('message', msg);
                if (eventType === 'listenKeyExpired') {
                    const { market } = (0, requestUtils_1.getContextFromWsKey)(wsKey);
                    this.logger.info(`${market} listenKey expired - attempting to respawn user data stream: ${wsKey}`);
                    // Just closing the connection (with the last parameter as true) will handle cleanup and respawn
                    const shouldTriggerReconnect = true;
                    this.close(wsKey, shouldTriggerReconnect);
                }
                if (this.options.beautify) {
                    const beautifiedMessage = this.beautifier.beautifyWsMessage(msg, eventType, false);
                    this.emit('formattedMessage', beautifiedMessage);
                    // emit a separate event for user data messages
                    if (!Array.isArray(beautifiedMessage)) {
                        if ([
                            'balanceUpdate',
                            'executionReport',
                            'listStatus',
                            'listenKeyExpired',
                            'outboundAccountPosition',
                            'ACCOUNT_CONFIG_UPDATE',
                            'ACCOUNT_UPDATE',
                            'MARGIN_CALL',
                            'ORDER_TRADE_UPDATE',
                            'CONDITIONAL_ORDER_TRIGGER_REJECT',
                        ].includes(eventType)) {
                            this.emit('formattedUserDataMessage', beautifiedMessage);
                        }
                    }
                }
                return;
            }
            if (msg.result !== undefined) {
                this.emit('reply', {
                    type: event.type,
                    data: msg,
                    wsKey,
                });
                return;
            }
            this.logger.warning('Bug? Unhandled ws message event type. Check if appendEventIfMissing needs to parse wsKey.', Object.assign(Object.assign({}, loggerCategory), { parsedMessage: JSON.stringify(msg), rawEvent: event, wsKey,
                source }));
        }
        catch (e) {
            this.logger.error('Exception parsing ws message: ', Object.assign(Object.assign({}, loggerCategory), { rawEvent: event, wsKey, error: e, source }));
            this.emit('error', { wsKey, error: e, rawEvent: event, source });
        }
    }
    sendPing(wsKey, wsUrl) {
        this.clearPongTimer(wsKey);
        this.logger.silly('Sending ping', Object.assign(Object.assign({}, loggerCategory), { wsKey }));
        this.tryWsPing(wsKey);
        this.wsStore.get(wsKey, true).activePongTimer = setTimeout(() => this.executeReconnectableClose(wsKey, 'Pong timeout', wsUrl), this.options.pongTimeout);
    }
    onWsPing(event, wsKey, ws, source) {
        this.logger.silly('Received ping, sending pong frame', Object.assign(Object.assign({}, loggerCategory), { wsKey,
            source }));
        ws.pong();
    }
    onWsPong(event, wsKey, source) {
        this.logger.silly('Received pong, clearing pong timer', Object.assign(Object.assign({}, loggerCategory), { wsKey,
            source }));
        this.clearPongTimer(wsKey);
    }
    /**
     * 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, wsUrl) {
        this.logger.info(`${reason} - closing socket to reconnect`, Object.assign(Object.assign({}, loggerCategory), { wsKey,
            reason }));
        const wasOpen = this.wsStore.isWsOpen(wsKey);
        (0, ws_utils_1.safeTerminateWs)(this.getWs(wsKey));
        this.clearPingTimer(wsKey);
        this.clearPongTimer(wsKey);
        if (!wasOpen) {
            this.logger.info(`${reason} - socket already closed - trigger immediate reconnect`, Object.assign(Object.assign({}, loggerCategory), { wsKey,
                reason }));
            this.reconnectWithDelay(wsKey, this.options.reconnectTimeout, wsUrl);
        }
    }
    close(wsKey, shouldReconnectAfterClose) {
        var _a;
        this.logger.info('Closing connection', Object.assign(Object.assign({}, loggerCategory), { wsKey, willReconnect: shouldReconnectAfterClose }));
        this.setWsState(wsKey, shouldReconnectAfterClose
            ? WsStore_1.WsConnectionStateEnum.RECONNECTING
            : WsStore_1.WsConnectionStateEnum.CLOSING);
        this.clearTimers(wsKey);
        (_a = this.getWs(wsKey)) === null || _a === void 0 ? void 0 : _a.close();
        const { listenKey } = (0, requestUtils_1.getContextFromWsKey)(wsKey);
        if (listenKey) {
            this.teardownUserDataListenKey(listenKey, this.getWs(wsKey));
        }
        else {
            (0, ws_utils_1.safeTerminateWs)(this.getWs(wsKey));
        }
    }
    closeAll(shouldReconnectAfterClose) {
        const keys = this.wsStore.getKeys();
        this.logger.info(`Closing all ws connections: ${keys}`);
        keys.forEach((key) => {
            this.close(key, shouldReconnectAfterClose);
        });
    }
    closeWs(ws, shouldReconnectAfterClose) {
        const wsKey = this.wsUrlKeyMap[ws.url] || (ws === null || ws === void 0 ? void 0 : ws.wsKey);
        if (!wsKey) {
            throw new Error('Cannot close websocket as it has no known wsKey attached.');
        }
        return this.close(wsKey, shouldReconnectAfterClose);
    }
    parseWsError(context, error, wsKey, wsUrl) {
        this.logger.error(context, Object.assign(Object.assign({}, loggerCategory), { wsKey, error }));
        if (!error.message) {
            this.logger.error(`${context} due to unexpected error: `, error);
            this.emit('error', { error, wsKey, wsUrl });
            return;
        }
        switch (error.message) {
            case 'Unexpected server response: 401':
                this.logger.error(`${context} due to 401 authorization failure.`, Object.assign(Object.assign({}, loggerCategory), { wsKey }));
                break;
            default:
                if (this.wsStore.getConnectionState(wsKey) !==
                    WsStore_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({}, loggerCategory), { wsKey, error }));
                    this.executeReconnectableClose(wsKey, 'unhandled onWsError', wsUrl);
                }
                else {
                    this.logger.info(`${wsKey} socket forcefully closed. Will not reconnect.`);
                }
                break;
        }
        this.emit('error', { error, wsKey, wsUrl });
    }
    reconnectWithDelay(wsKey, connectionDelayMs, wsUrl) {
        var _a;
        this.clearTimers(wsKey);
        if (this.wsStore.getConnectionState(wsKey) !==
            WsStore_1.WsConnectionStateEnum.CONNECTING) {
            this.setWsState(wsKey, WsStore_1.WsConnectionStateEnum.RECONNECTING);
        }
        this.logger.info('Reconnecting to websocket with delay...', Object.assign(Object.assign({}, loggerCategory), { 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);
            if (wsKey.includes('userData')) {
                const { market, symbol, isTestnet } = (0, requestUtils_1.getContextFromWsKey)(wsKey);
                this.logger.info('Reconnecting to user data stream', Object.assign(Object.assign({}, loggerCategory), { wsKey,
                    market,
                    symbol }));
                // We'll set a new one once the new stream respawns, with a diff listenKey in the key
                this.wsStore.delete(wsKey);
                this.respawnUserDataStream(market, symbol, isTestnet);
                return;
            }
            this.logger.info('Reconnecting to public websocket', Object.assign(Object.assign({}, loggerCategory), { wsKey,
                wsUrl }));
            this.connectToWsUrl(wsUrl, wsKey);
        }, connectionDelayMs);
    }
    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;
        }
    }
    // Timer tracking that a reconnect is about to happen / in progress
    clearReconnectTimer(wsKey) {
        const wsState = this.wsStore.get(wsKey);
        if (wsState === null || wsState === void 0 ? void 0 : wsState.activeReconnectTimer) {
            clearTimeout(wsState.activeReconnectTimer);
            wsState.activeReconnectTimer = undefined;
        }
    }
    clearUserDataKeepAliveTimer(listenKey) {
        const state = this.listenKeyStateStore[listenKey];
        if (!state) {
            return;
        }
        if (state.keepAliveTimer) {
            this.logger.silly(`Clearing old listen key interval timer for ${listenKey}`);
            clearInterval(state.keepAliveTimer);
        }
        if (state.keepAliveRetryTimer) {
            this.logger.silly(`Clearing old listen key keepAliveRetry timer for ${listenKey}`);
            clearTimeout(state.keepAliveRetryTimer);
        }
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    getWsBaseUrl(market, wsKey) {
        if (this.options.wsUrl) {
            return this.options.wsUrl;
        }
        return wsBaseEndpoints[market];
    }
    getWs(wsKey) {
        return this.wsStore.getWs(wsKey);
    }
    setWsState(wsKey, state) {
        this.wsStore.setConnectionState(wsKey, state);
    }
    getSpotRestClient() {
        if (!this.restClients.spot) {
            this.restClients.spot = new main_client_1.MainClient(this.getRestClientOptions(), this.options.requestOptions);
        }
        return this.restClients.spot;
    }
    getUSDMRestClient(isTestnet) {
        if (isTestnet) {
            if (!this.restClients.usdmFuturesTestnet) {
                this.restClients.usdmFuturesTestnet = new usdm_client_1.USDMClient(this.getRestClientOptions(), this.options.requestOptions, isTestnet);
            }
            return this.restClients.usdmFuturesTestnet;
        }
        if (!this.restClients.usdmFutures) {
            this.restClients.usdmFutures = new usdm_client_1.USDMClient(this.getRestClientOptions(), this.options.requestOptions);
        }
        return this.restClients.usdmFutures;
    }
    getCOINMRestClient(isTestnet) {
        if (isTestnet) {
            if (!this.restClients.coinmFuturesTestnet) {
                this.restClients.coinmFuturesTestnet = new coinm_client_1.CoinMClient(this.getRestClientOptions(), this.options.requestOptions, isTestnet);
            }
            return this.restClients.coinmFuturesTestnet;
        }
        if (!this.restClients.coinmFutures) {
            this.restClients.coinmFutures = new coinm_client_1.CoinMClient(this.getRestClientOptions(), this.options.requestOptions);
        }
        return this.restClients.coinmFutures;
    }
    /**
     * Send WS message to subscribe to topics. Use subscribe() to call this.
     */
    requestSubscribeTopics(wsKey, topics) {
        const wsMessage = JSON.stringify({
            method: 'SUBSCRIBE',
            params: topics,
            id: new Date().getTime(),
        });
        this.tryWsSend(wsKey, wsMessage);
    }
    /**
     * Send WS message to unsubscribe from topics. Use unsubscribe() to call this.
     */
    requestUnsubscribeTopics(wsKey, topics) {
        const wsMessage = JSON.stringify({
            op: 'UNSUBSCRIBE',
            params: topics,
            id: new Date().getTime(),
        });
        this.tryWsSend(wsKey, wsMessage);
    }
    /**
     * Send WS message to unsubscribe from topics.
     */
    requestListSubscriptions(wsKey, requestId) {
        const wsMessage = JSON.stringify({
            method: 'LIST_SUBSCRIPTIONS',
            id: requestId,
        });
        this.tryWsSend(wsKey, wsMessage);
    }
    /**
     * Send WS message to set property state
     */
    requestSetProperty(wsKey, property, value, requestId) {
        const wsMessage = JSON.stringify({
            method: 'SET_PROPERTY',
            params: [property, value],
            id: requestId,
        });
        this.tryWsSend(wsKey, wsMessage);
    }
    /**
     * Send WS message to get property state
     */
    requestGetProperty(wsKey, property, requestId) {
        const wsMessage = JSON.stringify({
            method: 'GET_PROPERTY',
            params: [property],
            id: requestId,
        });
        this.tryWsSend(wsKey, wsMessage);
    }
    /**
     * --------------------------
     * User data listen key tracking & persistence
     * --------------------------
     **/
    getListenKeyState(listenKey, market) {
        const state = this.listenKeyStateStore[listenKey];
        if (state) {
            return state;
        }
        this.listenKeyStateStore[listenKey] = {
            market,
            lastKeepAlive: 0,
            keepAliveTimer: undefined,
            keepAliveRetryTimer: undefined,
            keepAliveFailures: 0,
        };
        return this.listenKeyStateStore[listenKey];
    }
    setKeepAliveListenKeyTimer(listenKey, market, ws, wsKey, symbol, isTestnet) {
        const listenKeyState = this.getListenKeyState(listenKey, market);
        this.clearUserDataKeepAliveTimer(listenKey);
        this.logger.silly(`Created new listen key 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);
    }
    sendKeepAliveForMarket(listenKey, market, ws, wsKey, symbol, isTestnet) {
        switch (market) {
            case 'spot':
                return this.getSpotRestClient().keepAliveSpotUserDataListenKey(listenKey);
            case 'margin':
                return this.getSpotRestClient().keepAliveMarginUserDataListenKey(listenKey);
            case 'isolatedMargin':
                return this.getSpotRestClient().keepAliveIsolatedMarginUserDataListenKey({ listenKey, symbol: symbol });
            case 'coinm':
            case 'options':
            case 'optionsTestnet':
            case 'usdm':
                return this.getUSDMRestClient().keepAliveFuturesUserDataListenKey();
            case 'usdmTestnet':
                return this.getUSDMRestClient(isTestnet).keepAliveFuturesUserDataListenKey();
            case 'coinmTestnet':
                return this.getUSDMRestClient(isTestnet).keepAliveFuturesUserDataListenKey();
            default:
                throwUnhandledSwitch(market, `Failed to send keep alive for user data stream in unhandled market ${market}`);
        }
    }
    checkKeepAliveListenKey(listenKey, market, ws, wsKey, symbol, isTestnet) {
        return __awaiter(this, void 0, void 0, function* () {
            const listenKeyState = this.getListenKeyState(listenKey, market);
            try {
                if (listenKeyState.keepAliveRetryTimer) {
                    clearTimeout(listenKeyState.keepAliveRetryTimer);
                    listenKeyState.keepAliveRetryTimer = undefined;
                }
                // Simple way to test keep alive failure handling:
                // throw new Error(`Fake keep alive failure`);
                yield this.sendKeepAliveForMarket(listenKey, market, ws, wsKey, symbol, isTestnet);
                listenKeyState.lastKeepAlive = Date.now();
                listenKeyState.keepAliveFailures = 0;
                this.logger.info(`Completed keep alive cycle for listenKey(${listenKey}) in market(${market})`, Object.assign(Object.assign({}, loggerCategory), { listenKey }));
            }
            catch (e) {
                listenKeyState.keepAliveFailures++;
                // code: -1125,
                // message: 'This listenKey does not exist.',
                const errorCode = e === null || e === void 0 ? void 0 : e.code;
                if (errorCode === -1125) {
                    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({}, loggerCategory), { listenKey, error: e, errorCode, errorMsg: e === null || e === void 0 ? void 0 : e.message }));
                    const shouldReconnectAfterClose = false;
                    this.close(wsKey, shouldReconnectAfterClose);
                    this.respawnUserDataStream(market, symbol);
                    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({}, loggerCategory), { listenKey, error: e }));
                    // 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
                    const shouldReconnectAfterClose = false;
                    this.close(wsKey, shouldReconnectAfterClose);
                    // Then respawn a connection with a potentially new listen key (since the old one may be invalid now)
                    this.respawnUserDataStream(market, symbol);
                    return;
                }
                const reconnectDelaySeconds = 1000 * 15;
                this.logger.warning(`Userdata keep alive request failed due to error, trying again with short delay (${reconnectDelaySeconds} seconds)`, Object.assign(Object.assign({}, loggerCategory), { listenKey, error: e, keepAliveAttempts: listenKeyState.keepAliveFailures }));
                listenKeyState.keepAliveRetryTimer = setTimeout(() => this.checkKeepAliveListenKey(listenKey, market, ws, wsKey, symbol), reconnectDelaySeconds);
            }
        });
    }
    teardownUserDataListenKey(listenKey, ws) {
        if (listenKey) {
            this.clearUserDataKeepAliveTimer(listenKey);
            delete this.listenKeyStateStore[listenKey];
            (0, ws_utils_1.safeTerminateWs)(ws);
        }
    }
    respawnUserDataStream(market, symbol, isTestnet, respawnAttempt) {
        return __awaiter(this, void 0, void 0, function* () {
            // If another connection attempt is in progress for this listen key, don't initiate a retry or the risk is multiple connections on the same listen key
            const forceNewConnection = false;
            const isReconnecting = true;
            let ws;
            try {
                switch (market) {
                    case 'spot':
                        ws = yield this.subscribeSpotUserDataStream(forceNewConnection, isReconnecting);
                        break;
                    case 'margin':
                        ws = yield this.subscribeMarginUserDataStream(forceNewConnection, isReconnecting);
                        break;
                    case 'isolatedMargin':
                        ws = yield this.subscribeIsolatedMarginUserDataStream(symbol, forceNewConnection, isReconnecting);
                        break;
                    case 'usdm':
                        ws = yield this.subscribeUsdFuturesUserDataStream(isTestnet, forceNewConnection, isReconnecting);
                        break;
                    case 'usdmTestnet':
                        ws = yield this.subscribeUsdFuturesUserDataStream(true, forceNewConnection, isReconnecting);
                        break;
                    case 'coinm':
                        ws = yield this.subscribeCoinFuturesUserDataStream(isTestnet, forceNewConnection, isReconnecting);
                        break;
                    case 'coinmTestnet':
                        ws = yield this.subscribeCoinFuturesUserDataStream(true, forceNewConnection, isReconnecting);
                        break;
                    case 'options':
                    case 'optionsTestnet':
                        throw new Error('TODO: respawn other user data streams once subscribe methods have been added');
                    default:
                        throwUnhandledSwitch(market, `Failed to respawn user data stream - unhandled market: ${market}`);
                }
            }
            catch (e) {
                this.logger.error('Exception trying to spawn user data stream', Object.assign(Object.assign({}, loggerCategory), { market,
                    symbol,
                    isTestnet, error: e }));
                this.emit('error', { wsKey: market + '_' + 'userData', error: e });
            }
            if (!ws) {
                const delayInSeconds = 2;
                this.logger.error('User key respawn failed, trying again with short delay', Object.assign(Object.assign({}, loggerCategory), { market,
                    symbol,
                    isTestnet,
                    respawnAttempt,
                    delayInSeconds }));
                setTimeout(() => this.respawnUserDataStream(market, symbol, isTestnet, respawnAttempt ? respawnAttempt + 1 : 1), 1000 * delayInSeconds);
            }
        });
    }
    /**
     * --------------------------
     * Universal market websocket streams (may apply to one or more API markets)
     * --------------------------
     **/
    /**
     * Subscribe to a universal market websocket stream
     */
    subscribeEndpoint(endpoint, market, forceNewConnection) {
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, endpoint);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${endpoint}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to aggregate trades for a symbol in a market category
     */
    subscribeAggregateTrades(symbol, market, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'aggTrade';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, lowerCaseSymbol);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to trades for a symbol in a market category
     * IMPORTANT: This topic for usdm and coinm is not listed in the api docs and might stop working without warning
     */
    subscribeTrades(symbol, market, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'trade';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, lowerCaseSymbol);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to coin index for a symbol in COINM Futures markets
     */
    subscribeCoinIndexPrice(symbol, updateSpeedMs = 3000, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'indexPrice';
        const speedSuffix = updateSpeedMs === 1000 ? '@1s' : '';
        const market = 'coinm';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, lowerCaseSymbol);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) +
            `/ws/${lowerCaseSymbol}@${streamName}${speedSuffix}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to mark price for a symbol in a market category
     */
    subscribeMarkPrice(symbol, market, updateSpeedMs = 3000, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'markPrice';
        const speedSuffix = updateSpeedMs === 1000 ? '@1s' : '';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, lowerCaseSymbol);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) +
            `/ws/${lowerCaseSymbol}@${streamName}${speedSuffix}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to mark price for all symbols in a market category
     */
    subscribeAllMarketMarkPrice(market, updateSpeedMs = 3000, forceNewConnection) {
        const streamName = '!markPrice@arr';
        const speedSuffix = updateSpeedMs === 1000 ? '@1s' : '';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${streamName}${speedSuffix}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to klines(candles) for a symbol in a market category
     */
    subscribeKlines(symbol, interval, market, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'kline';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, lowerCaseSymbol, interval);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) +
            `/ws/${lowerCaseSymbol}@${streamName}_${interval}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to continuous contract klines(candles) for a symbol futures
     */
    subscribeContinuousContractKlines(symbol, contractType, interval, market, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'continuousKline';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, lowerCaseSymbol, interval);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) +
            `/ws/${lowerCaseSymbol}_${contractType}@${streamName}_${interval}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to index klines(candles) for a symbol in a coinm futures
     */
    subscribeIndexKlines(symbol, interval, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'indexPriceKline';
        const market = 'coinm';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, lowerCaseSymbol, interval);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) +
            `/ws/${lowerCaseSymbol}@${streamName}_${interval}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to index klines(candles) for a symbol in a coinm futures
     */
    subscribeMarkPriceKlines(symbol, interval, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'markPrice_kline';
        const market = 'coinm';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, lowerCaseSymbol, interval);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) +
            `/ws/${lowerCaseSymbol}@${streamName}_${interval}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to mini 24hr ticker for a symbol in market category.
     */
    subscribeSymbolMini24hrTicker(symbol, market, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'miniTicker';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, lowerCaseSymbol);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to mini 24hr mini ticker in market category.
     */
    subscribeAllMini24hrTickers(market, forceNewConnection) {
        const streamName = 'miniTicker';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/!${streamName}@arr`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to 24hr ticker for a symbol in any market.
     */
    subscribeSymbol24hrTicker(symbol, market, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'ticker';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, lowerCaseSymbol);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to 24hr ticker in any market.
     */
    subscribeAll24hrTickers(market, forceNewConnection) {
        const streamName = 'ticker';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/!${streamName}@arr`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to rolling window ticker statistics for all market symbols,
     * computed over multiple windows. Note that only tickers that have
     * changed will be present in the array.
     *
     * Notes:
     * - Supported window sizes: 1h,4h,1d.
     * - Supported markets: spot
     */
    subscribeAllRollingWindowTickers(market, windowSize, forceNewConnection) {
        const streamName = 'ticker';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, windowSize);
        const wsUrl = this.getWsBaseUrl(market, wsKey) + `/ws/!${streamName}_${windowSize}@arr`;
        return this.connectToWsUrl(wsUrl, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to best bid/ask for symbol in spot markets.
     */
    subscribeSymbolBookTicker(symbol, market, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'bookTicker';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, lowerCaseSymbol);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to best bid/ask for all symbols in spot markets.
     */
    subscribeAllBookTickers(market, forceNewConnection) {
        const streamName = 'bookTicker';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/!${streamName}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to best bid/ask for symbol in spot markets.
     */
    subscribeSymbolLiquidationOrders(symbol, market, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'forceOrder';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, lowerCaseSymbol);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to best bid/ask for all symbols in spot markets.
     */
    subscribeAllLiquidationOrders(market, forceNewConnection) {
        const streamName = 'forceOrder@arr';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/!${streamName}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to partial book depths (snapshots).
     *
     * Note:
     * - spot only supports 1000ms or 100ms for updateMs
     * - futures only support 100, 250 or 500ms for updateMs
     *
     * Use getContextFromWsKey(data.wsKey) to extract symbol from events
     */
    subscribePartialBookDepths(symbol, levels, updateMs, market, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'depth';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName, lowerCaseSymbol);
        const updateMsSuffx = typeof updateMs === 'number' ? `@${updateMs}ms` : '';
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) +
            `/ws/${lowerCaseSymbol}@${streamName}${levels}${updateMsSuffx}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to orderbook depth updates to locally manage an order book.
     *
     * Note that the updatems parameter depends on which market you're trading
     *
     * - Spot: https://binance-docs.github.io/apidocs/spot/en/#diff-depth-stream
     * - USDM Futures: https://binance-docs.github.io/apidocs/futures/en/#diff-book-depth-streams
     *
     * Use getContextFromWsKey(data.wsKey) to extract symbol from events
     */
    subscribeDiffBookDepth(symbol, updateMs = 100, market, forceNewConnection) {
        const lowerCaseSymbol = symbol.toLowerCase();
        const streamName = 'depth';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, 'diffBookDepth', lowerCaseSymbol, String(updateMs));
        const updateMsSuffx = typeof updateMs === 'number' ? `@${updateMs}ms` : '';
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) +
            `/ws/${lowerCaseSymbol}@${streamName}${updateMsSuffx}`, wsKey, forceNewConnection);
    }
    /**
     * Subscribe to best bid/ask for all symbols in spot markets.
     */
    subscribeContractInfoStream(market, forceNewConnection) {
        const streamName = '!contractInfo';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, streamName);
        return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${streamName}`, wsKey, forceNewConnection);
    }
    /**
     * --------------------------
     * SPOT market websocket streams
     * --------------------------
     **/
    /**
     * Subscribe to aggregate trades for a symbol in spot markets.
     */
    subscribeSpotAggregateTrades(symbol, forceNewConnection) {
        return this.subscribeAggregateTrades(symbol, 'spot', forceNewConnection);
    }
    /**
     * Subscribe to trades for a symbol in spot markets.
     */
    subscribeSpotTrades(symbol, forceNewConnection) {
        return this.subscribeTrades(symbol, 'spot', forceNewConnection);
    }
    /**
     * Subscribe to candles for a symbol in spot markets.
     */
    subscribeSpotKline(symbol, interval, forceNewConnection) {
        return this.subscribeKlines(symbol, interval, 'spot', forceNewConnection);
    }
    /**
     * Subscribe to mini 24hr ticker for a symbol in spot markets.
     */
    subscribeSpotSymbolMini24hrTicker(symbol, forceNewConnection) {
        return this.subscribeSymbolMini24hrTicker(symbol, 'spot', forceNewConnection);
    }
    /**
     * Subscribe to mini 24hr mini ticker in spot markets.
     */
    subscribeSpotAllMini24hrTickers(forceNewConnection) {
        return this.subscribeAllMini24hrTickers('spot', forceNewConnection);
    }
    /**
     * Subscribe to 24hr ticker for a symbol in spot markets.
     */
    subscribeSpotSymbol24hrTicker(symbol, forceNewConnection) {
        return this.subscribeSymbol24hrTicker(symbol, 'spot', forceNewConnection);
    }
    /**
     * Subscribe to 24hr ticker in spot markets.
     */
    subscribeSpotAll24hrTickers(forceNewConnection) {
        return this.subscribeAll24hrTickers('spot', forceNewConnection);
    }
    /**
     * Subscribe to best bid/ask for symbol in spot markets.
     */
    subscribeSpotSymbolBookTicker(symbol, forceNewConnection) {
        return this.subscribeSymbolBookTicker(symbol, 'spot', forceNewConnection);
    }
    /**
     * Subscribe to best bid/ask for all symbols in spot markets.
     */
    subscribeSpotAllBookTickers(forceNewConnection) {
        return this.subscribeAllBookTickers('spot', forceNewConnection);
    }
    /**
     * Subscribe to top bid/ask levels for symbol in spot markets.
     */
    subscribeSpotPartialBookDepth(symbol, levels, updateMs = 1000, forceNewConnection) {
        return this.subscribePartialBookDepths(symbol, levels, updateMs, 'spot', forceNewConnection);
    }
    /**
     * Subscribe to spot orderbook depth updates to locally manage an order book.
     */
    subscribeSpotDiffBookDepth(symbol, updateMs = 1000, forceNewConnection) {
        return this.subscribeDiffBookDepth(symbol, updateMs, 'spot', forceNewConnection);
    }
    /**
     * Subscribe to a spot user data stream. Use REST client to generate and persist listen key.
     * Supports spot, margin & isolated margin listen keys.
     */
    subscribeSpotUserDataStreamWithListenKey(listenKey, forceNewConnection, isReconnecting) {
        const market = 'spot';
        const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, 'userData', undefined, listenKey);
        if (!forceNewConnection && this.wsStore.isWsConnecting(wsKey)) {
            this.logger.silly('Existing spot user data connection in progress for listen key. Avoiding duplicate');
            return this.getWs(wsKey);
        }
        this.setWsState(wsKey, isReconnecting
            ? WsStore_1.WsConnectionStateEnum.RECONNECTING
            : WsStore_1.WsConnectionStateEnum.CONNECTING);
        const ws = this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${listenKey}`, wsKey, forceNewConnection);
        // Start & store timer to keep alive listen key (and handle expiration)
        this.setKeepAliveListenKeyTimer(listenKey, market, ws, wsKey);
        return ws;
    }
    /**
     * Subscribe to spot user data stream - listen key is automatically generated. Calling multiple times only opens one connection.
     */
    subscribeSpotUserDataStream(forceNewConnection, isReconnecting) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const { listenKey } = yield this.getSpotRestClient().getSpotUserDataListenKey();
                return this.subscribeSpotUserDataStreamWithListenKey(listenKey, forceNewConnection, isReconnecting);
            }
            catch (e) {
                this.logger.error('Failed to connect to spot user data', Object.assign(Object.assign({}, loggerCategory), { error: e }));
                this.emit('error', { wsKey: 'spot' + '_' + 'userData', error: e });
            }
        });
    }
    /**
     * Subscribe to margin user data stream - listen key is automatically generated.
     */
    subscribeMarginUserDataStream(forceNewConnection, isReconnecting) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const { listenKey } = yield this.getSpotRestClient().getMarginUserDataListenKey();
                const market = 'margin';
                const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, 'userData', undefined, listenKey);
                if (!forceNewConnection && this.wsStore.isWsConnecting(wsKey)) {
                    this.logger.silly('Existing margin user data connection in progress for listen key. Avoiding duplicate');
                    return this.getWs(wsKey);
                }
                this.setWsState(wsKey, isReconnecting
                    ? WsStore_1.WsConnectionStateEnum.RECONNECTING
                    : WsStore_1.WsConnectionStateEnum.CONNECTING);
                const ws = this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${listenKey}`, wsKey, forceNewConnection);
                // Start & store timer to keep alive listen key (and handle expiration)
                this.setKeepAliveListenKeyTimer(listenKey, market, ws, wsKey);
                return ws;
            }
            catch (e) {
                this.logger.error('Failed to connect to margin user data', Object.assign(Object.assign({}, loggerCategory), { error: e }));
                this.emit('error', { wsKey: 'margin' + '_' + 'userData', error: e });
            }
        });
    }
    /**
     * Subscribe to isolated margin user data stream - listen key is automatically generated.
     */
    subscribeIsolatedMarginUserDataStream(symbol, forceNewConnection, isReconnecting) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const lowerCaseSymbol = symbol.toLowerCase();
                const { listenKey } = yield this.getSpotRestClient().getIsolatedMarginUserDataListenKey({
                    symbol: lowerCaseSymbol,
                });
                const market = 'isolatedMargin';
                const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, 'userData', lowerCaseSymbol, listenKey);
                if (!forceNewConnection && this.wsStore.isWsConnecting(wsKey)) {
                    this.logger.silly('Existing isolated margin user data connection in progress for listen key. Avoiding duplicate');
                    return this.getWs(wsKey);
                }
                this.setWsState(wsKey, isReconnecting
                    ? WsStore_1.WsConnectionStateEnum.RECONNECTING
                    : WsStore_1.WsConnectionStateEnum.CONNECTING);
                const ws = this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${listenKey}`, wsKey, forceNewConnection);
                // Start & store timer to keep alive listen key (and handle expiration)
                this.setKeepAliveListenKeyTimer(listenKey, market, ws, wsKey, symbol);
                return ws;
            }
            catch (e) {
                this.logger.error('Failed to connect to isolated margin user data', Object.assign(Object.assign({}, loggerCategory), { error: e, symbol }));
                this.emit('error', {
                    wsKey: 'isolatedMargin' + '_' + 'userData',
                    error: e,
                });
            }
        });
    }
    /**
     * --------------------------
     * End of SPOT market websocket streams
     * --------------------------
     **/
    /**
     * Subscribe to USD-M Futures user data stream - listen key is automatically generated.
     */
    subscribeUsdFuturesUserDataStream(isTestnet, forceNewConnection, isReconnecting) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const restClient = this.getUSDMRestClient(isTestnet);
                const { listenKey } = yield restClient.getFuturesUserDataListenKey();
                const market = isTestnet ? 'usdmTestnet' : 'usdm';
                const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, 'userData', undefined, listenKey);
                if (!forceNewConnection && this.wsStore.isWsConnecting(wsKey)) {
                    this.logger.silly('Existing usd futures user data connection in progress for listen key. Avoiding duplicate');
                    return this.getWs(wsKey);
                }
                // Necessary so client knows this is a reconnect
                this.setWsState(wsKey, isReconnecting
                    ? WsStore_1.WsConnectionStateEnum.RECONNECTING
                    : WsStore_1.WsConnectionStateEnum.CONNECTING);
                const ws = this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${listenKey}`, wsKey, forceNewConnection);
                // Start & store timer to keep alive listen key (and handle expiration)
                this.setKeepAliveListenKeyTimer(listenKey, market, ws, wsKey, undefined, isTestnet);
                return ws;
            }
            catch (e) {
                this.logger.error('Failed to connect to USD Futures user data', Object.assign(Object.assign({}, loggerCategory), { error: e }));
                this.emit('error', { wsKey: 'usdm' + '_' + 'userData', error: e });
            }
        });
    }
    /**
     * Subscribe to COIN-M Futures user data stream - listen key is automatically generated.
     */
    subscribeCoinFuturesUserDataStream(isTestnet, forceNewConnection, isReconnecting) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const { listenKey } = yield this.getCOINMRestClient(isTestnet).getFuturesUserDataListenKey();
                const market = isTestnet ? 'coinmTestnet' : 'coinm';
                const wsKey = (0, requestUtils_1.getWsKeyWithContext)(market, 'userData', undefined, listenKey);
                if (!forceNewConnection && this.wsStore.isWsConnecting(wsKey)) {
                    this.logger.silly('Existing usd futures user data connection in progress for listen key. Avoiding duplicate');
                    return this.getWs(wsKey);
                }
                // Necessary so client knows this is a reconnect
                this.setWsState(wsKey, isReconnecting
                    ? WsStore_1.WsConnectionStateEnum.RECONNECTING
                    : WsStore_1.WsConnectionStateEnum.CONNECTING);
                const ws = this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${listenKey}`, wsKey, forceNewConnection);
                // Start & store timer to keep alive listen key (and handle expiration)
                this.setKeepAliveListenKeyTimer(listenKey, market, ws, wsKey, undefined, isTestnet);
                return ws;
            }
            catch (e) {
                this.logger.error('Failed to connect to COIN Futures user data', Object.assign(Object.assign({}, loggerCategory), { error: e }));
                this.emit('error', { wsKey: 'coinm' + '_' + 'userData', error: e });
            }
        });
    }
}
exports.WebsocketClient = WebsocketClient;
//# sourceMappingURL=websocket-client.js.map