UNPKG

pulsar_sdk_js

Version:

A convenient way for you to interact with Pulsar's Third Party APIs, using typified classes.

299 lines (298 loc) 12.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const errors_1 = require("../types/errors"); const time_1 = require("../utils/time"); const class_transformer_1 = require("class-transformer"); const encrypt_1 = require("./encryption/encrypt"); const isomorphic_ws_1 = __importDefault(require("isomorphic-ws")); const schema_1 = require("../data/dataclasses/schema"); const constants_1 = require("../data/dataclasses/constants"); const NO_MORE_MESSAGES_IN_WEBSOCKET = 'stream_end'; const WEBSOCKET_ABORTED_MESSAGE = 'websocket_aborted'; const WEBSOCKET_ABORTED_ERROR_MESSAGE = 'WebSocket connection was closed and all ongoing messages were aborted.'; class WebsocketClient { constructor(api_key, base_url, use_ssl) { this.callbackHandlers = {}; this.messagePromises = {}; this.websocketIsOpening = false; this.websocketIsClosing = false; if (use_ssl) { this.base_url = `wss://${base_url}/v1/thirdparty/ws`; } else { this.base_url = `ws://${base_url}/v1/thirdparty/ws`; } this.api_key = api_key; this.websocket_conn = null; } async transitionToOpen() { if (this.websocketIsOpening) { await this.waitForWebSocketOpeningCompletion(); return; } this.websocketIsOpening = true; try { if (!this.websocket_conn) { await this.createNewWebSocketConnection(); return; } switch (this.websocket_conn.readyState) { case isomorphic_ws_1.default.CONNECTING: await this.waitForState(isomorphic_ws_1.default.OPEN); case isomorphic_ws_1.default.OPEN: break; case isomorphic_ws_1.default.CLOSING: await this.waitForState(isomorphic_ws_1.default.CLOSED); case isomorphic_ws_1.default.CLOSED: await this.createNewWebSocketConnection(); break; } } finally { this.websocketIsOpening = false; } } async transitionToClosed() { var _a; switch ((_a = this.websocket_conn) === null || _a === void 0 ? void 0 : _a.readyState) { case isomorphic_ws_1.default.OPEN: this.websocket_conn.close(1000, 'Normal closure'); await this.waitForState(isomorphic_ws_1.default.CLOSED); break; case isomorphic_ws_1.default.CONNECTING: await this.waitForState(isomorphic_ws_1.default.OPEN); this.websocket_conn.close(1000, 'Normal closure'); await this.waitForState(isomorphic_ws_1.default.CLOSED); break; case isomorphic_ws_1.default.CLOSING: await this.waitForState(isomorphic_ws_1.default.CLOSED); break; case isomorphic_ws_1.default.CLOSED: break; } } async createNewWebSocketConnection(attempt = 1) { if (attempt > 3) { throw new errors_1.APIError('Failed to connect to WebSocket after multiple attempts', 500); } const dictToSend = JSON.stringify({ api_key: this.api_key, timestamp: Date.now() / 1000 }); const url_encoded_api_key = await (0, encrypt_1.encryptMessage)(dictToSend); const url_with_api_key = `${this.base_url}?access_token=${encodeURIComponent(url_encoded_api_key)}`; this.websocket_conn = new isomorphic_ws_1.default(url_with_api_key); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.websocket_conn.close(); if (attempt < 3) { resolve(this.createNewWebSocketConnection(attempt + 1)); } else { reject(new errors_1.APIError('WebSocket connection timeout', 408)); } }, 10000); this.websocket_conn.onopen = () => { clearTimeout(timeout); resolve(this.websocket_conn); this.websocket_conn.onmessage = (event) => { const messageData = JSON.parse(event.data); const requestID = messageData.event.request_id; if (this.callbackHandlers[requestID]) { this.callbackHandlers[requestID](event); } }; this.websocket_conn.onclose = (event) => { this.handleWebSocketClosure(event); }; this.websocket_conn.onerror = (event) => { this.handleWebSocketClosure(event); }; }; this.websocket_conn.onerror = (event) => { clearTimeout(timeout); if (attempt < 3) { resolve(this.createNewWebSocketConnection(attempt + 1)); } else if (event.message) { reject(new errors_1.APIError(event.message, 500)); } else { reject(new errors_1.APIError('Failed to connect to WebSocket', 500)); } }; }); } handleWebSocketClosure(event) { for (const requestID in this.messagePromises) { for (const messagePromise of this.messagePromises[requestID]) { if ('reason' in event) { if (event.code === 1000) { messagePromise.resolve(WEBSOCKET_ABORTED_MESSAGE); } else { messagePromise.reject(new errors_1.APIError(`WebSocket closed: ${event.reason}`, event.code)); } } else if ('message' in event) { messagePromise.reject(new errors_1.APIError(`WebSocket error: ${event.message}`, 500)); } else { messagePromise.reject(new errors_1.APIError(`WebSocket closed unexpectedly.`, 500)); } } } } async closeWebsocket() { this.websocketIsClosing = true; if (this.websocketIsClosing && Object.keys(this.messagePromises).length === 0 && Object.keys(this.callbackHandlers).length === 0) { this.transitionToClosed(); } this.websocketIsClosing = false; } async sendMessage(msg) { await this.transitionToOpen(); if (!this.websocket_conn) { throw new errors_1.APIError(`Failed to connect to WebSocket.`, 500); } this.websocket_conn.send(JSON.stringify(msg)); } async *handleResponse(requestID, msg, finishedEventType) { if (this.websocketIsClosing) { await this.waitForWebSocketClosureCompletion(); } this.setupCallbackHandler(requestID, finishedEventType); try { for await (const message of this.sendMessageAndProcessMessages(requestID, msg)) { yield message; } } finally { this.performFinalCleanup(requestID); } } setupCallbackHandler(requestID, finishedEventType) { const messagePromise = this.createMessagePromise(); this.messagePromises[requestID] = [messagePromise]; this.callbackHandlers[requestID] = async (response) => { const eventDict = await this.getEventDict(response.data); const currentMessagePromise = this.messagePromises[requestID][this.messagePromises[requestID].length - 1]; if (eventDict === null || eventDict === void 0 ? void 0 : eventDict.error_message) { currentMessagePromise.reject(new errors_1.APIError(eventDict.error_message)); return; } if (eventDict === null || eventDict === void 0 ? void 0 : eventDict.is_error) { currentMessagePromise.reject(new errors_1.APIError('An error occurred while processing the request')); return; } try { const eventType = eventDict.key; const eventPayload = eventDict.payload; if (eventType === finishedEventType) { currentMessagePromise.resolve(NO_MORE_MESSAGES_IN_WEBSOCKET); } if (eventType.includes('PREFETCH') || eventType.includes('FINISHED')) { return; } if (eventPayload == null) { return; } this.messagePromises[requestID].push(this.createMessagePromise()); const item = await this.processPayload(eventPayload.type, eventPayload.data); currentMessagePromise.resolve(item); } catch (error) { currentMessagePromise.reject(new errors_1.APIError(`An error occurred while parsing a response. ${error}`)); } }; } async *sendMessageAndProcessMessages(requestID, msg) { await this.sendMessage(msg); while (true) { const message = await this.waitForMessageOrTimeout(requestID); if (message === NO_MORE_MESSAGES_IN_WEBSOCKET) { break; } if (message === WEBSOCKET_ABORTED_MESSAGE) { throw new errors_1.APIError(WEBSOCKET_ABORTED_ERROR_MESSAGE, 503); } yield message; } } async waitForMessageOrTimeout(requestID) { const messagePromise = this.messagePromises[requestID][0]; let timeoutId; const timeout = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new errors_1.APIError(`WebSocket response timeout.`, 408)); }, constants_1.defaultTimeoutInMs); }); try { const result = await Promise.race([messagePromise.promise, timeout]); clearTimeout(timeoutId); this.messagePromises[requestID].shift(); return result; } catch (error) { clearTimeout(timeoutId); throw error; } } performFinalCleanup(requestID) { this.cleanupRequest(requestID); if (Object.keys(this.messagePromises).length === 0 && Object.keys(this.callbackHandlers).length === 0) { setTimeout(() => this.closeWebsocket(), 1000); } } async processPayload(payload_type, payload_data) { if (payload_type.startsWith('Timeseries')) { return (0, class_transformer_1.plainToInstance)(schema_1.Timeseries, payload_data[0]); } else if (payload_type.startsWith('AggregateWalletIntegrations')) { return (0, class_transformer_1.plainToInstance)(schema_1.WalletIntegrations, payload_data); } else if (payload_type.startsWith('NFTItem')) { return (0, class_transformer_1.plainToInstance)(schema_1.WalletNFTs, payload_data); } else if (payload_type.startsWith('AggregateWalletTokens')) { return (0, class_transformer_1.plainToInstance)(schema_1.WalletTokens, payload_data); } else { throw new errors_1.APIError(`Unknown payload type: ${payload_type}`); } } async getEventDict(response) { const json_response = JSON.parse(response); const { event } = json_response; return event; } createMessagePromise() { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise: promise, resolve: resolve, reject: reject, }; } cleanupRequest(requestID) { delete this.callbackHandlers[requestID]; delete this.messagePromises[requestID]; } async waitForState(desiredState) { await (0, time_1.waitForCondition)(() => { var _a; return ((_a = this.websocket_conn) === null || _a === void 0 ? void 0 : _a.readyState) === desiredState; }, 10000); } async waitForWebSocketClosureCompletion() { await (0, time_1.waitForCondition)(() => this.websocketIsClosing == false, 10000); } async waitForWebSocketOpeningCompletion() { await (0, time_1.waitForCondition)(() => this.websocketIsOpening == false, 10000); } } exports.default = WebsocketClient;