@lifi/perps-sdk-provider-hyperliquid 1.5.5 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/cjs/accountConfig.d.ts.map +1 -1
  2. package/dist/cjs/accountConfig.js +1 -0
  3. package/dist/cjs/accountConfig.js.map +1 -1
  4. package/dist/cjs/utils/decodeFastAssetCtxs.d.ts.map +1 -1
  5. package/dist/cjs/utils/decodeFastAssetCtxs.js +11 -6
  6. package/dist/cjs/utils/decodeFastAssetCtxs.js.map +1 -1
  7. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +5 -4
  8. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  9. package/dist/cjs/websocket/HyperliquidWsProvider.js +18 -24
  10. package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
  11. package/dist/cjs/websocket/decodeChain.d.ts +11 -0
  12. package/dist/cjs/websocket/decodeChain.d.ts.map +1 -0
  13. package/dist/cjs/websocket/decodeChain.js +37 -0
  14. package/dist/cjs/websocket/decodeChain.js.map +1 -0
  15. package/dist/esm/accountConfig.d.ts.map +1 -1
  16. package/dist/esm/accountConfig.js +1 -0
  17. package/dist/esm/accountConfig.js.map +1 -1
  18. package/dist/esm/utils/decodeFastAssetCtxs.d.ts +1 -0
  19. package/dist/esm/utils/decodeFastAssetCtxs.d.ts.map +1 -1
  20. package/dist/esm/utils/decodeFastAssetCtxs.js +14 -6
  21. package/dist/esm/utils/decodeFastAssetCtxs.js.map +1 -1
  22. package/dist/esm/websocket/HyperliquidWsProvider.d.ts +7 -5
  23. package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  24. package/dist/esm/websocket/HyperliquidWsProvider.js +25 -27
  25. package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
  26. package/dist/esm/websocket/decodeChain.d.ts +30 -0
  27. package/dist/esm/websocket/decodeChain.d.ts.map +1 -0
  28. package/dist/esm/websocket/decodeChain.js +44 -0
  29. package/dist/esm/websocket/decodeChain.js.map +1 -0
  30. package/dist/types/accountConfig.d.ts.map +1 -1
  31. package/dist/types/utils/decodeFastAssetCtxs.d.ts +1 -0
  32. package/dist/types/utils/decodeFastAssetCtxs.d.ts.map +1 -1
  33. package/dist/types/websocket/HyperliquidWsProvider.d.ts +7 -5
  34. package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  35. package/dist/types/websocket/decodeChain.d.ts +30 -0
  36. package/dist/types/websocket/decodeChain.d.ts.map +1 -0
  37. package/package.json +2 -2
  38. package/src/accountConfig.ts +1 -0
  39. package/src/utils/decodeFastAssetCtxs.ts +14 -6
  40. package/src/websocket/HyperliquidWsProvider.ts +67 -56
  41. package/src/websocket/decodeChain.ts +54 -0
@@ -61,6 +61,7 @@ import {
61
61
  spotBalance,
62
62
  spotPriceById,
63
63
  } from '../utils/index.js'
64
+ import { DecodeChain } from './decodeChain.js'
64
65
 
65
66
  /** HL's compact `l2` snapshot carries 20 levels per side. */
66
67
  const HL_L2_BOOK_MAX_LEVELS_PER_SIDE = 20
@@ -107,12 +108,29 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
107
108
  // frames carry only changed coins — and overlaid onto asset contexts that
108
109
  // carry oracle/funding/OI/metadata.
109
110
  private fastCtxByMarketId: Record<string, HlWsFastAssetCtx> = {}
110
- // `fastAssetCtxs` payloads are base64 + raw-DEFLATE; decode is async, so
111
- // chain decodes to apply incremental frames in arrival order.
112
- private fastDecodeChain: Promise<void> = Promise.resolve()
113
- private pacDecodeChain: Promise<void> = Promise.resolve()
114
- private sacDecodeChain: Promise<void> = Promise.resolve()
115
- private orderbookDecodeChain: Promise<void> = Promise.resolve()
111
+ private readonly reportDecodeFailure = (error: unknown) =>
112
+ wsLog.handlerFailure(this.providerKey, error)
113
+ // Compressed payloads (base64 + raw-DEFLATE) decode async, so each channel
114
+ // chains decodes to apply frames in arrival order. Only the fast feed
115
+ // coalesces a stall backlog to its newest frame: a coin's dropped tick is
116
+ // refreshed by its next one. pac/sac field-merges and l2 deltas depend on
117
+ // every frame, so they must not coalesce.
118
+ private readonly fastDecodeChain = new DecodeChain(
119
+ 'latest',
120
+ this.reportDecodeFailure
121
+ )
122
+ private readonly pacDecodeChain = new DecodeChain(
123
+ 'every',
124
+ this.reportDecodeFailure
125
+ )
126
+ private readonly sacDecodeChain = new DecodeChain(
127
+ 'every',
128
+ this.reportDecodeFailure
129
+ )
130
+ private readonly orderbookDecodeChain = new DecodeChain(
131
+ 'every',
132
+ this.reportDecodeFailure
133
+ )
116
134
 
117
135
  constructor(wsUrl: string, providerKey: string, client?: PerpsSDKClient) {
118
136
  super(
@@ -297,10 +315,10 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
297
315
  this.marketsContextByMarketId = {}
298
316
  this.orderbookKeysByMarketId.clear()
299
317
  this.latestOrderbookByMarketId.clear()
300
- this.fastDecodeChain = Promise.resolve()
301
- this.pacDecodeChain = Promise.resolve()
302
- this.sacDecodeChain = Promise.resolve()
303
- this.orderbookDecodeChain = Promise.resolve()
318
+ this.fastDecodeChain.reset()
319
+ this.pacDecodeChain.reset()
320
+ this.sacDecodeChain.reset()
321
+ this.orderbookDecodeChain.reset()
304
322
  this.orderUpdatesKey = undefined
305
323
  }
306
324
 
@@ -492,15 +510,13 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
492
510
  }
493
511
 
494
512
  private handlePac(base64: string) {
495
- this.pacDecodeChain = this.pacDecodeChain
496
- .then(async () => {
497
- this.emitMarketsContext(
498
- this.mergePerpAssetCtxEntries(
499
- await decodeCompressedJson<HlWsPacData>(base64)
500
- )
513
+ this.pacDecodeChain.push(async () => {
514
+ this.emitMarketsContext(
515
+ this.mergePerpAssetCtxEntries(
516
+ await decodeCompressedJson<HlWsPacData>(base64)
501
517
  )
502
- })
503
- .catch((error) => wsLog.handlerFailure(this.providerKey, error))
518
+ )
519
+ })
504
520
  }
505
521
 
506
522
  /** Merge perp asset contexts in place; returns the marketIds this frame touched. */
@@ -530,25 +546,23 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
530
546
  }
531
547
 
532
548
  private handleSac(base64: string) {
533
- this.sacDecodeChain = this.sacDecodeChain
534
- .then(async () => {
535
- const spotMarketIdBySacKey = this.spotMarketIdBySacKey()
536
- const ctxs = await decodeCompressedJson<HlWsSacData>(base64)
537
- const touched = new Set<string>()
538
- for (const [sacKey, ctx] of Object.entries(ctxs)) {
539
- const marketId = spotMarketIdBySacKey.get(sacKey)
540
- if (marketId === undefined) {
541
- continue
542
- }
543
- this.spotCtxByMarketId[marketId] = {
544
- ...this.spotCtxByMarketId[marketId],
545
- ...ctx,
546
- }
547
- touched.add(marketId)
549
+ this.sacDecodeChain.push(async () => {
550
+ const spotMarketIdBySacKey = this.spotMarketIdBySacKey()
551
+ const ctxs = await decodeCompressedJson<HlWsSacData>(base64)
552
+ const touched = new Set<string>()
553
+ for (const [sacKey, ctx] of Object.entries(ctxs)) {
554
+ const marketId = spotMarketIdBySacKey.get(sacKey)
555
+ if (marketId === undefined) {
556
+ continue
548
557
  }
549
- this.emitMarketsContext(touched)
550
- })
551
- .catch((error) => wsLog.handlerFailure(this.providerKey, error))
558
+ this.spotCtxByMarketId[marketId] = {
559
+ ...this.spotCtxByMarketId[marketId],
560
+ ...ctx,
561
+ }
562
+ touched.add(marketId)
563
+ }
564
+ this.emitMarketsContext(touched)
565
+ })
552
566
  }
553
567
 
554
568
  private spotMarketIdBySacKey(): Map<string, string> {
@@ -570,22 +584,21 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
570
584
  * Decode a `fastAssetCtxs` frame (base64 + raw-DEFLATE) and merge its changed
571
585
  * coins into the per-market fast-context store. Frames are incremental, so a
572
586
  * field absent from this frame keeps its prior value. Decodes are chained to
573
- * preserve arrival order across the async boundary.
587
+ * preserve arrival order across the async boundary; a backlog coalesces to
588
+ * its newest frame (see the chain's mode).
574
589
  */
575
590
  private handleFastAssetCtxs(base64: string) {
576
- this.fastDecodeChain = this.fastDecodeChain
577
- .then(async () => {
578
- const ctxs = await decodeFastAssetCtxs(base64)
579
- for (const [marketId, ctx] of Object.entries(ctxs)) {
580
- const prev = this.fastCtxByMarketId[marketId]
581
- this.fastCtxByMarketId[marketId] = {
582
- markPx: 'markPx' in ctx ? ctx.markPx : prev?.markPx,
583
- midPx: 'midPx' in ctx ? ctx.midPx : prev?.midPx,
584
- }
591
+ this.fastDecodeChain.push(async () => {
592
+ const ctxs = await decodeFastAssetCtxs(base64)
593
+ for (const [marketId, ctx] of Object.entries(ctxs)) {
594
+ const prev = this.fastCtxByMarketId[marketId]
595
+ this.fastCtxByMarketId[marketId] = {
596
+ markPx: 'markPx' in ctx ? ctx.markPx : prev?.markPx,
597
+ midPx: 'midPx' in ctx ? ctx.midPx : prev?.midPx,
585
598
  }
586
- this.emitMarketsContext(Object.keys(ctxs))
587
- })
588
- .catch((error) => wsLog.handlerFailure(this.providerKey, error))
599
+ }
600
+ this.emitMarketsContext(Object.keys(ctxs))
601
+ })
589
602
  }
590
603
 
591
604
  /**
@@ -735,13 +748,11 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
735
748
  return
736
749
  }
737
750
  const compressed = data.c
738
- this.orderbookDecodeChain = this.orderbookDecodeChain
739
- .then(async () => {
740
- this.handleCompactL2Delta(
741
- await decodeCompressedJson<HlWsCompressedL2Data>(compressed)
742
- )
743
- })
744
- .catch((error) => wsLog.handlerFailure(this.providerKey, error))
751
+ this.orderbookDecodeChain.push(async () => {
752
+ this.handleCompactL2Delta(
753
+ await decodeCompressedJson<HlWsCompressedL2Data>(compressed)
754
+ )
755
+ })
745
756
  }
746
757
 
747
758
  private handleCompactL2Delta(delta: HlWsCompressedL2Data) {
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Frame semantics of a {@link DecodeChain}:
3
+ * - `'every'`: every frame decodes and applies in arrival order — required for
4
+ * delta-carrying channels, where dropping a frame corrupts downstream state.
5
+ * - `'latest'`: frames queued behind an in-flight decode coalesce to the
6
+ * newest — for snapshot-like channels, where a newer frame supersedes
7
+ * anything still waiting.
8
+ */
9
+ export type DecodeChainMode = 'every' | 'latest'
10
+
11
+ /**
12
+ * Serializes async WS frame decodes so results apply in arrival order.
13
+ * In `'latest'` mode a backlog (e.g. queued behind a main-thread stall)
14
+ * collapses to its newest frame instead of replaying every stale decode.
15
+ * Decode failures are reported to `onError` and never break the chain.
16
+ */
17
+ export class DecodeChain {
18
+ private tail: Promise<void> = Promise.resolve()
19
+ private queued: (() => Promise<void>) | undefined
20
+
21
+ constructor(
22
+ private readonly mode: DecodeChainMode,
23
+ private readonly onError: (error: unknown) => void
24
+ ) {}
25
+
26
+ push(decode: () => Promise<void>): void {
27
+ if (this.mode === 'every') {
28
+ this.tail = this.tail.then(decode).catch(this.onError)
29
+ return
30
+ }
31
+ const drainScheduled = this.queued !== undefined
32
+ this.queued = decode
33
+ if (drainScheduled) {
34
+ return
35
+ }
36
+ this.tail = this.tail
37
+ .then(() => {
38
+ const next = this.queued
39
+ this.queued = undefined
40
+ return next?.()
41
+ })
42
+ .catch(this.onError)
43
+ }
44
+
45
+ /**
46
+ * Detach on connection teardown: drops the queued latest-mode decode and
47
+ * starts subsequent pushes on a fresh chain. A decode already in flight
48
+ * still settles.
49
+ */
50
+ reset(): void {
51
+ this.tail = Promise.resolve()
52
+ this.queued = undefined
53
+ }
54
+ }