@pear-protocol/symmio-client 0.3.12 → 0.3.14

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.
@@ -57,6 +57,9 @@ function resolveBinanceSymbol(symmSymbol) {
57
57
  // src/utils/binance-ws.ts
58
58
  var BINANCE_WS_URL = "wss://fstream.binance.com/market/ws";
59
59
  var RECONNECT_DELAYS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
60
+ var IDLE_CLOSE_DELAY_MS = 3e4;
61
+ var STALE_CONNECTION_MS = 3e4;
62
+ var STALE_CHECK_INTERVAL_MS = 1e4;
60
63
  var STABLE_QUOTES = ["USDT0", "USDT", "USDC", "USDE", "USDH", "USD"];
61
64
  function normalizeBaseSymbol(symbol) {
62
65
  const normalized = symbol.toUpperCase().trim();
@@ -106,8 +109,11 @@ var BinanceWsManager = class {
106
109
  streams = /* @__PURE__ */ new Map();
107
110
  reconnectAttempt = 0;
108
111
  reconnectTimer = null;
112
+ idleCloseTimer = null;
113
+ staleCheckTimer = null;
109
114
  intentionalClose = false;
110
- pendingSubscribes = [];
115
+ pendingSubscribes = /* @__PURE__ */ new Set();
116
+ lastMessageAt = 0;
111
117
  idCounter = 0;
112
118
  /**
113
119
  * Subscribe to a kline stream. Returns an unsubscribe function.
@@ -180,10 +186,10 @@ var BinanceWsManager = class {
180
186
  */
181
187
  destroy() {
182
188
  this.intentionalClose = true;
183
- if (this.reconnectTimer) {
184
- clearTimeout(this.reconnectTimer);
185
- this.reconnectTimer = null;
186
- }
189
+ this.clearReconnectTimer();
190
+ this.clearIdleCloseTimer();
191
+ this.clearStaleCheckTimer();
192
+ this.pendingSubscribes.clear();
187
193
  if (this.ws) {
188
194
  this.ws.close();
189
195
  this.ws = null;
@@ -203,6 +209,7 @@ var BinanceWsManager = class {
203
209
  }
204
210
  sub.callbacks.set(id, cb);
205
211
  if (isNew) {
212
+ this.clearIdleCloseTimer();
206
213
  this.ensureConnected();
207
214
  this.sendSubscribe([streamName]);
208
215
  }
@@ -214,18 +221,14 @@ var BinanceWsManager = class {
214
221
  if (sub.callbacks.size === 0) {
215
222
  this.streams.delete(streamName);
216
223
  this.sendUnsubscribe([streamName]);
217
- if (this.streams.size === 0 && this.ws) {
218
- this.intentionalClose = true;
219
- this.ws.close();
220
- this.ws = null;
221
- this.intentionalClose = false;
222
- }
224
+ this.scheduleIdleClose();
223
225
  }
224
226
  }
225
227
  ensureConnected() {
226
228
  if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
227
229
  return;
228
230
  }
231
+ this.clearReconnectTimer();
229
232
  this.connect();
230
233
  }
231
234
  connect() {
@@ -234,23 +237,27 @@ var BinanceWsManager = class {
234
237
  this.ws = new WebSocket(BINANCE_WS_URL);
235
238
  this.ws.onopen = () => {
236
239
  this.reconnectAttempt = 0;
237
- const activeStreams = Array.from(this.streams.keys());
240
+ this.lastMessageAt = Date.now();
241
+ this.startStaleCheck();
242
+ const activeStreams = Array.from(
243
+ /* @__PURE__ */ new Set([...this.streams.keys(), ...this.pendingSubscribes])
244
+ );
245
+ this.pendingSubscribes.clear();
238
246
  if (activeStreams.length > 0) {
239
247
  this.sendSubscribe(activeStreams);
240
248
  }
241
- if (this.pendingSubscribes.length > 0) {
242
- this.sendSubscribe(this.pendingSubscribes);
243
- this.pendingSubscribes = [];
244
- }
245
249
  };
246
250
  this.ws.onmessage = (event) => {
247
251
  try {
248
252
  const data = JSON.parse(event.data);
253
+ this.lastMessageAt = Date.now();
249
254
  this.handleMessage(data);
250
255
  } catch {
251
256
  }
252
257
  };
253
258
  this.ws.onclose = () => {
259
+ this.ws = null;
260
+ this.clearStaleCheckTimer();
254
261
  if (this.intentionalClose) return;
255
262
  this.scheduleReconnect();
256
263
  };
@@ -284,25 +291,27 @@ var BinanceWsManager = class {
284
291
  }
285
292
  sendSubscribe(streams) {
286
293
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
287
- this.pendingSubscribes.push(...streams);
294
+ streams.forEach((stream) => this.pendingSubscribes.add(stream));
288
295
  return;
289
296
  }
297
+ const params = Array.from(new Set(streams));
298
+ if (params.length === 0) return;
290
299
  this.ws.send(JSON.stringify({
291
300
  method: "SUBSCRIBE",
292
- params: streams,
301
+ params,
293
302
  id: Date.now()
294
303
  }));
295
304
  }
296
305
  sendUnsubscribe(streams) {
297
306
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
298
- this.pendingSubscribes = this.pendingSubscribes.filter(
299
- (s) => !streams.includes(s)
300
- );
307
+ streams.forEach((stream) => this.pendingSubscribes.delete(stream));
301
308
  return;
302
309
  }
310
+ const params = Array.from(new Set(streams));
311
+ if (params.length === 0) return;
303
312
  this.ws.send(JSON.stringify({
304
313
  method: "UNSUBSCRIBE",
305
- params: streams,
314
+ params,
306
315
  id: Date.now()
307
316
  }));
308
317
  }
@@ -316,6 +325,44 @@ var BinanceWsManager = class {
316
325
  this.connect();
317
326
  }, delay);
318
327
  }
328
+ scheduleIdleClose() {
329
+ if (this.streams.size > 0 || this.idleCloseTimer) return;
330
+ this.clearReconnectTimer();
331
+ if (!this.ws) return;
332
+ this.idleCloseTimer = setTimeout(() => {
333
+ this.idleCloseTimer = null;
334
+ if (this.streams.size > 0 || !this.ws) return;
335
+ this.intentionalClose = true;
336
+ this.ws.close();
337
+ this.ws = null;
338
+ this.intentionalClose = false;
339
+ this.clearStaleCheckTimer();
340
+ }, IDLE_CLOSE_DELAY_MS);
341
+ }
342
+ startStaleCheck() {
343
+ this.clearStaleCheckTimer();
344
+ this.staleCheckTimer = setInterval(() => {
345
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
346
+ if (this.streams.size === 0) return;
347
+ if (Date.now() - this.lastMessageAt < STALE_CONNECTION_MS) return;
348
+ this.ws.close();
349
+ }, STALE_CHECK_INTERVAL_MS);
350
+ }
351
+ clearReconnectTimer() {
352
+ if (!this.reconnectTimer) return;
353
+ clearTimeout(this.reconnectTimer);
354
+ this.reconnectTimer = null;
355
+ }
356
+ clearIdleCloseTimer() {
357
+ if (!this.idleCloseTimer) return;
358
+ clearTimeout(this.idleCloseTimer);
359
+ this.idleCloseTimer = null;
360
+ }
361
+ clearStaleCheckTimer() {
362
+ if (!this.staleCheckTimer) return;
363
+ clearInterval(this.staleCheckTimer);
364
+ this.staleCheckTimer = null;
365
+ }
319
366
  };
320
367
  var _instance = null;
321
368
  function getBinanceWsManager() {
@@ -346,6 +393,28 @@ function getNextRefCount(binanceSymbol) {
346
393
  function getPrevRefCount(binanceSymbol) {
347
394
  return Math.max(0, (refCounts.get(binanceSymbol) ?? 0) - 1);
348
395
  }
396
+ function addMappedSymbol(binanceSymbol, symmSymbol) {
397
+ const symbols = streamSymbols.get(binanceSymbol) ?? /* @__PURE__ */ new Map();
398
+ symbols.set(symmSymbol, (symbols.get(symmSymbol) ?? 0) + 1);
399
+ streamSymbols.set(binanceSymbol, symbols);
400
+ }
401
+ function removeMappedSymbol(binanceSymbol, symmSymbol) {
402
+ const symbols = streamSymbols.get(binanceSymbol);
403
+ if (!symbols) return false;
404
+ const currentCount = symbols.get(symmSymbol) ?? 0;
405
+ if (currentCount <= 0) return false;
406
+ if (currentCount === 1) {
407
+ symbols.delete(symmSymbol);
408
+ } else {
409
+ symbols.set(symmSymbol, currentCount - 1);
410
+ }
411
+ if (symbols.size === 0) {
412
+ streamSymbols.delete(binanceSymbol);
413
+ } else {
414
+ streamSymbols.set(binanceSymbol, symbols);
415
+ }
416
+ return true;
417
+ }
349
418
  var useBinanceMarkPriceStore = zustand.create((set) => ({
350
419
  markPrices: {},
351
420
  fundingRates: {},
@@ -354,9 +423,7 @@ var useBinanceMarkPriceStore = zustand.create((set) => ({
354
423
  const binanceSymbol = normalizeBinanceSymbol(rawBinanceSymbol);
355
424
  const nextRefCount = getNextRefCount(binanceSymbol);
356
425
  refCounts.set(binanceSymbol, nextRefCount);
357
- const symbols = streamSymbols.get(binanceSymbol) ?? /* @__PURE__ */ new Set();
358
- symbols.add(symmSymbol);
359
- streamSymbols.set(binanceSymbol, symbols);
426
+ addMappedSymbol(binanceSymbol, symmSymbol);
360
427
  if (allMarkPricesRefCount === 0) {
361
428
  const wsManager = getBinanceWsManager();
362
429
  allMarkPricesUnsubscribe = wsManager.subscribeAllMarkPrices((entries) => {
@@ -371,7 +438,7 @@ var useBinanceMarkPriceStore = zustand.create((set) => ({
371
438
  nextMarkPrices ??= { ...state.markPrices };
372
439
  nextFundingRates ??= { ...state.fundingRates };
373
440
  nextFundingTimes ??= { ...state.nextFundingTimes };
374
- mappedSymbols.forEach((mappedSymbol) => {
441
+ mappedSymbols.forEach((_count, mappedSymbol) => {
375
442
  nextMarkPrices[mappedSymbol] = entry.markPrice;
376
443
  nextFundingRates[mappedSymbol] = entry.fundingRate;
377
444
  nextFundingTimes[mappedSymbol] = entry.nextFundingTime;
@@ -392,14 +459,9 @@ var useBinanceMarkPriceStore = zustand.create((set) => ({
392
459
  },
393
460
  unsubscribeSymbol: (symmSymbol, rawBinanceSymbol) => {
394
461
  const binanceSymbol = normalizeBinanceSymbol(rawBinanceSymbol);
395
- const symbols = streamSymbols.get(binanceSymbol);
396
- if (symbols) {
397
- symbols.delete(symmSymbol);
398
- if (symbols.size === 0) {
399
- streamSymbols.delete(binanceSymbol);
400
- } else {
401
- streamSymbols.set(binanceSymbol, symbols);
402
- }
462
+ const removedSubscription = removeMappedSymbol(binanceSymbol, symmSymbol);
463
+ if (!removedSubscription) {
464
+ return;
403
465
  }
404
466
  const nextRefCount = getPrevRefCount(binanceSymbol);
405
467
  if (nextRefCount === 0) {
@@ -413,6 +475,10 @@ var useBinanceMarkPriceStore = zustand.create((set) => ({
413
475
  allMarkPricesUnsubscribe = null;
414
476
  }
415
477
  set((state) => {
478
+ const hasMappedSymbol = Boolean(streamSymbols.get(binanceSymbol)?.has(symmSymbol));
479
+ if (hasMappedSymbol) {
480
+ return state;
481
+ }
416
482
  if (state.markPrices[symmSymbol] == null && state.fundingRates[symmSymbol] == null && state.nextFundingTimes[symmSymbol] == null) {
417
483
  return state;
418
484
  }
@@ -497,7 +563,7 @@ var symmKeys = {
497
563
  signature: (address, chainId) => ["symm", "signature", address, chainId],
498
564
  auth: (accountAddress, chainId, signerAddress) => ["symm", "auth", accountAddress, chainId, signerAddress],
499
565
  approval: (owner, spender, chainId, token) => ["symm", "approval", owner, spender, chainId, token],
500
- balances: (address, chainId) => ["symm", "balances", address, chainId],
566
+ balances: (address, chainId, multiAccountAddress) => multiAccountAddress === void 0 ? ["symm", "balances", address, chainId] : ["symm", "balances", address, chainId, multiAccountAddress],
501
567
  positions: (params) => ["symm", "positions", params],
502
568
  openOrders: (params) => ["symm", "openOrders", params],
503
569
  tradeHistory: (params) => ["symm", "tradeHistory", params],
@@ -530,8 +596,33 @@ var useSymmWsStore = zustand.create((set) => ({
530
596
  }));
531
597
 
532
598
  // src/react/hooks/use-symm-ws.ts
533
- function asUnsubscribeFn(value) {
534
- return typeof value === "function" ? value : null;
599
+ var wsOwnerCounts = /* @__PURE__ */ new WeakMap();
600
+ var wsConnectPromises = /* @__PURE__ */ new WeakMap();
601
+ function addWsOwner(ws) {
602
+ wsOwnerCounts.set(ws, (wsOwnerCounts.get(ws) ?? 0) + 1);
603
+ }
604
+ function removeWsOwner(ws) {
605
+ const nextCount = Math.max(0, (wsOwnerCounts.get(ws) ?? 0) - 1);
606
+ if (nextCount === 0) {
607
+ wsOwnerCounts.delete(ws);
608
+ } else {
609
+ wsOwnerCounts.set(ws, nextCount);
610
+ }
611
+ return nextCount;
612
+ }
613
+ function connectShared(ws) {
614
+ if (ws.isConnected()) {
615
+ return Promise.resolve();
616
+ }
617
+ const existing = wsConnectPromises.get(ws);
618
+ if (existing) {
619
+ return existing;
620
+ }
621
+ const promise = ws.connect().finally(() => {
622
+ wsConnectPromises.delete(ws);
623
+ });
624
+ wsConnectPromises.set(ws, promise);
625
+ return promise;
535
626
  }
536
627
  function useSymmWs(params = {}) {
537
628
  const ctx = react.useContext(SymmContext);
@@ -548,114 +639,76 @@ function useSymmWs(params = {}) {
548
639
  }
549
640
  const ws = symmCoreClient.ws;
550
641
  const addr = accountAddress;
551
- const unsubscribers = [];
552
642
  let cancelled = false;
553
- const removeOnConnect = ws.onConnect(() => {
554
- setConnected(true);
643
+ addWsOwner(ws);
644
+ const removeOnConnect = ws.onConnect(() => setConnected(true));
645
+ const removeOnDisconnect = ws.onDisconnect(() => setConnected(false));
646
+ const subscriptions = [];
647
+ const addSubscription = (channel, handler) => {
648
+ ws.subscribe(channel, addr, chainId, handler);
649
+ subscriptions.push({ channel, handler });
650
+ };
651
+ addSubscription("positions", () => {
652
+ queryClient.invalidateQueries({ queryKey: symmKeys.positionsRoot });
555
653
  });
556
- const removeOnDisconnect = ws.onDisconnect(() => {
557
- setConnected(false);
654
+ addSubscription("open-orders", () => {
655
+ queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
558
656
  });
559
- const removeOnError = asUnsubscribeFn(ws.onError?.(() => {
560
- }));
561
- const removeOnWelcome = asUnsubscribeFn(ws.onWelcome?.(() => {
562
- }));
563
- unsubscribers.push(removeOnConnect, removeOnDisconnect);
564
- if (removeOnError) unsubscribers.push(removeOnError);
565
- if (removeOnWelcome) unsubscribers.push(removeOnWelcome);
566
- const positionsUnsub = asUnsubscribeFn(
567
- ws.subscribeToPositions(addr, chainId, () => {
568
- queryClient.invalidateQueries({
569
- queryKey: symmKeys.positionsRoot
570
- });
571
- })
572
- );
573
- if (positionsUnsub) unsubscribers.push(positionsUnsub);
574
- const openOrdersUnsub = asUnsubscribeFn(
575
- ws.subscribeToOpenOrders(addr, chainId, () => {
576
- queryClient.invalidateQueries({
577
- queryKey: symmKeys.openOrdersRoot
578
- });
579
- })
580
- );
581
- if (openOrdersUnsub) unsubscribers.push(openOrdersUnsub);
582
- const tradesUnsub = asUnsubscribeFn(
583
- ws.subscribeToTrades(addr, chainId, () => {
584
- queryClient.invalidateQueries({
585
- queryKey: symmKeys.tradeHistoryRoot
586
- });
587
- })
588
- );
589
- if (tradesUnsub) unsubscribers.push(tradesUnsub);
590
- const accountSummaryUnsub = asUnsubscribeFn(
591
- ws.subscribeToAccountSummary(addr, chainId, () => {
592
- queryClient.invalidateQueries({
593
- queryKey: symmKeys.balances(accountAddress, chainId)
594
- });
595
- queryClient.invalidateQueries({
596
- queryKey: symmKeys.accountSummary(accountAddress, chainId)
597
- });
598
- })
599
- );
600
- if (accountSummaryUnsub) unsubscribers.push(accountSummaryUnsub);
601
- const notificationsUnsub = asUnsubscribeFn(
602
- ws.subscribeToNotifications(addr, chainId, () => {
603
- queryClient.invalidateQueries({
604
- queryKey: symmKeys.notifications({ accountAddress, chainId })
605
- });
606
- queryClient.invalidateQueries({
607
- queryKey: symmKeys.unreadCount({ accountAddress, chainId })
608
- });
609
- })
610
- );
611
- if (notificationsUnsub) unsubscribers.push(notificationsUnsub);
612
- const tpslUnsub = asUnsubscribeFn(
613
- ws.subscribeToTpsl(addr, chainId, () => {
614
- queryClient.invalidateQueries({ queryKey: symmKeys.tpslOrdersRoot });
615
- queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
616
- })
617
- );
618
- if (tpslUnsub) unsubscribers.push(tpslUnsub);
619
- const twapUnsub = asUnsubscribeFn(
620
- ws.subscribeToTwapOrders(addr, chainId, () => {
621
- queryClient.invalidateQueries({ queryKey: symmKeys.twapOrdersRoot });
622
- queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
623
- })
624
- );
625
- if (twapUnsub) unsubscribers.push(twapUnsub);
626
- const triggerOrdersUnsub = asUnsubscribeFn(
627
- ws.subscribeToTriggerOrders(addr, chainId, () => {
628
- queryClient.invalidateQueries({ queryKey: symmKeys.triggerOrdersRoot });
629
- queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
630
- })
631
- );
632
- if (triggerOrdersUnsub) unsubscribers.push(triggerOrdersUnsub);
633
- const executionsUnsub = asUnsubscribeFn(
634
- ws.subscribeToExecutions(addr, chainId, () => {
635
- queryClient.invalidateQueries({
636
- queryKey: symmKeys.positionsRoot
637
- });
638
- queryClient.invalidateQueries({
639
- queryKey: symmKeys.portfolioRoot
640
- });
641
- })
642
- );
643
- if (executionsUnsub) unsubscribers.push(executionsUnsub);
644
- void ws.connect().then(() => {
645
- if (cancelled) {
646
- return;
657
+ addSubscription("trades", () => {
658
+ queryClient.invalidateQueries({ queryKey: symmKeys.tradeHistoryRoot });
659
+ });
660
+ addSubscription("account-summary", () => {
661
+ queryClient.invalidateQueries({
662
+ queryKey: symmKeys.balances(accountAddress, chainId)
663
+ });
664
+ queryClient.invalidateQueries({
665
+ queryKey: symmKeys.accountSummary(accountAddress, chainId)
666
+ });
667
+ });
668
+ addSubscription("notifications", () => {
669
+ queryClient.invalidateQueries({
670
+ queryKey: symmKeys.notifications({ accountAddress, chainId })
671
+ });
672
+ queryClient.invalidateQueries({
673
+ queryKey: symmKeys.unreadCount({ accountAddress, chainId })
674
+ });
675
+ });
676
+ addSubscription("tpsl", () => {
677
+ queryClient.invalidateQueries({ queryKey: symmKeys.tpslOrdersRoot });
678
+ queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
679
+ });
680
+ addSubscription("twap-orders", () => {
681
+ queryClient.invalidateQueries({ queryKey: symmKeys.twapOrdersRoot });
682
+ queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
683
+ });
684
+ addSubscription("trigger-orders", () => {
685
+ queryClient.invalidateQueries({ queryKey: symmKeys.triggerOrdersRoot });
686
+ queryClient.invalidateQueries({ queryKey: symmKeys.openOrdersRoot });
687
+ });
688
+ addSubscription("executions", () => {
689
+ queryClient.invalidateQueries({ queryKey: symmKeys.positionsRoot });
690
+ queryClient.invalidateQueries({ queryKey: symmKeys.portfolioRoot });
691
+ });
692
+ void connectShared(ws).then(() => {
693
+ if (cancelled) return;
694
+ if (ws.isConnected()) {
695
+ setConnected(true);
647
696
  }
648
697
  }).catch(() => {
649
- if (cancelled) {
650
- return;
651
- }
698
+ if (cancelled) return;
652
699
  setConnected(false);
653
700
  });
654
701
  return () => {
655
702
  cancelled = true;
656
- unsubscribers.forEach((unsubscribe) => unsubscribe());
657
- ws.disconnect();
658
- setConnected(false);
703
+ removeOnConnect();
704
+ removeOnDisconnect();
705
+ subscriptions.forEach(({ channel, handler }) => {
706
+ ws.unsubscribe(channel, addr, chainId, handler);
707
+ });
708
+ if (removeWsOwner(ws) === 0) {
709
+ ws.disconnect();
710
+ setConnected(false);
711
+ }
659
712
  };
660
713
  }, [symmCoreClient, accountAddress, chainId, queryClient, setConnected]);
661
714
  return { isConnected };