v0.1.4
What's changed
Performance work on the websocket receive path, plus two behaviours restored from 0.1.3.
Faster
Measured on CI, base vs head, three paired rounds on the same runner — 9 faster, 0 regressed, 37 unchanged:
l2book_dispatch_50_coins−40.0%eip712_agent_digest_wasm−26.7%,eip712_multisig_digest_wasm−24.5%ws_request_round_trip−23.3%subscribe_200_coins−18.8%prepare_request−17.6%reconnect_resubscribe_burst−15.3%l1_action_hash_order_100_wasm−10.8%
What changed under those numbers:
- The socket no longer re-boxes every inbound frame.
ReconnectingWebSocketwas allocating aMessageEventand paying a fullEventTarget.dispatchEventper frame purely to hand it to the SDK's own consumer, which then ran its own dispatch. That consumer now takes a direct frame hook; the publicmessageevent is still dispatched whenever anything outside the package listens. 1.42×, 305 ns/frame. fastAssetCtxsdecodes natively. Where the runtime exposesnode:zlib, a per-frameDecompressionStream+ writer + reader + four promises collapses into oneinflateRawSync, delivered in the dispatch tick rather than through a promise queue. 7.6× on delta frames, 1.8× on full snapshots.DecompressionStreamremains the browser / React Native fallback, and both paths are differentially tested to agree — including multi-chunk payloads and every malformed input.- Cheaper listener dispatch. A lone listener is stored unboxed and promoted to a copy-on-write array on the second registration, so multi-listener dispatch no longer copies into a snapshot buffer and clears it. 2.6× at 8 listeners, 1.18× at 1.
- One less serialization per subscribe. The dispatcher reuses the subscription id it already built by concatenation as the wire frame.
Large frames move least: JSON.parse dominates them and the SDK cannot touch that.
Fixed
fastAssetCtxscould deliver the wrong frame. The payload was read from the recycled event shell inside a queued continuation, so once frames arrived faster than decompression completed, the continuation read a later frame's data — delta updates silently dropped and others delivered twice, corrupting downstream price state. The payload is now captured synchronously.- A listener could receive a frame after unsubscribing. Dispatch skipped the liveness check for a listener unsubscribed by an earlier listener mid-dispatch. 0.1.3 matched
EventTargethere; the behaviour is restored. createL1ActionHashvalidatesvaultAddressagain. It is publicly exported and was taking the address with no runtime check — a0x${string}type constrains neither charset nor length at runtime, and nothing at all for JavaScript callers. A malformed address silently hashed to different bytes than the caller described, and a 32-byte address hashed identically to its 20-byte truncation. Both now throw, as they did at 0.1.3.
Behavioural change
HyperliquidEventTarget is no longer a subclass of EventTarget. Dispatch is now a hand-rolled listener map, which is where most of the per-frame win above comes from. addEventListener / removeEventListener keep their semantics — including duplicate-registration dedup, once, AbortSignal, and not delivering to a listener removed mid-dispatch — but instanceof EventTarget is now false and dispatchEvent is not available. Reaching for either was never part of the documented surface; if you depended on it, that access needs replacing. Flagged here because it landed in a patch release.
Also
New perf scenarios cover the fastAssetCtxs decode path and the user-account channels, neither of which was measured before.