kucoin-universal-sdk
Version:
Official KuCoin Universal SDK.
332 lines • 13.5 kB
JavaScript
"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