@lifi/perps-sdk-provider-hyperliquid 1.5.4 → 1.5.6

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 (55) hide show
  1. package/dist/cjs/services/getFills.d.ts.map +1 -1
  2. package/dist/cjs/services/getFills.js +13 -6
  3. package/dist/cjs/services/getFills.js.map +1 -1
  4. package/dist/cjs/services/getOrders.js +1 -1
  5. package/dist/cjs/services/getOrders.js.map +1 -1
  6. package/dist/cjs/types/order.d.ts +1 -1
  7. package/dist/cjs/types/order.d.ts.map +1 -1
  8. package/dist/cjs/utils/decodeFastAssetCtxs.d.ts.map +1 -1
  9. package/dist/cjs/utils/decodeFastAssetCtxs.js +11 -6
  10. package/dist/cjs/utils/decodeFastAssetCtxs.js.map +1 -1
  11. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +7 -4
  12. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  13. package/dist/cjs/websocket/HyperliquidWsProvider.js +77 -62
  14. package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
  15. package/dist/cjs/websocket/decodeChain.d.ts +11 -0
  16. package/dist/cjs/websocket/decodeChain.d.ts.map +1 -0
  17. package/dist/cjs/websocket/decodeChain.js +37 -0
  18. package/dist/cjs/websocket/decodeChain.js.map +1 -0
  19. package/dist/esm/services/getFills.d.ts +4 -4
  20. package/dist/esm/services/getFills.d.ts.map +1 -1
  21. package/dist/esm/services/getFills.js +19 -9
  22. package/dist/esm/services/getFills.js.map +1 -1
  23. package/dist/esm/services/getOrders.js +1 -1
  24. package/dist/esm/services/getOrders.js.map +1 -1
  25. package/dist/esm/types/order.d.ts +1 -1
  26. package/dist/esm/types/order.d.ts.map +1 -1
  27. package/dist/esm/utils/decodeFastAssetCtxs.d.ts +1 -0
  28. package/dist/esm/utils/decodeFastAssetCtxs.d.ts.map +1 -1
  29. package/dist/esm/utils/decodeFastAssetCtxs.js +14 -6
  30. package/dist/esm/utils/decodeFastAssetCtxs.js.map +1 -1
  31. package/dist/esm/websocket/HyperliquidWsProvider.d.ts +20 -7
  32. package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  33. package/dist/esm/websocket/HyperliquidWsProvider.js +100 -71
  34. package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
  35. package/dist/esm/websocket/decodeChain.d.ts +30 -0
  36. package/dist/esm/websocket/decodeChain.d.ts.map +1 -0
  37. package/dist/esm/websocket/decodeChain.js +44 -0
  38. package/dist/esm/websocket/decodeChain.js.map +1 -0
  39. package/dist/types/services/getFills.d.ts +4 -4
  40. package/dist/types/services/getFills.d.ts.map +1 -1
  41. package/dist/types/types/order.d.ts +1 -1
  42. package/dist/types/types/order.d.ts.map +1 -1
  43. package/dist/types/utils/decodeFastAssetCtxs.d.ts +1 -0
  44. package/dist/types/utils/decodeFastAssetCtxs.d.ts.map +1 -1
  45. package/dist/types/websocket/HyperliquidWsProvider.d.ts +20 -7
  46. package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  47. package/dist/types/websocket/decodeChain.d.ts +30 -0
  48. package/dist/types/websocket/decodeChain.d.ts.map +1 -0
  49. package/package.json +3 -3
  50. package/src/services/getFills.ts +37 -11
  51. package/src/services/getOrders.ts +1 -1
  52. package/src/types/order.ts +1 -1
  53. package/src/utils/decodeFastAssetCtxs.ts +14 -6
  54. package/src/websocket/HyperliquidWsProvider.ts +136 -93
  55. 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
@@ -99,6 +100,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
99
100
  private readonly registry: MarketRegistry | undefined
100
101
  private perpCtxBySubDex = new Map<string, Record<string, HlWsPerpAssetCtx>>()
101
102
  private spotCtxByMarketId: Record<string, HlWsSpotAssetCtx> = {}
103
+ private marketsContextByMarketId: Record<string, MarketContext> = {}
102
104
  private orderbookKeysByMarketId = new Map<string, Set<string>>()
103
105
  private latestOrderbookByMarketId = new Map<string, OrderbookResponse>()
104
106
  // Latest mid/mark per `Market.id` from the high-frequency `fastAssetCtxs`
@@ -106,12 +108,29 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
106
108
  // frames carry only changed coins — and overlaid onto asset contexts that
107
109
  // carry oracle/funding/OI/metadata.
108
110
  private fastCtxByMarketId: Record<string, HlWsFastAssetCtx> = {}
109
- // `fastAssetCtxs` payloads are base64 + raw-DEFLATE; decode is async, so
110
- // chain decodes to apply incremental frames in arrival order.
111
- private fastDecodeChain: Promise<void> = Promise.resolve()
112
- private pacDecodeChain: Promise<void> = Promise.resolve()
113
- private sacDecodeChain: Promise<void> = Promise.resolve()
114
- 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
+ )
115
134
 
116
135
  constructor(wsUrl: string, providerKey: string, client?: PerpsSDKClient) {
117
136
  super(
@@ -173,6 +192,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
173
192
  this.perpCtxBySubDex.clear()
174
193
  this.spotCtxByMarketId = {}
175
194
  this.fastCtxByMarketId = {}
195
+ this.marketsContextByMarketId = {}
176
196
  }
177
197
  }
178
198
 
@@ -292,12 +312,13 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
292
312
  this.perpCtxBySubDex.clear()
293
313
  this.spotCtxByMarketId = {}
294
314
  this.fastCtxByMarketId = {}
315
+ this.marketsContextByMarketId = {}
295
316
  this.orderbookKeysByMarketId.clear()
296
317
  this.latestOrderbookByMarketId.clear()
297
- this.fastDecodeChain = Promise.resolve()
298
- this.pacDecodeChain = Promise.resolve()
299
- this.sacDecodeChain = Promise.resolve()
300
- this.orderbookDecodeChain = Promise.resolve()
318
+ this.fastDecodeChain.reset()
319
+ this.pacDecodeChain.reset()
320
+ this.sacDecodeChain.reset()
321
+ this.orderbookDecodeChain.reset()
301
322
  this.orderUpdatesKey = undefined
302
323
  }
303
324
 
@@ -483,61 +504,65 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
483
504
  }
484
505
 
485
506
  private handleAllDexsAssetCtxs(data: HlWsAllDexsAssetCtxsData) {
486
- this.mergePerpAssetCtxEntries(allDexsAssetCtxEntries(data))
487
- this.emitMarketsContext()
507
+ this.emitMarketsContext(
508
+ this.mergePerpAssetCtxEntries(allDexsAssetCtxEntries(data))
509
+ )
488
510
  }
489
511
 
490
512
  private handlePac(base64: string) {
491
- this.pacDecodeChain = this.pacDecodeChain
492
- .then(async () => {
513
+ this.pacDecodeChain.push(async () => {
514
+ this.emitMarketsContext(
493
515
  this.mergePerpAssetCtxEntries(
494
516
  await decodeCompressedJson<HlWsPacData>(base64)
495
517
  )
496
- this.emitMarketsContext()
497
- })
498
- .catch((error) => wsLog.handlerFailure(this.providerKey, error))
518
+ )
519
+ })
499
520
  }
500
521
 
522
+ /** Merge perp asset contexts in place; returns the marketIds this frame touched. */
501
523
  private mergePerpAssetCtxEntries(
502
524
  entries: [string, HlWsPerpAssetCtxPayload[]][]
503
- ) {
525
+ ): Set<string> {
526
+ const touched = new Set<string>()
504
527
  for (const [dex, ctxs] of entries) {
505
- const previous = this.perpCtxBySubDex.get(dex || 'default')
506
- const byMarketId: Record<string, HlWsPerpAssetCtx> = {
507
- ...(previous ?? {}),
528
+ let byMarketId = this.perpCtxBySubDex.get(dex || 'default')
529
+ if (byMarketId === undefined) {
530
+ byMarketId = {}
531
+ this.perpCtxBySubDex.set(dex || 'default', byMarketId)
508
532
  }
509
533
  for (const ctx of ctxs) {
510
534
  const marketId = ctx.coin
511
535
  if (marketId === undefined) {
512
536
  continue
513
537
  }
514
- const merged = mergePerpAssetCtx(previous?.[marketId], marketId, ctx)
538
+ const merged = mergePerpAssetCtx(byMarketId[marketId], marketId, ctx)
515
539
  if (merged !== undefined) {
516
540
  byMarketId[marketId] = merged
541
+ touched.add(marketId)
517
542
  }
518
543
  }
519
- this.perpCtxBySubDex.set(dex || 'default', byMarketId)
520
544
  }
545
+ return touched
521
546
  }
522
547
 
523
548
  private handleSac(base64: string) {
524
- this.sacDecodeChain = this.sacDecodeChain
525
- .then(async () => {
526
- const spotMarketIdBySacKey = this.spotMarketIdBySacKey()
527
- const ctxs = await decodeCompressedJson<HlWsSacData>(base64)
528
- for (const [sacKey, ctx] of Object.entries(ctxs)) {
529
- const marketId = spotMarketIdBySacKey.get(sacKey)
530
- if (marketId === undefined) {
531
- continue
532
- }
533
- this.spotCtxByMarketId[marketId] = {
534
- ...this.spotCtxByMarketId[marketId],
535
- ...ctx,
536
- }
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
537
557
  }
538
- this.emitMarketsContext()
539
- })
540
- .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
+ })
541
566
  }
542
567
 
543
568
  private spotMarketIdBySacKey(): Map<string, string> {
@@ -559,65 +584,78 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
559
584
  * Decode a `fastAssetCtxs` frame (base64 + raw-DEFLATE) and merge its changed
560
585
  * coins into the per-market fast-context store. Frames are incremental, so a
561
586
  * field absent from this frame keeps its prior value. Decodes are chained to
562
- * 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).
563
589
  */
564
590
  private handleFastAssetCtxs(base64: string) {
565
- this.fastDecodeChain = this.fastDecodeChain
566
- .then(async () => {
567
- const ctxs = await decodeFastAssetCtxs(base64)
568
- for (const [marketId, ctx] of Object.entries(ctxs)) {
569
- const prev = this.fastCtxByMarketId[marketId]
570
- this.fastCtxByMarketId[marketId] = {
571
- markPx: 'markPx' in ctx ? ctx.markPx : prev?.markPx,
572
- midPx: 'midPx' in ctx ? ctx.midPx : prev?.midPx,
573
- }
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,
574
598
  }
575
- this.emitMarketsContext()
576
- })
577
- .catch((error) => wsLog.handlerFailure(this.providerKey, error))
599
+ }
600
+ this.emitMarketsContext(Object.keys(ctxs))
601
+ })
578
602
  }
579
603
 
580
604
  /**
581
- * Build the all-markets context map from perp + spot asset contexts, with mid
582
- * + mark overlaid from `fastAssetCtxs`.
605
+ * Emit the all-markets context map, re-mapping only the markets the incoming
606
+ * frame touched. Each emission is a fresh snapshot object that carries
607
+ * untouched entries over by reference, so a listener holding an earlier
608
+ * snapshot never observes later-frame mutation.
583
609
  */
584
- private emitMarketsContext() {
585
- const data: Record<string, MarketContext> = {}
586
- for (const byMarketId of this.perpCtxBySubDex.values()) {
587
- for (const [marketId, ctx] of Object.entries(byMarketId)) {
588
- const base = mapMarketContext(marketId, ctx)
589
- const fast = this.fastCtxByMarketId[marketId]
590
- data[marketId] = {
591
- ...base,
592
- midPrice: fast?.midPx != null ? fast.midPx : base.midPrice,
593
- markPrice: fast?.markPx != null ? fast.markPx : base.markPrice,
594
- }
595
- }
610
+ private emitMarketsContext(touchedMarketIds: Iterable<string>) {
611
+ const data: Record<string, MarketContext> = {
612
+ ...this.marketsContextByMarketId,
596
613
  }
597
- for (const [marketId, ctx] of Object.entries(this.spotCtxByMarketId)) {
598
- const context = mapSpotMarketContext(
599
- marketId,
600
- ctx,
601
- this.fastCtxByMarketId[marketId]
602
- )
614
+ for (const marketId of touchedMarketIds) {
615
+ const context = this.computeMarketContext(marketId)
603
616
  if (context === undefined) {
604
- continue
617
+ delete data[marketId]
618
+ } else {
619
+ data[marketId] = context
605
620
  }
606
- data[marketId] = context
607
621
  }
608
- for (const [marketId, fast] of Object.entries(this.fastCtxByMarketId)) {
609
- if (data[marketId] !== undefined) {
610
- continue
622
+ this.marketsContextByMarketId = data
623
+ this.emit('marketsContext', { channel: 'marketsContext', data })
624
+ }
625
+
626
+ /**
627
+ * Context for one market from the cached feeds: spot asset context when it
628
+ * maps, else perp asset context with mid + mark overlaid from
629
+ * `fastAssetCtxs`, else the fast feed alone — each price standing in for the
630
+ * other where it carries only one of mid/mark.
631
+ */
632
+ private computeMarketContext(marketId: string): MarketContext | undefined {
633
+ const fast = this.fastCtxByMarketId[marketId]
634
+ const spotCtx = this.spotCtxByMarketId[marketId]
635
+ if (spotCtx !== undefined) {
636
+ const context = mapSpotMarketContext(marketId, spotCtx, fast)
637
+ if (context !== undefined) {
638
+ return context
611
639
  }
612
- // No asset-context frame for this market yet — each price stands in for
613
- // the other where the fast feed carries only one of mid/mark.
614
- const midPrice = fast.midPx ?? fast.markPx
615
- const markPrice = fast.markPx ?? fast.midPx
616
- if (midPrice != null && markPrice != null) {
617
- data[marketId] = { marketId, midPrice, markPrice }
640
+ }
641
+ let perpCtx: HlWsPerpAssetCtx | undefined
642
+ for (const byMarketId of this.perpCtxBySubDex.values()) {
643
+ perpCtx = byMarketId[marketId] ?? perpCtx
644
+ }
645
+ if (perpCtx !== undefined) {
646
+ const base = mapMarketContext(marketId, perpCtx)
647
+ return {
648
+ ...base,
649
+ midPrice: fast?.midPx != null ? fast.midPx : base.midPrice,
650
+ markPrice: fast?.markPx != null ? fast.markPx : base.markPrice,
618
651
  }
619
652
  }
620
- this.emit('marketsContext', { channel: 'marketsContext', data })
653
+ const midPrice = fast?.midPx ?? fast?.markPx
654
+ const markPrice = fast?.markPx ?? fast?.midPx
655
+ if (midPrice != null && markPrice != null) {
656
+ return { marketId, midPrice, markPrice }
657
+ }
658
+ return undefined
621
659
  }
622
660
 
623
661
  private handleActiveAssetCtx(data: HlWsActiveAssetCtxData, raw: string) {
@@ -710,13 +748,11 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
710
748
  return
711
749
  }
712
750
  const compressed = data.c
713
- this.orderbookDecodeChain = this.orderbookDecodeChain
714
- .then(async () => {
715
- this.handleCompactL2Delta(
716
- await decodeCompressedJson<HlWsCompressedL2Data>(compressed)
717
- )
718
- })
719
- .catch((error) => wsLog.handlerFailure(this.providerKey, error))
751
+ this.orderbookDecodeChain.push(async () => {
752
+ this.handleCompactL2Delta(
753
+ await decodeCompressedJson<HlWsCompressedL2Data>(compressed)
754
+ )
755
+ })
720
756
  }
721
757
 
722
758
  private handleCompactL2Delta(delta: HlWsCompressedL2Data) {
@@ -725,6 +761,14 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
725
761
  return
726
762
  }
727
763
 
764
+ // Compact deltas decode asynchronously via `orderbookDecodeChain`, so one
765
+ // can resolve after a newer frame (sync snapshot/update, or a later delta)
766
+ // has already been applied. A delta no newer than the maintained book is
767
+ // stale; applying it would overwrite fresher state with older levels.
768
+ if (delta.t <= previous.timestamp) {
769
+ return
770
+ }
771
+
728
772
  const book: OrderbookResponse = {
729
773
  provider: this.providerKey,
730
774
  marketId: delta.c,
@@ -806,8 +850,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
806
850
  continue
807
851
  }
808
852
  const type = mapOrderType(o.orderType)
809
- // A miss warns once and schedules a cooldown-gated registry refetch
810
- // (the id may have listed after the snapshot); skip just this item.
853
+ // Unknown market id (absent from the synced snapshot); skip just this item.
811
854
  const market = this.registry?.get(o.coin)
812
855
  if (!market) {
813
856
  continue
@@ -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
+ }