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