UNPKG

@nktkas/hyperliquid

Version:

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

153 lines 7.08 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebSocketSubscriptionManager = void 0; const _postRequest_js_1 = require("./_postRequest.js"); /** Maximum number of subscriptions allowed by Hyperliquid. */ const MAX_SUBSCRIPTIONS = 1000; /** Maximum number of subscriptions with user parameter allowed by Hyperliquid. */ const MAX_USER_SUBSCRIPTIONS = 10; /** * Manages WebSocket subscriptions to Hyperliquid event channels. * Handles subscription lifecycle, resubscription on reconnect, and cleanup. */ class WebSocketSubscriptionManager { /** Enable automatic re-subscription to Hyperliquid subscription after reconnection. */ resubscribe; _socket; _wsRequester; _hlEvents; _subscriptions = new Map(); constructor(socket, wsRequester, hlEvents, resubscribe) { this._socket = socket; this._wsRequester = wsRequester; this._hlEvents = hlEvents; this.resubscribe = resubscribe; // Subscribe to socket events socket.addEventListener("open", () => this._handleOpen()); socket.addEventListener("close", () => this._handleClose()); socket.addEventListener("error", () => this._handleClose()); } // ============================================================ // Public methods // ============================================================ /** * Subscribes to a Hyperliquid event channel. * Sends a subscription request to the server and listens for events. * * @param channel - The event channel to listen to. * @param payload - A payload to send with the subscription request. * @param listener - A function to call when the event is dispatched. * * @returns A promise that resolves with a {@link WebSocketSubscription} object to manage the subscription lifecycle. * * @throws {WebSocketRequestError} - An error that occurs when a WebSocket request fails. */ async subscribe(channel, payload, listener) { // Create a unique identifier for the subscription const id = _postRequest_js_1.WebSocketAsyncRequest.requestToId(payload); // Initialize new subscription, if it doesn't exist let subscription = this._subscriptions.get(id); if (!subscription) { // Check total subscription limits if (this._subscriptions.size >= MAX_SUBSCRIPTIONS) { throw new _postRequest_js_1.WebSocketRequestError("Cannot subscribe to more than 1000 channels."); } // Check user subscription limits if (this._hasUserParam(payload) && this._countUserSubscriptions() >= MAX_USER_SUBSCRIPTIONS) { throw new _postRequest_js_1.WebSocketRequestError("Cannot track more than 10 total users."); } // Send subscription request const promise = this._wsRequester.request("subscribe", payload) .finally(() => subscription.promiseFinished = true); // Cache subscription info subscription = { listeners: new Map(), promise, promiseFinished: false, failureController: new AbortController(), }; this._subscriptions.set(id, subscription); } // Initialize new listener, if it doesn't exist let unsubscribe = subscription.listeners.get(listener); if (!unsubscribe) { // Create new unsubscribe function unsubscribe = async () => { // Remove listener and cleanup this._hlEvents.removeEventListener(channel, listener); const subscription = this._subscriptions.get(id); subscription?.listeners.delete(listener); // If no listeners remain, remove subscription entirely if (subscription?.listeners.size === 0) { // Cleanup subscription this._subscriptions.delete(id); // If the socket is open, send unsubscription request if (this._socket.readyState === 1) { // OPEN await this._wsRequester.request("unsubscribe", payload); } } }; // Add listener and cache unsubscribe function this._hlEvents.addEventListener(channel, listener); subscription.listeners.set(listener, unsubscribe); } // Wait for the initial subscription request to complete await subscription.promise; // Return subscription control object return { unsubscribe, failureSignal: subscription.failureController.signal, }; } // ============================================================ // Socket event handlers // ============================================================ /** Resubscribe to all existing subscriptions if auto-resubscribe is enabled. */ _handleOpen() { if (this.resubscribe) { for (const [id, subscription] of this._subscriptions.entries()) { // reconnect only previously connected subscriptions to avoid double subscriptions due to message buffering if (subscription.promiseFinished) { subscription.promise = this._wsRequester.request("subscribe", JSON.parse(id)) .catch((error) => subscription.failureController.abort(error)) .finally(() => subscription.promiseFinished = true); subscription.promiseFinished = false; } } } } /** Cleanup subscriptions if resubscribe is disabled or socket is terminated. */ _handleClose() { if (!this.resubscribe || this._socket.terminationSignal.aborted) { for (const subscriptionInfo of this._subscriptions.values()) { for (const [_, unsubscribe] of subscriptionInfo.listeners) { unsubscribe(); // does not cause an error if used when the connection is closed } } } } // ============================================================ // Subscription limit checks // ============================================================ /** Checks if a payload contains a user parameter. */ _hasUserParam(payload) { return typeof payload === "object" && payload !== null && "user" in payload; } /** Counts the number of active subscriptions with user parameter. */ _countUserSubscriptions() { let count = 0; for (const id of this._subscriptions.keys()) { try { const payload = JSON.parse(id); if (this._hasUserParam(payload)) count++; } catch { // Ignore parsing errors } } return count; } } exports.WebSocketSubscriptionManager = WebSocketSubscriptionManager; //# sourceMappingURL=_subscriptionManager.js.map