@nktkas/hyperliquid
Version:
Unofficial Hyperliquid API SDK for all major JS runtimes, written in TypeScript.
172 lines (171 loc) • 7.67 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebSocketAsyncRequest = void 0;
const websocket_transport_js_1 = require("./websocket_transport.js");
/**
* Manages WebSocket requests to the Hyperliquid API.
* Handles request creation, sending, and mapping responses to their corresponding requests.
*/
class WebSocketAsyncRequest {
socket;
lastId = 0;
queue = [];
lastRequestTime = 0;
/**
* Creates a new WebSocket async request handler.
* @param socket - WebSocket connection instance for sending requests to the Hyperliquid WebSocket API
* @param hlEvents - Used to recognize Hyperliquid responses and match them with sent requests
*/
constructor(socket, hlEvents) {
this.socket = socket;
// Monitor responses and match the pending request
hlEvents.addEventListener("subscriptionResponse", (event) => {
const detail = event.detail;
// Use a stringified request as an id
const id = WebSocketAsyncRequest.requestToId(detail);
this.queue.findLast((item) => item.id === id)?.resolve(detail);
});
hlEvents.addEventListener("post", (event) => {
const detail = event.detail;
const data = detail.response.type === "info" ? detail.response.payload.data : detail.response.payload;
this.queue.findLast((item) => item.id === detail.id)?.resolve(data);
});
hlEvents.addEventListener("pong", () => {
this.queue.findLast((item) => item.id === "ping")?.resolve();
});
hlEvents.addEventListener("error", (event) => {
const detail = event.detail;
try {
// Error event doesn't have an id, use original request to match
const request = detail.match(/{.*}/)?.[0];
if (!request)
return;
const parsedRequest = JSON.parse(request);
// For `post` requests
if ("id" in parsedRequest && typeof parsedRequest.id === "number") {
this.queue.findLast((item) => item.id === parsedRequest.id)
?.reject(new websocket_transport_js_1.WebSocketRequestError(`Server error: ${detail}`, { cause: detail }));
return;
}
// For `subscribe` and `unsubscribe` requests
if ("subscription" in parsedRequest &&
typeof parsedRequest.subscription === "object" && parsedRequest.subscription !== null) {
const id = WebSocketAsyncRequest.requestToId(parsedRequest);
this.queue.findLast((item) => item.id === id)
?.reject(new websocket_transport_js_1.WebSocketRequestError(`Server error: ${detail}`, { cause: detail }));
return;
}
// For `Already subscribed` and `Invalid subscription` requests
if (detail.startsWith("Already subscribed") || detail.startsWith("Invalid subscription")) {
const id = WebSocketAsyncRequest.requestToId({
method: "subscribe",
subscription: parsedRequest,
});
this.queue.findLast((item) => item.id === id)
?.reject(new websocket_transport_js_1.WebSocketRequestError(`Server error: ${detail}`, { cause: detail }));
return;
}
// For `Already unsubscribed` requests
if (detail.startsWith("Already unsubscribed")) {
const id = WebSocketAsyncRequest.requestToId({
method: "unsubscribe",
subscription: parsedRequest,
});
this.queue.findLast((item) => item.id === id)
?.reject(new websocket_transport_js_1.WebSocketRequestError(`Server error: ${detail}`, { cause: detail }));
return;
}
// For unknown requests
const id = WebSocketAsyncRequest.requestToId(parsedRequest);
this.queue.findLast((item) => item.id === id)
?.reject(new websocket_transport_js_1.WebSocketRequestError(`Server error: ${detail}`, { cause: detail }));
}
catch {
// Ignore JSON parsing errors
}
});
// Throws all pending requests if the connection is dropped
socket.addEventListener("close", () => {
this.queue.forEach(({ reject }) => {
reject(new websocket_transport_js_1.WebSocketRequestError("WebSocket connection closed."));
});
this.queue = [];
});
}
async request(method, payload_or_signal, maybeSignal) {
const payload = payload_or_signal instanceof AbortSignal ? undefined : payload_or_signal;
const signal = payload_or_signal instanceof AbortSignal ? payload_or_signal : maybeSignal;
// Reject the request if the signal is aborted
if (signal?.aborted)
return Promise.reject(signal.reason);
// Create a request
let id;
let request;
if (method === "post") {
id = ++this.lastId;
request = { method, id, request: payload };
}
else if (method === "ping") {
id = "ping";
request = { method };
}
else {
request = { method, subscription: payload };
id = WebSocketAsyncRequest.requestToId(request);
}
// Send the request
this.socket.send(JSON.stringify(request), signal);
this.lastRequestTime = Date.now();
// Wait for a response
const { promise, resolve, reject } = Promise.withResolvers();
this.queue.push({ id, resolve, reject });
const onAbort = () => reject(signal?.reason);
signal?.addEventListener("abort", onAbort, { once: true });
return await promise.finally(() => {
const index = this.queue.findLastIndex((item) => item.id === id);
if (index !== -1)
this.queue.splice(index, 1);
signal?.removeEventListener("abort", onAbort);
});
}
/** Normalizes an object and then converts it to a string. */
static requestToId(value) {
const lowerHex = deepLowerHex(value);
const sorted = deepSortKeys(lowerHex);
return JSON.stringify(sorted); // Also removes undefined
}
}
exports.WebSocketAsyncRequest = WebSocketAsyncRequest;
/** Deeply converts hexadecimal strings in an object/array to lowercase. */
function deepLowerHex(obj) {
if (typeof obj === "string") {
return /^(0X[0-9a-fA-F]*|0x[0-9a-fA-F]*[A-F][0-9a-fA-F]*)$/.test(obj) ? obj.toLowerCase() : obj;
}
if (Array.isArray(obj)) {
return obj.map((value) => deepLowerHex(value));
}
if (typeof obj === "object" && obj !== null) {
const result = {};
const entries = Object.entries(obj);
for (const [key, value] of entries) {
result[key] = deepLowerHex(value);
}
return result;
}
return obj;
}
/** Deeply sort the keys of an object. */
function deepSortKeys(obj) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(deepSortKeys);
}
const result = {};
const keys = Object.keys(obj).sort();
for (const key of keys) {
result[key] = deepSortKeys(obj[key]);
}
return result;
}