Skip to content

v0.2.1

Choose a tag to compare

@joeblau joeblau released this 04 Aug 05:40
· 2 commits to main since this release
2121e18

Four fixes. Every one was implemented, then independently cross-reviewed by three agents — Claude (Opus/Fable), Kimi K3 Max, and Codex gpt-5.6-sol at maximum reasoning effort — and iterated until all three approved the final diff. Two review-round findings hardened the fixes beyond their original scope.

Fixed

A dying feed is now loud, for every subscriber (#89, #93). When a confirmed subscription fails — the server refuses a re-subscribe after a reconnect, the connection is permanently terminated, or it closes with resubscribe: false — every subscriber's onError now fires (previously: exactly one, and only if that caller passed onError; everyone else's feed died silently while unsubscribe() still "worked"). New: every subscription handle from WebSocketTransport carries a failureSignal: AbortSignal that aborts with the failure as its reason — and never on a voluntary unsubscribe() — so a dead feed is observable without registering any callback:

const sub = await client.allMids((data) => render(data.mids));
sub.failureSignal?.addEventListener("abort", () => resubscribe(sub.failureSignal?.reason));

Notification runs off a snapshot taken before any user code executes, so an onError that unsubscribes a sibling cannot rob it of its notification (a cross-review finding).

WebSocket messages are paced by default (#90, #96). The shared default quota now runs a token bucket sized to Hyperliquid's documented budget (2000 messages/minute per IP, burst 2000). The default transport was exactly the one that tripped the limit: at the 1000-subscription cap one reconnect re-sent every subscribe frame instantly — half the minute's budget in a burst a flapping socket repeated until the server refused. Pacing only ever delays subscribe/unsubscribe frames; post requests (orders) and keep-alive pings never wait. The uncontended path stays synchronous via a new tryAcquire fast path, requests flushed after a reconnect debit the budget exactly once, aborted requests no longer spend tokens, and a terminated transport abandons its paced waits instead of blocking other transports sharing the quota. Opt out with an accounting-only quota:

import { WebSocketQuota, WebSocketTransport } from "@bloxwap/hyperliquid";

const transport = new WebSocketTransport({ quota: new WebSocketQuota() });

200-OK error envelopes throw instead of masquerading as data (#91, #94). Hyperliquid reports some failures inside a 200 OK as a top-level { "type": "error", "message": "..." } envelope. HttpTransport returned those as data — unvalidated info methods yielded the envelope as a "result", schema-validated ones failed with a confusing ValidationError. They now throw HttpRequestError carrying the server's own message. Arrays, nested type fields, and the exchange endpoint's { status: "err" } envelope are unaffected.

Wallet detection no longer inspects signTypedData arity (#92, #95). Function.length counts only parameters declared before the first default/rest parameter, so wrapped or adapted wallets (a Privy-style adapter declaring signTypedData(...args)) reported length 0, failed both shape guards, and died with an opaque unknown wallet type error at first signing. Detection is now by member presence alone. Positional ethers-style signTypedData(domain, types, value) is rejected up front with an explanatory error, and a wallet matching neither shape gets a diagnostic enumerating every missing member of both shapes — with what was found instead — so one iteration fixes the adapter.

Upgrading from 0.2.0

No API breaks. One behavioral default changed: outbound WebSocket subscribe/unsubscribe frames now pace against the server's own 2000/minute budget instead of silently overrunning it (orders are never delayed). If you deliberately want unpaced sends, pass new WebSocketQuota() as shown above.