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
JavaScript
"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;