@agg-build/sdk 4.32.0 → 4.34.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.
@@ -509,6 +509,7 @@ const WATCHDOG_CHECK_INTERVAL_MS = 6e4;
509
509
  const WATCHDOG_BATCH_MAX = 100;
510
510
  /** A watchdog REST check that takes longer than this is abandoned. */
511
511
  const WATCHDOG_REQUEST_TIMEOUT_MS = 15e3;
512
+ const CATALOG_FRAME_ID_PREFIX = "catalog-";
512
513
  /**
513
514
  * Agg WebSocket client. Single WS connection with refcounted market subscriptions.
514
515
  */
@@ -548,6 +549,10 @@ var AggWebSocket = class {
548
549
  this.seenEventIds = /* @__PURE__ */ new Set();
549
550
  this.hasConnected = false;
550
551
  this.appEventCbs = /* @__PURE__ */ new Set();
552
+ this.catalogSubs = /* @__PURE__ */ new Set();
553
+ this.catalogSentKey = null;
554
+ this.catalogFrameSeq = 0;
555
+ this.catalogAckedSeq = 0;
551
556
  this.rejected = false;
552
557
  this.deferBookViews = options.deferBookViews ?? false;
553
558
  this.url = options.url;
@@ -601,6 +606,7 @@ var AggWebSocket = class {
601
606
  tradeOutcomeIds: this.activeTradeMarkets(),
602
607
  arbMarketIds: this.activeArbMarkets(),
603
608
  arbFeedSubscribers: this.arbFeedCbs.size,
609
+ catalogSubscribers: this.catalogSubs.size,
604
610
  cachedBooks: this.books.size,
605
611
  cachedArb: this.latestArb.size,
606
612
  resyncingBooks: this.resyncing.size,
@@ -683,6 +689,78 @@ var AggWebSocket = class {
683
689
  };
684
690
  }
685
691
  /**
692
+ * Subscribe to the catalog feed: new, changed and deleted events and
693
+ * markets. Live only — pair it with `GET /catalog/changes` (or use
694
+ * `AggClient.watchCatalog`, which does both).
695
+ *
696
+ * One server subscription is shared by every caller. Its filter is the
697
+ * union of the callers' filters (no filter when any caller has none), and
698
+ * each caller only receives frames matching its own filter. The subscribe
699
+ * is re-sent on every reconnect; `onSubscribed` fires each time the server
700
+ * confirms it. Returns an unsubscribe function.
701
+ */
702
+ subscribeCatalog(filter, cb, opts = {}) {
703
+ const sub = {
704
+ filter: {
705
+ ...filter.venues?.length ? { venues: [...filter.venues] } : {},
706
+ ...filter.entities?.length ? { entities: [...filter.entities] } : {}
707
+ },
708
+ cb,
709
+ opts
710
+ };
711
+ this.catalogSubs.add(sub);
712
+ this.ensureConnected();
713
+ if (this._connected) {
714
+ if (!this.sendCatalogSubscribe(sub)) {
715
+ if (this.catalogAckedSeq >= this.catalogFrameSeq) queueMicrotask(() => {
716
+ if (this.catalogSubs.has(sub)) sub.opts.onSubscribed?.();
717
+ });
718
+ else sub.awaitingAck = this.catalogFrameSeq;
719
+ }
720
+ }
721
+ return () => {
722
+ if (!this.catalogSubs.delete(sub) || !this._connected) return;
723
+ if (this.catalogSubs.size === 0) {
724
+ this.catalogSentKey = null;
725
+ this.send({
726
+ action: "unsubscribe",
727
+ channel: "catalog"
728
+ });
729
+ } else this.sendCatalogSubscribe();
730
+ };
731
+ }
732
+ /**
733
+ * Send the union filter if it differs from what the server holds. Callers
734
+ * in `confirm` wait for this frame's ack. Returns whether a frame was sent.
735
+ */
736
+ sendCatalogSubscribe(...confirm) {
737
+ const filter = unionCatalogFilter([...this.catalogSubs].map((s) => s.filter));
738
+ const key = JSON.stringify(filter);
739
+ if (key === this.catalogSentKey) return false;
740
+ this.catalogSentKey = key;
741
+ const seq = ++this.catalogFrameSeq;
742
+ for (const sub of confirm) sub.awaitingAck = seq;
743
+ this.send({
744
+ action: "subscribe",
745
+ channel: "catalog",
746
+ ...filter,
747
+ id: `${CATALOG_FRAME_ID_PREFIX}${seq}`
748
+ });
749
+ return true;
750
+ }
751
+ handleCatalogAck(msg) {
752
+ const seq = catalogFrameSeq(msg.id) ?? this.catalogFrameSeq;
753
+ if (seq > this.catalogAckedSeq) this.catalogAckedSeq = seq;
754
+ for (const sub of [...this.catalogSubs]) {
755
+ if (sub.awaitingAck === void 0 || sub.awaitingAck > seq) continue;
756
+ sub.awaitingAck = void 0;
757
+ sub.opts.onSubscribed?.();
758
+ }
759
+ }
760
+ handleCatalogChange(msg) {
761
+ for (const sub of [...this.catalogSubs]) if (this.catalogSubs.has(sub) && matchesCatalogFilter(sub.filter, msg)) sub.cb(msg);
762
+ }
763
+ /**
686
764
  * Subscribe to a feed by outcome ID.
687
765
  * @param outcomeId The venueMarketOutcomeId to subscribe to.
688
766
  * @param channel "orderbook" (default) or "trades".
@@ -887,6 +965,8 @@ var AggWebSocket = class {
887
965
  this.arbSubs.clear();
888
966
  this.arbFeedCbs.clear();
889
967
  this.appEventCbs.clear();
968
+ this.catalogSubs.clear();
969
+ this.catalogSentKey = null;
890
970
  this.seenEventIds.clear();
891
971
  this.latestArb.clear();
892
972
  this.pendingFlushArbSubs.clear();
@@ -944,7 +1024,7 @@ var AggWebSocket = class {
944
1024
  });
945
1025
  return;
946
1026
  }
947
- if (!this.destroyed && (this.subs.size > 0 || this.arbSubs.size > 0 || this.arbFeedCbs.size > 0 || this.appEventCbs.size > 0 || !!this.initialToken)) this.scheduleReconnect();
1027
+ if (!this.destroyed && (this.subs.size > 0 || this.arbSubs.size > 0 || this.arbFeedCbs.size > 0 || this.appEventCbs.size > 0 || this.catalogSubs.size > 0 || !!this.initialToken)) this.scheduleReconnect();
948
1028
  };
949
1029
  ws.onerror = () => {};
950
1030
  }
@@ -1015,6 +1095,7 @@ var AggWebSocket = class {
1015
1095
  this.handleHeartbeat(raw);
1016
1096
  break;
1017
1097
  case "subscribed":
1098
+ if (raw.channel === "catalog") this.handleCatalogAck(raw);
1018
1099
  this.callbacks.onSubscribed?.(raw);
1019
1100
  break;
1020
1101
  case "unsubscribed":
@@ -1046,13 +1127,24 @@ var AggWebSocket = class {
1046
1127
  channels: msg.channels
1047
1128
  });
1048
1129
  this.callbacks.onStreamInterrupted?.(msg);
1130
+ if (msg.channels?.includes("catalog")) for (const sub of [...this.catalogSubs]) sub.opts.onInterrupted?.();
1049
1131
  break;
1050
1132
  }
1051
1133
  case "market_resolved":
1052
1134
  this.handleMarketResolved(raw);
1053
1135
  break;
1054
- case "error":
1055
- this.callbacks.onError?.(raw);
1136
+ case "error": {
1137
+ const msg = raw;
1138
+ const seq = catalogFrameSeq(msg.id);
1139
+ if (seq !== void 0) {
1140
+ if (seq === this.catalogFrameSeq) this.catalogSentKey = null;
1141
+ for (const sub of [...this.catalogSubs]) if (sub.awaitingAck !== void 0 && sub.awaitingAck <= seq) sub.opts.onError?.(msg);
1142
+ }
1143
+ this.callbacks.onError?.(msg);
1144
+ break;
1145
+ }
1146
+ case "catalog_change":
1147
+ this.handleCatalogChange(raw);
1056
1148
  break;
1057
1149
  case "order_submitted":
1058
1150
  this.handleOrderSubmitted(raw);
@@ -1152,6 +1244,9 @@ var AggWebSocket = class {
1152
1244
  channel: "arb-feed"
1153
1245
  });
1154
1246
  if (this.appEventCbs.size > 0) this.sendAppEventsSubscribe();
1247
+ this.catalogSentKey = null;
1248
+ this.catalogAckedSeq = this.catalogFrameSeq;
1249
+ if (this.catalogSubs.size > 0) this.sendCatalogSubscribe(...this.catalogSubs);
1155
1250
  }
1156
1251
  /**
1157
1252
  * Track `eventId` per stream and drop duplicates a resume re-delivers.
@@ -1556,6 +1651,24 @@ var AggWebSocket = class {
1556
1651
  return this.enableLogs;
1557
1652
  }
1558
1653
  };
1654
+ /** Union of caller filters; an unset dimension on any caller leaves it unset. */
1655
+ function unionCatalogFilter(filters) {
1656
+ const union = {};
1657
+ if (filters.every((f) => f.venues?.length)) union.venues = [...new Set(filters.flatMap((f) => f.venues))].sort();
1658
+ if (filters.every((f) => f.entities?.length)) union.entities = [...new Set(filters.flatMap((f) => f.entities))].sort();
1659
+ return union;
1660
+ }
1661
+ /** Same rule as the server: deletes carry no venue and pass the venue filter. */
1662
+ function matchesCatalogFilter(filter, change) {
1663
+ if (filter.entities && !filter.entities.includes(change.entity)) return false;
1664
+ if (change.op === "deleted" || change.venue === null) return true;
1665
+ return !filter.venues || filter.venues.includes(change.venue);
1666
+ }
1667
+ function catalogFrameSeq(id) {
1668
+ if (typeof id !== "string" || !id.startsWith(CATALOG_FRAME_ID_PREFIX)) return void 0;
1669
+ const seq = Number(id.slice(8));
1670
+ return Number.isInteger(seq) ? seq : void 0;
1671
+ }
1559
1672
  /** Order server event ids ("<ms>-<seq>"); an absent id sorts first. */
1560
1673
  function compareEventIds(a, b) {
1561
1674
  if (b === void 0) return 1;