UNPKG

@nktkas/hyperliquid

Version:

Unofficial Hyperliquid API SDK for all major JS runtimes, written in TypeScript.

172 lines (171 loc) 7.67 kB
"use strict"; 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; }