@nktkas/hyperliquid
Version:
Hyperliquid API SDK for all major JS runtimes, written in TypeScript.
153 lines • 7.08 kB
JavaScript
"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