@pear-protocol/symmio-client 0.3.12 → 0.3.13

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.
@@ -72,6 +72,9 @@ function toBinanceSymbol(symmSymbol) {
72
72
  // src/utils/binance-ws.ts
73
73
  var BINANCE_WS_URL = "wss://fstream.binance.com/market/ws";
74
74
  var RECONNECT_DELAYS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
75
+ var IDLE_CLOSE_DELAY_MS = 3e4;
76
+ var STALE_CONNECTION_MS = 3e4;
77
+ var STALE_CHECK_INTERVAL_MS = 1e4;
75
78
  var STABLE_QUOTES = ["USDT0", "USDT", "USDC", "USDE", "USDH", "USD"];
76
79
  function normalizeBaseSymbol(symbol) {
77
80
  const normalized = symbol.toUpperCase().trim();
@@ -121,8 +124,11 @@ var BinanceWsManager = class {
121
124
  streams = /* @__PURE__ */ new Map();
122
125
  reconnectAttempt = 0;
123
126
  reconnectTimer = null;
127
+ idleCloseTimer = null;
128
+ staleCheckTimer = null;
124
129
  intentionalClose = false;
125
- pendingSubscribes = [];
130
+ pendingSubscribes = /* @__PURE__ */ new Set();
131
+ lastMessageAt = 0;
126
132
  idCounter = 0;
127
133
  /**
128
134
  * Subscribe to a kline stream. Returns an unsubscribe function.
@@ -195,10 +201,10 @@ var BinanceWsManager = class {
195
201
  */
196
202
  destroy() {
197
203
  this.intentionalClose = true;
198
- if (this.reconnectTimer) {
199
- clearTimeout(this.reconnectTimer);
200
- this.reconnectTimer = null;
201
- }
204
+ this.clearReconnectTimer();
205
+ this.clearIdleCloseTimer();
206
+ this.clearStaleCheckTimer();
207
+ this.pendingSubscribes.clear();
202
208
  if (this.ws) {
203
209
  this.ws.close();
204
210
  this.ws = null;
@@ -218,6 +224,7 @@ var BinanceWsManager = class {
218
224
  }
219
225
  sub.callbacks.set(id, cb);
220
226
  if (isNew) {
227
+ this.clearIdleCloseTimer();
221
228
  this.ensureConnected();
222
229
  this.sendSubscribe([streamName]);
223
230
  }
@@ -229,18 +236,14 @@ var BinanceWsManager = class {
229
236
  if (sub.callbacks.size === 0) {
230
237
  this.streams.delete(streamName);
231
238
  this.sendUnsubscribe([streamName]);
232
- if (this.streams.size === 0 && this.ws) {
233
- this.intentionalClose = true;
234
- this.ws.close();
235
- this.ws = null;
236
- this.intentionalClose = false;
237
- }
239
+ this.scheduleIdleClose();
238
240
  }
239
241
  }
240
242
  ensureConnected() {
241
243
  if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
242
244
  return;
243
245
  }
246
+ this.clearReconnectTimer();
244
247
  this.connect();
245
248
  }
246
249
  connect() {
@@ -249,23 +252,27 @@ var BinanceWsManager = class {
249
252
  this.ws = new WebSocket(BINANCE_WS_URL);
250
253
  this.ws.onopen = () => {
251
254
  this.reconnectAttempt = 0;
252
- const activeStreams = Array.from(this.streams.keys());
255
+ this.lastMessageAt = Date.now();
256
+ this.startStaleCheck();
257
+ const activeStreams = Array.from(
258
+ /* @__PURE__ */ new Set([...this.streams.keys(), ...this.pendingSubscribes])
259
+ );
260
+ this.pendingSubscribes.clear();
253
261
  if (activeStreams.length > 0) {
254
262
  this.sendSubscribe(activeStreams);
255
263
  }
256
- if (this.pendingSubscribes.length > 0) {
257
- this.sendSubscribe(this.pendingSubscribes);
258
- this.pendingSubscribes = [];
259
- }
260
264
  };
261
265
  this.ws.onmessage = (event) => {
262
266
  try {
263
267
  const data = JSON.parse(event.data);
268
+ this.lastMessageAt = Date.now();
264
269
  this.handleMessage(data);
265
270
  } catch {
266
271
  }
267
272
  };
268
273
  this.ws.onclose = () => {
274
+ this.ws = null;
275
+ this.clearStaleCheckTimer();
269
276
  if (this.intentionalClose) return;
270
277
  this.scheduleReconnect();
271
278
  };
@@ -299,25 +306,27 @@ var BinanceWsManager = class {
299
306
  }
300
307
  sendSubscribe(streams) {
301
308
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
302
- this.pendingSubscribes.push(...streams);
309
+ streams.forEach((stream) => this.pendingSubscribes.add(stream));
303
310
  return;
304
311
  }
312
+ const params = Array.from(new Set(streams));
313
+ if (params.length === 0) return;
305
314
  this.ws.send(JSON.stringify({
306
315
  method: "SUBSCRIBE",
307
- params: streams,
316
+ params,
308
317
  id: Date.now()
309
318
  }));
310
319
  }
311
320
  sendUnsubscribe(streams) {
312
321
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
313
- this.pendingSubscribes = this.pendingSubscribes.filter(
314
- (s) => !streams.includes(s)
315
- );
322
+ streams.forEach((stream) => this.pendingSubscribes.delete(stream));
316
323
  return;
317
324
  }
325
+ const params = Array.from(new Set(streams));
326
+ if (params.length === 0) return;
318
327
  this.ws.send(JSON.stringify({
319
328
  method: "UNSUBSCRIBE",
320
- params: streams,
329
+ params,
321
330
  id: Date.now()
322
331
  }));
323
332
  }
@@ -331,6 +340,44 @@ var BinanceWsManager = class {
331
340
  this.connect();
332
341
  }, delay);
333
342
  }
343
+ scheduleIdleClose() {
344
+ if (this.streams.size > 0 || this.idleCloseTimer) return;
345
+ this.clearReconnectTimer();
346
+ if (!this.ws) return;
347
+ this.idleCloseTimer = setTimeout(() => {
348
+ this.idleCloseTimer = null;
349
+ if (this.streams.size > 0 || !this.ws) return;
350
+ this.intentionalClose = true;
351
+ this.ws.close();
352
+ this.ws = null;
353
+ this.intentionalClose = false;
354
+ this.clearStaleCheckTimer();
355
+ }, IDLE_CLOSE_DELAY_MS);
356
+ }
357
+ startStaleCheck() {
358
+ this.clearStaleCheckTimer();
359
+ this.staleCheckTimer = setInterval(() => {
360
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
361
+ if (this.streams.size === 0) return;
362
+ if (Date.now() - this.lastMessageAt < STALE_CONNECTION_MS) return;
363
+ this.ws.close();
364
+ }, STALE_CHECK_INTERVAL_MS);
365
+ }
366
+ clearReconnectTimer() {
367
+ if (!this.reconnectTimer) return;
368
+ clearTimeout(this.reconnectTimer);
369
+ this.reconnectTimer = null;
370
+ }
371
+ clearIdleCloseTimer() {
372
+ if (!this.idleCloseTimer) return;
373
+ clearTimeout(this.idleCloseTimer);
374
+ this.idleCloseTimer = null;
375
+ }
376
+ clearStaleCheckTimer() {
377
+ if (!this.staleCheckTimer) return;
378
+ clearInterval(this.staleCheckTimer);
379
+ this.staleCheckTimer = null;
380
+ }
334
381
  };
335
382
  var _instance = null;
336
383
  function getBinanceWsManager() {
@@ -361,6 +408,28 @@ function getNextRefCount(binanceSymbol) {
361
408
  function getPrevRefCount(binanceSymbol) {
362
409
  return Math.max(0, (refCounts.get(binanceSymbol) ?? 0) - 1);
363
410
  }
411
+ function addMappedSymbol(binanceSymbol, symmSymbol) {
412
+ const symbols = streamSymbols.get(binanceSymbol) ?? /* @__PURE__ */ new Map();
413
+ symbols.set(symmSymbol, (symbols.get(symmSymbol) ?? 0) + 1);
414
+ streamSymbols.set(binanceSymbol, symbols);
415
+ }
416
+ function removeMappedSymbol(binanceSymbol, symmSymbol) {
417
+ const symbols = streamSymbols.get(binanceSymbol);
418
+ if (!symbols) return false;
419
+ const currentCount = symbols.get(symmSymbol) ?? 0;
420
+ if (currentCount <= 0) return false;
421
+ if (currentCount === 1) {
422
+ symbols.delete(symmSymbol);
423
+ } else {
424
+ symbols.set(symmSymbol, currentCount - 1);
425
+ }
426
+ if (symbols.size === 0) {
427
+ streamSymbols.delete(binanceSymbol);
428
+ } else {
429
+ streamSymbols.set(binanceSymbol, symbols);
430
+ }
431
+ return true;
432
+ }
364
433
  var useBinanceMarkPriceStore = zustand.create((set) => ({
365
434
  markPrices: {},
366
435
  fundingRates: {},
@@ -369,9 +438,7 @@ var useBinanceMarkPriceStore = zustand.create((set) => ({
369
438
  const binanceSymbol = normalizeBinanceSymbol(rawBinanceSymbol);
370
439
  const nextRefCount = getNextRefCount(binanceSymbol);
371
440
  refCounts.set(binanceSymbol, nextRefCount);
372
- const symbols = streamSymbols.get(binanceSymbol) ?? /* @__PURE__ */ new Set();
373
- symbols.add(symmSymbol);
374
- streamSymbols.set(binanceSymbol, symbols);
441
+ addMappedSymbol(binanceSymbol, symmSymbol);
375
442
  if (allMarkPricesRefCount === 0) {
376
443
  const wsManager = getBinanceWsManager();
377
444
  allMarkPricesUnsubscribe = wsManager.subscribeAllMarkPrices((entries) => {
@@ -386,7 +453,7 @@ var useBinanceMarkPriceStore = zustand.create((set) => ({
386
453
  nextMarkPrices ??= { ...state.markPrices };
387
454
  nextFundingRates ??= { ...state.fundingRates };
388
455
  nextFundingTimes ??= { ...state.nextFundingTimes };
389
- mappedSymbols.forEach((mappedSymbol) => {
456
+ mappedSymbols.forEach((_count, mappedSymbol) => {
390
457
  nextMarkPrices[mappedSymbol] = entry.markPrice;
391
458
  nextFundingRates[mappedSymbol] = entry.fundingRate;
392
459
  nextFundingTimes[mappedSymbol] = entry.nextFundingTime;
@@ -407,14 +474,9 @@ var useBinanceMarkPriceStore = zustand.create((set) => ({
407
474
  },
408
475
  unsubscribeSymbol: (symmSymbol, rawBinanceSymbol) => {
409
476
  const binanceSymbol = normalizeBinanceSymbol(rawBinanceSymbol);
410
- const symbols = streamSymbols.get(binanceSymbol);
411
- if (symbols) {
412
- symbols.delete(symmSymbol);
413
- if (symbols.size === 0) {
414
- streamSymbols.delete(binanceSymbol);
415
- } else {
416
- streamSymbols.set(binanceSymbol, symbols);
417
- }
477
+ const removedSubscription = removeMappedSymbol(binanceSymbol, symmSymbol);
478
+ if (!removedSubscription) {
479
+ return;
418
480
  }
419
481
  const nextRefCount = getPrevRefCount(binanceSymbol);
420
482
  if (nextRefCount === 0) {
@@ -428,6 +490,10 @@ var useBinanceMarkPriceStore = zustand.create((set) => ({
428
490
  allMarkPricesUnsubscribe = null;
429
491
  }
430
492
  set((state) => {
493
+ const hasMappedSymbol = Boolean(streamSymbols.get(binanceSymbol)?.has(symmSymbol));
494
+ if (hasMappedSymbol) {
495
+ return state;
496
+ }
431
497
  if (state.markPrices[symmSymbol] == null && state.fundingRates[symmSymbol] == null && state.nextFundingTimes[symmSymbol] == null) {
432
498
  return state;
433
499
  }
@@ -545,8 +611,33 @@ var useSymmWsStore = zustand.create((set) => ({
545
611
  }));
546
612
 
547
613
  // src/react/hooks/use-symm-ws.ts
548
- function asUnsubscribeFn(value) {
549
- return typeof value === "function" ? value : null;
614
+ var wsOwnerCounts = /* @__PURE__ */ new WeakMap();
615
+ var wsConnectPromises = /* @__PURE__ */ new WeakMap();
616
+ function addWsOwner(ws) {
617
+ wsOwnerCounts.set(ws, (wsOwnerCounts.get(ws) ?? 0) + 1);
618
+ }
619
+ function removeWsOwner(ws) {
620
+ const nextCount = Math.max(0, (wsOwnerCounts.get(ws) ?? 0) - 1);
621
+ if (nextCount === 0) {
622
+ wsOwnerCounts.delete(ws);
623
+ } else {
624
+ wsOwnerCounts.set(ws, nextCount);
625
+ }
626
+ return nextCount;
627
+ }
628
+ function connectShared(ws) {
629
+ if (ws.isConnected()) {
630
+ return Promise.resolve();
631
+ }
632
+ const existing = wsConnectPromises.get(ws);
633
+ if (existing) {
634
+ return existing;
635
+ }
636
+ const promise = ws.connect().finally(() => {
637
+ wsConnectPromises.delete(ws);
638
+ });
639
+ wsConnectPromises.set(ws, promise);
640
+ return promise;
550
641
  }
551
642
  function useSymmWs(params = {}) {
552
643
  const ctx = react.useContext(SymmContext);
@@ -563,114 +654,76 @@ function useSymmWs(params = {}) {
563
654
  }
564
655
  const ws = symmCoreClient.ws;
565
656
  const addr = accountAddress;
566
- const unsubscribers = [];
567
657
  let cancelled = false;
568
- const removeOnConnect = ws.onConnect(() => {
569
- setConnected(true);
658
+ addWsOwner(ws);
659
+ const removeOnConnect = ws.onConnect(() => setConnected(true));
660
+ const removeOnDisconnect = ws.onDisconnect(() => setConnected(false));
661
+ const subscriptions = [];
662
+ const addSubscription = (channel, handler) => {
663
+ ws.subscribe(channel, addr, chainId, handler);
664
+ subscriptions.push({ channel, handler });
665
+ };
666
+ addSubscription("positions", () => {
667
+ queryClient.invalidateQueries({ queryKey: symmKeys.positionsRoot });
570
668
  });
571
- const removeOnDisconnect = ws.onDisconnect(() => {
572
- setConnected(false);
669
+ addSubscription("open-orders", () => {
670
+ queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
573
671
  });
574
- const removeOnError = asUnsubscribeFn(ws.onError?.(() => {
575
- }));
576
- const removeOnWelcome = asUnsubscribeFn(ws.onWelcome?.(() => {
577
- }));
578
- unsubscribers.push(removeOnConnect, removeOnDisconnect);
579
- if (removeOnError) unsubscribers.push(removeOnError);
580
- if (removeOnWelcome) unsubscribers.push(removeOnWelcome);
581
- const positionsUnsub = asUnsubscribeFn(
582
- ws.subscribeToPositions(addr, chainId, () => {
583
- queryClient.invalidateQueries({
584
- queryKey: symmKeys.positionsRoot
585
- });
586
- })
587
- );
588
- if (positionsUnsub) unsubscribers.push(positionsUnsub);
589
- const openOrdersUnsub = asUnsubscribeFn(
590
- ws.subscribeToOpenOrders(addr, chainId, () => {
591
- queryClient.invalidateQueries({
592
- queryKey: symmKeys.openOrdersRoot
593
- });
594
- })
595
- );
596
- if (openOrdersUnsub) unsubscribers.push(openOrdersUnsub);
597
- const tradesUnsub = asUnsubscribeFn(
598
- ws.subscribeToTrades(addr, chainId, () => {
599
- queryClient.invalidateQueries({
600
- queryKey: symmKeys.tradeHistoryRoot
601
- });
602
- })
603
- );
604
- if (tradesUnsub) unsubscribers.push(tradesUnsub);
605
- const accountSummaryUnsub = asUnsubscribeFn(
606
- ws.subscribeToAccountSummary(addr, chainId, () => {
607
- queryClient.invalidateQueries({
608
- queryKey: symmKeys.balances(accountAddress, chainId)
609
- });
610
- queryClient.invalidateQueries({
611
- queryKey: symmKeys.accountSummary(accountAddress, chainId)
612
- });
613
- })
614
- );
615
- if (accountSummaryUnsub) unsubscribers.push(accountSummaryUnsub);
616
- const notificationsUnsub = asUnsubscribeFn(
617
- ws.subscribeToNotifications(addr, chainId, () => {
618
- queryClient.invalidateQueries({
619
- queryKey: symmKeys.notifications({ accountAddress, chainId })
620
- });
621
- queryClient.invalidateQueries({
622
- queryKey: symmKeys.unreadCount({ accountAddress, chainId })
623
- });
624
- })
625
- );
626
- if (notificationsUnsub) unsubscribers.push(notificationsUnsub);
627
- const tpslUnsub = asUnsubscribeFn(
628
- ws.subscribeToTpsl(addr, chainId, () => {
629
- queryClient.invalidateQueries({ queryKey: symmKeys.tpslOrdersRoot });
630
- queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
631
- })
632
- );
633
- if (tpslUnsub) unsubscribers.push(tpslUnsub);
634
- const twapUnsub = asUnsubscribeFn(
635
- ws.subscribeToTwapOrders(addr, chainId, () => {
636
- queryClient.invalidateQueries({ queryKey: symmKeys.twapOrdersRoot });
637
- queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
638
- })
639
- );
640
- if (twapUnsub) unsubscribers.push(twapUnsub);
641
- const triggerOrdersUnsub = asUnsubscribeFn(
642
- ws.subscribeToTriggerOrders(addr, chainId, () => {
643
- queryClient.invalidateQueries({ queryKey: symmKeys.triggerOrdersRoot });
644
- queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
645
- })
646
- );
647
- if (triggerOrdersUnsub) unsubscribers.push(triggerOrdersUnsub);
648
- const executionsUnsub = asUnsubscribeFn(
649
- ws.subscribeToExecutions(addr, chainId, () => {
650
- queryClient.invalidateQueries({
651
- queryKey: symmKeys.positionsRoot
652
- });
653
- queryClient.invalidateQueries({
654
- queryKey: symmKeys.portfolioRoot
655
- });
656
- })
657
- );
658
- if (executionsUnsub) unsubscribers.push(executionsUnsub);
659
- void ws.connect().then(() => {
660
- if (cancelled) {
661
- return;
672
+ addSubscription("trades", () => {
673
+ queryClient.invalidateQueries({ queryKey: symmKeys.tradeHistoryRoot });
674
+ });
675
+ addSubscription("account-summary", () => {
676
+ queryClient.invalidateQueries({
677
+ queryKey: symmKeys.balances(accountAddress, chainId)
678
+ });
679
+ queryClient.invalidateQueries({
680
+ queryKey: symmKeys.accountSummary(accountAddress, chainId)
681
+ });
682
+ });
683
+ addSubscription("notifications", () => {
684
+ queryClient.invalidateQueries({
685
+ queryKey: symmKeys.notifications({ accountAddress, chainId })
686
+ });
687
+ queryClient.invalidateQueries({
688
+ queryKey: symmKeys.unreadCount({ accountAddress, chainId })
689
+ });
690
+ });
691
+ addSubscription("tpsl", () => {
692
+ queryClient.invalidateQueries({ queryKey: symmKeys.tpslOrdersRoot });
693
+ queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
694
+ });
695
+ addSubscription("twap-orders", () => {
696
+ queryClient.invalidateQueries({ queryKey: symmKeys.twapOrdersRoot });
697
+ queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
698
+ });
699
+ addSubscription("trigger-orders", () => {
700
+ queryClient.invalidateQueries({ queryKey: symmKeys.triggerOrdersRoot });
701
+ queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
702
+ });
703
+ addSubscription("executions", () => {
704
+ queryClient.invalidateQueries({ queryKey: symmKeys.positionsRoot });
705
+ queryClient.invalidateQueries({ queryKey: symmKeys.portfolioRoot });
706
+ });
707
+ void connectShared(ws).then(() => {
708
+ if (cancelled) return;
709
+ if (ws.isConnected()) {
710
+ setConnected(true);
662
711
  }
663
712
  }).catch(() => {
664
- if (cancelled) {
665
- return;
666
- }
713
+ if (cancelled) return;
667
714
  setConnected(false);
668
715
  });
669
716
  return () => {
670
717
  cancelled = true;
671
- unsubscribers.forEach((unsubscribe) => unsubscribe());
672
- ws.disconnect();
673
- setConnected(false);
718
+ removeOnConnect();
719
+ removeOnDisconnect();
720
+ subscriptions.forEach(({ channel, handler }) => {
721
+ ws.unsubscribe(channel, addr, chainId, handler);
722
+ });
723
+ if (removeWsOwner(ws) === 0) {
724
+ ws.disconnect();
725
+ setConnected(false);
726
+ }
674
727
  };
675
728
  }, [symmCoreClient, accountAddress, chainId, queryClient, setConnected]);
676
729
  return { isConnected };