UNPKG

kucoin-universal-sdk

Version:
332 lines 13.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebSocketClient = void 0; const events_1 = require("events"); const path_1 = __importDefault(require("path")); const common_1 = require("../../model/common"); const constant_1 = require("../../model/constant"); const websocket_option_1 = require("../../model/websocket_option"); const fs_1 = __importDefault(require("fs")); const worker_threads_1 = require("worker_threads"); const common_2 = require("../../common"); const message_data_1 = require("./message_data"); const util_1 = require("../util/util"); const stream_1 = require("stream"); var ConnectionState; (function (ConnectionState) { ConnectionState[ConnectionState["DISCONNECTED"] = 0] = "DISCONNECTED"; ConnectionState[ConnectionState["CONNECTING"] = 1] = "CONNECTING"; ConnectionState[ConnectionState["CONNECTED"] = 2] = "CONNECTED"; })(ConnectionState || (ConnectionState = {})); // WebSocketClient class, used to manage WebSocket connection and its related operations class WebSocketClient extends events_1.EventEmitter { constructor(tokenProvider, options) { super(); this.reconnecting = false; this.worker = null; this.options = options; this.state = ConnectionState.DISCONNECTED; this.tokenProvider = tokenProvider; this.tokenInfo = null; this.shutdown = false; this.ackEvents = new Map(); this.messageBuffer = new stream_1.Readable({ objectMode: true, highWaterMark: this.options.readMessageBuffer || websocket_option_1.DEFAULT_WEBSOCKET_CLIENT_OPTION.readMessageBuffer, read(size) { }, }); this.messageBuffer.on('data', (data) => { this.emit('message', data); }); } start() { if (this.state != ConnectionState.DISCONNECTED) { common_2.logger.warn('WebSocket client is already connected.'); return Promise.resolve(); } this.state = ConnectionState.CONNECTING; return this.dial() .then(() => { this.state = ConnectionState.CONNECTED; this.keepAliveInterval = setInterval(() => this.keepAlive(), this.tokenInfo.pingInterval); this.emit('event', websocket_option_1.WebSocketEvent.EventConnected, ''); }) .catch((err) => { this.state = ConnectionState.DISCONNECTED; common_2.logger.error('Failed to start webSocket client:', err); throw err; }); } stop() { this.shutdown = true; common_2.logger.info('shutting down websocket client...'); return this.close().finally(() => { this.emit('event', websocket_option_1.WebSocketEvent.EventClientShutdown, ''); }); } write(ms, timeout) { if (this.state != ConnectionState.CONNECTED || this.shutdown) { return Promise.reject(new Error('Not connected or shutting down')); } return (0, util_1.withTimeout)((resolve, reject) => { try { this.ackEvents.set(ms.id, { msg: ms, resolve: resolve, reject: reject, }); // @ts-ignore this.worker.postMessage({ type: message_data_1.EventType.MESSAGE, data: ms, }); } catch (error) { common_2.logger.error('Failed to send message:', error); this.ackEvents.delete(ms.id); reject(error); } }, timeout).catch((err) => { if (err instanceof util_1.TimeoutError) { common_2.logger.error('Send message timeout, id:', ms.id); this.ackEvents.delete(ms.id); throw err; } }); } on(event, listener) { return super.on(event, listener); } emit(event, ...args) { return super.emit(event, ...args); } close() { common_2.logger.info('closing websocket client...'); // clear intervals if (this.keepAliveInterval) { clearInterval(this.keepAliveInterval); this.keepAliveInterval = null; } // clear acks this.ackEvents.forEach((writeMsg) => { writeMsg.reject(new Error('WebSocket connection closed')); }); this.ackEvents.clear(); // set stats this.state = ConnectionState.DISCONNECTED; this.emit('event', websocket_option_1.WebSocketEvent.EventDisconnected, ''); // delete worker if (this.worker) { this.worker.postMessage({ type: message_data_1.EventType.CLOSED }); let worker = this.worker; this.worker = null; return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }).then(() => { return worker.terminate().then(); }); } return Promise.resolve(); } // dial connects to the WebSocket server dial() { return this.tokenProvider.getToken().then((tokenInfos) => { this.tokenInfo = this.randomEndpoint(tokenInfos); // create WebSocket connection parameters const queryParams = new URLSearchParams({ connectId: Date.now().toString(), token: this.tokenInfo.token, }); // create WebSocket URL const wsUrl = `${this.tokenInfo.endpoint}?${queryParams.toString()}`; // Get the worker file path relative to the compiled js file const workerPath = path_1.default.join(__dirname, 'message_worker.js'); if (!fs_1.default.existsSync(workerPath)) { throw new Error(`Worker file not found at path: ${workerPath}. Please ensure the project is built.`); } // Create a new worker thread this.worker = new worker_threads_1.Worker(workerPath); return (0, util_1.withTimeout)((resolve, reject) => { if (!this.worker) { reject(new Error('Failed to create worker')); return; } this.worker.once('message', (message) => { if (message.type === message_data_1.EventType.MESSAGE) { try { let m = common_1.WsMessage.fromJson(message.data); if (m.type == constant_1.MessageType.WelcomeMessage) { common_2.logger.info(`receive welcome message, ready to process message`); // Handle all worker messages through the message event this.worker.addListener('message', (message) => { switch (message.type) { case message_data_1.EventType.MESSAGE: case message_data_1.EventType.ERROR: this.onMessage(message); break; case message_data_1.EventType.CLOSED: this.onClose(message.data.code, message.data.reason); break; } }); resolve(); return; } } catch (e) { reject(e); return; } } reject(new Error(`Failed to init worker connection, msg:${message.error}`)); }); // Init underlying connection this.worker.postMessage({ type: message_data_1.EventType.INIT, data: wsUrl, }); }, this.options.dialTimeout).catch((err) => { common_2.logger.error(`failed to create worker`, err); return this.close().then(() => { throw err; }); }); }); } // receive message callback onMessage(message) { var _a; if (this.state != ConnectionState.CONNECTED) { common_2.logger.warn('Ignoring message as client is disconnected', message); return; } // error message if (message.type == message_data_1.EventType.ERROR) { common_2.logger.warn(`Got error message, error=${message.error}`); if ((_a = message.data) === null || _a === void 0 ? void 0 : _a.id) { this.handleAckEvent(message.data.id, message.data.error); } return; } let m; try { m = JSON.parse(message.data); } catch (e) { common_2.logger.error('Failed to parse message:', e); return; } switch (m.type) { case constant_1.MessageType.Message: if (!this.messageBuffer.push(m)) { this.emit('event', websocket_option_1.WebSocketEvent.EventReadBufferFull, ''); } break; case constant_1.MessageType.PongMessage: { this.emit('event', websocket_option_1.WebSocketEvent.EventPongReceived, ''); this.handleAckEvent(m.id, null); break; } case constant_1.MessageType.AckMessage: { this.handleAckEvent(m.id, null); break; } case constant_1.MessageType.ErrorMessage: { const errorMsg = String(m.data); this.emit('event', websocket_option_1.WebSocketEvent.EventErrorReceived, String(errorMsg)); this.handleAckEvent(m.id, new Error(errorMsg)); break; } default: common_2.logger.warn('Unknown message type:', m.type); } } handleAckEvent(id, err) { const data = this.ackEvents.get(id); if (!data) { common_2.logger.warn('Unknown ack event id: ', id); return; } this.ackEvents.delete(id); if (err) { data.reject(err); } else { data.resolve(); } } // close callback onClose(code, reason) { if (!this.shutdown) { common_2.logger.warn(`WebSocket closed with code ${code}: ${reason}`); this.reconnect(); } } keepAlive() { const pingMsg = new common_1.WsMessage(); pingMsg.id = Date.now().toString(); pingMsg.type = constant_1.MessageType.PingMessage; this.write(pingMsg, this.options.writeTimeout) .catch((e) => { common_2.logger.error('keepalive ping error:', e); }) .then(() => { common_2.logger.debug('send ping success'); }); } randomEndpoint(tokens) { if (!tokens.length) { throw new Error('Tokens list is empty'); } return tokens[Math.floor(Math.random() * tokens.length)]; } reconnect() { if (this.reconnecting) { return Promise.resolve(); } return Promise.resolve() .then(() => { this.reconnecting = true; }) .then(() => { return this.close(); }) .then(() => { if (!this.shutdown && this.options.reconnect) { this.emit('event', websocket_option_1.WebSocketEvent.EventTryReconnect, ''); const maxAttempts = this.options.reconnectAttempts == -1 ? Number.MAX_VALUE : this.options.reconnectAttempts; return Promise.resolve().then(async () => { for (let i = 0; i < maxAttempts; i++) { common_2.logger.warn(`reconnecting... ${i}/${maxAttempts}`); await new Promise((resolve) => { setTimeout(resolve, this.options.reconnectInterval); }); try { await this.start(); common_2.logger.info('Successfully reconnected to WebSocket server'); this.emit('reconnected'); return; } catch (e) { common_2.logger.error(`reconnecting fail:`, e); } } this.emit('event', websocket_option_1.WebSocketEvent.EventClientFail, 'Failed to reconnect after all attempts'); common_2.logger.error('Failed to reconnect after all attempts.'); }); } }) .finally(async () => { this.reconnecting = false; }); } } exports.WebSocketClient = WebSocketClient; //# sourceMappingURL=default_ws_client.js.map