@agg-build/sdk 4.34.1 → 4.37.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.
@@ -472,9 +472,12 @@ var IndexedOrderbook = class {
472
472
  /**
473
473
  * Agg platform WebSocket client.
474
474
  *
475
- * Single connection, refcounted subscriptions. Orderbook/trades subscriptions
476
- * are keyed by venueMarketOutcomeId (the `outcomes[].id` on REST market
477
- * responses — NOT a venueMarketId); arb subscriptions are keyed by marketId.
475
+ * Refcounted subscriptions. Orderbook/trades subscriptions are keyed by
476
+ * venueMarketOutcomeId (the `outcomes[].id` on REST market responses — NOT a
477
+ * venueMarketId); arb subscriptions are keyed by marketId.
478
+ * One socket unless `orderbookUrl` is set, in which case only the orderbook
479
+ * channel uses that second socket. An orderbook-only subscription then leaves
480
+ * the primary socket idle. Trades, arb, catalog, and user events still use it.
478
481
  * Maintains in-memory orderbook state per subscribed outcome id.
479
482
  * All timestamps converted from server ms to seconds before invoking callbacks.
480
483
  *
@@ -486,6 +489,10 @@ var IndexedOrderbook = class {
486
489
  */
487
490
  const RECONNECT_BASE_MS = 1e3;
488
491
  const RECONNECT_MAX_MS = 3e4;
492
+ const SNAPSHOT_RETRY_MAX_ATTEMPTS = 3;
493
+ /** Bound recovery traffic independently of the number of subscribed outcomes. */
494
+ const SNAPSHOT_RETRY_BATCH_SIZE = 20;
495
+ const SNAPSHOT_RETRY_TICK_MS = 1e3;
489
496
  /** WebSocket.OPEN — a literal so environments without a global WebSocket still load. */
490
497
  const WS_OPEN = 1;
491
498
  /** Close codes for configuration errors that a retry cannot fix. */
@@ -511,7 +518,8 @@ const WATCHDOG_BATCH_MAX = 100;
511
518
  const WATCHDOG_REQUEST_TIMEOUT_MS = 15e3;
512
519
  const CATALOG_FRAME_ID_PREFIX = "catalog-";
513
520
  /**
514
- * Agg WebSocket client. Single WS connection with refcounted market subscriptions.
521
+ * Agg WebSocket client. Refcounted market subscriptions on one socket,
522
+ * or a second socket for orderbooks when `orderbookUrl` is set.
515
523
  */
516
524
  var AggWebSocket = class {
517
525
  constructor(options) {
@@ -537,6 +545,9 @@ var AggWebSocket = class {
537
545
  this.watchdogTimer = null;
538
546
  this.watchdogAbort = null;
539
547
  this.watchdogPausedUntil = 0;
548
+ this.snapshotRetries = /* @__PURE__ */ new Map();
549
+ this.snapshotRetryTimer = null;
550
+ this.snapshotRetryLastSentAt = -Infinity;
540
551
  this.connecting = false;
541
552
  this._connected = false;
542
553
  this.reconnectAttempts = 0;
@@ -545,6 +556,14 @@ var AggWebSocket = class {
545
556
  this.lastMessageTime = 0;
546
557
  this.destroyed = false;
547
558
  this.historyControllers = /* @__PURE__ */ new Set();
559
+ this.obWs = null;
560
+ this.obConnecting = false;
561
+ this._obConnected = false;
562
+ this.obReconnectAttempts = 0;
563
+ this.obReconnectTimer = null;
564
+ this.obStaleTimer = null;
565
+ this.obLastMessageTime = 0;
566
+ this.obRejected = false;
548
567
  this.lastEventId = {};
549
568
  this.seenEventIds = /* @__PURE__ */ new Set();
550
569
  this.hasConnected = false;
@@ -556,6 +575,8 @@ var AggWebSocket = class {
556
575
  this.rejected = false;
557
576
  this.deferBookViews = options.deferBookViews ?? false;
558
577
  this.url = options.url;
578
+ this.orderbookUrl = options.orderbookUrl || void 0;
579
+ this.orderbookLatency = options.orderbookLatency === "realtime" ? "realtime" : "conflated";
559
580
  this.webSocketImpl = options.webSocketImpl;
560
581
  this.headers = options.headers;
561
582
  this.resumeEvents = options.resumeEvents ?? true;
@@ -662,6 +683,7 @@ var AggWebSocket = class {
662
683
  this.scheduleFlush();
663
684
  }
664
685
  }
686
+ this.releasePrimaryIfIdle();
665
687
  }
666
688
  };
667
689
  }
@@ -686,6 +708,7 @@ var AggWebSocket = class {
686
708
  action: "unsubscribe",
687
709
  channel: "arb-feed"
688
710
  });
711
+ this.releasePrimaryIfIdle();
689
712
  };
690
713
  }
691
714
  /**
@@ -719,14 +742,17 @@ var AggWebSocket = class {
719
742
  }
720
743
  }
721
744
  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();
745
+ if (!this.catalogSubs.delete(sub)) return;
746
+ if (this._connected) {
747
+ if (this.catalogSubs.size === 0) {
748
+ this.catalogSentKey = null;
749
+ this.send({
750
+ action: "unsubscribe",
751
+ channel: "catalog"
752
+ });
753
+ } else this.sendCatalogSubscribe();
754
+ }
755
+ this.releasePrimaryIfIdle();
730
756
  };
731
757
  }
732
758
  /**
@@ -779,8 +805,8 @@ var AggWebSocket = class {
779
805
  if (sub.refCount === 0) {
780
806
  this.bookCheckedAt.set(outcomeId, Date.now());
781
807
  this.pendingSubs.add(outcomeId);
782
- this.ensureConnected();
783
- if (this._connected) {
808
+ this.ensureOrderbookTransport();
809
+ if (this.orderbookTransportReady()) {
784
810
  this.pendingFlushUnsubs.delete(outcomeId);
785
811
  this.pendingFlushSubs.add(outcomeId);
786
812
  this.pendingSubs.delete(outcomeId);
@@ -789,6 +815,7 @@ var AggWebSocket = class {
789
815
  }
790
816
  sub.refCount++;
791
817
  let released = false;
818
+ this.releasePrimaryIfIdle();
792
819
  return () => {
793
820
  if (released) return;
794
821
  released = true;
@@ -803,8 +830,9 @@ var AggWebSocket = class {
803
830
  }
804
831
  this.resyncing.delete(outcomeId);
805
832
  this.bookCheckedAt.delete(outcomeId);
833
+ this.snapshotRetries.delete(outcomeId);
806
834
  this.watchdogResnapshotted.delete(outcomeId);
807
- if (this._connected) {
835
+ if (this.orderbookTransportReady()) {
808
836
  if (this.pendingFlushSubs.has(outcomeId)) this.pendingFlushSubs.delete(outcomeId);
809
837
  else {
810
838
  this.pendingFlushUnsubs.add(outcomeId);
@@ -813,6 +841,7 @@ var AggWebSocket = class {
813
841
  }
814
842
  this.pendingSubs.delete(outcomeId);
815
843
  this.pendingResnapshots.delete(outcomeId);
844
+ this.releasePrimaryIfIdle();
816
845
  }
817
846
  };
818
847
  }
@@ -867,6 +896,7 @@ var AggWebSocket = class {
867
896
  this.books.delete(outcomeId);
868
897
  this.resyncing.delete(outcomeId);
869
898
  }
899
+ this.releasePrimaryIfIdle();
870
900
  }
871
901
  };
872
902
  }
@@ -896,6 +926,7 @@ var AggWebSocket = class {
896
926
  action: "unsubscribe",
897
927
  channel: "app-events"
898
928
  });
929
+ this.releasePrimaryIfIdle();
899
930
  };
900
931
  }
901
932
  sendAppEventsSubscribe() {
@@ -923,20 +954,34 @@ var AggWebSocket = class {
923
954
  /** Connect (idempotent). Called automatically on first subscribe. */
924
955
  connect() {
925
956
  this.rejected = false;
926
- this.ensureConnected();
957
+ this.obRejected = false;
958
+ if (!this.orderbookUrl || this.needsPrimarySocket()) this.ensureConnected();
959
+ if (this.orderbookUrl && (this.activeOrderbookMarkets().length > 0 || this.pendingSubs.size > 0 || this.pendingResnapshots.size > 0)) this.ensureOrderbookConnected();
927
960
  }
928
961
  /** Disconnect and clean up. Reusable — call connect() to reconnect. */
929
962
  disconnect() {
930
963
  this.cancelReconnect();
964
+ this.cancelOrderbookReconnect();
931
965
  this.cancelStaleTimer();
966
+ this.cancelOrderbookStaleTimer();
967
+ this.clearSnapshotRetries();
968
+ const orderbookWasConnected = !!this.orderbookUrl && this._obConnected;
932
969
  this._connected = false;
970
+ this._obConnected = false;
933
971
  this.connecting = false;
972
+ this.obConnecting = false;
934
973
  if (this.ws) {
935
974
  this.ws.onclose = null;
936
975
  this.ws.close();
937
976
  this.ws = null;
938
977
  }
978
+ if (this.obWs) {
979
+ this.obWs.onclose = null;
980
+ this.obWs.close();
981
+ this.obWs = null;
982
+ }
939
983
  this.callbacks.onConnectionStateChange?.(false);
984
+ if (orderbookWasConnected) this.callbacks.onOrderbookConnectionStateChange?.(false);
940
985
  }
941
986
  /** Permanently destroy. Cannot reconnect after this. */
942
987
  destroy() {
@@ -983,6 +1028,50 @@ var AggWebSocket = class {
983
1028
  if (this.ws?.readyState === WS_OPEN || this.connecting) return;
984
1029
  this.doConnect();
985
1030
  }
1031
+ /** Trades, arb, catalog, app events, and user sessions. Orderbooks count only on the shared socket. */
1032
+ needsPrimarySocket() {
1033
+ if (this.initialToken) return true;
1034
+ if (this.arbSubs.size > 0 || this.arbFeedCbs.size > 0 || this.appEventCbs.size > 0 || this.catalogSubs.size > 0) return true;
1035
+ for (const sub of this.subs.values()) {
1036
+ if (sub.tradeRefs > 0) return true;
1037
+ if (!this.orderbookUrl && sub.refCount > 0) return true;
1038
+ }
1039
+ return false;
1040
+ }
1041
+ /** Drop a primary socket that only an orderbook subscription was holding open. */
1042
+ releasePrimaryIfIdle() {
1043
+ if (!this.orderbookUrl || this.needsPrimarySocket()) return;
1044
+ this.cancelReconnect();
1045
+ const wasConnected = this._connected;
1046
+ this.connecting = false;
1047
+ this._connected = false;
1048
+ this.cancelStaleTimer();
1049
+ const ws = this.ws;
1050
+ this.ws = null;
1051
+ if (ws) {
1052
+ ws.onclose = null;
1053
+ ws.close();
1054
+ }
1055
+ if (wasConnected) this.callbacks.onConnectionStateChange?.(false);
1056
+ }
1057
+ /** True when orderbook subscriptions use the separate `orderbookUrl` socket. */
1058
+ get usesOrderbookSocket() {
1059
+ return !!this.orderbookUrl;
1060
+ }
1061
+ /** Orderbook readiness. Without `orderbookUrl` this is the primary socket. */
1062
+ orderbookTransportReady() {
1063
+ return this.orderbookUrl ? this._obConnected : this._connected;
1064
+ }
1065
+ ensureOrderbookTransport() {
1066
+ if (this.orderbookUrl) this.ensureOrderbookConnected();
1067
+ else this.ensureConnected();
1068
+ }
1069
+ ensureOrderbookConnected() {
1070
+ if (!this.orderbookUrl) return;
1071
+ if (this.destroyed || this.obRejected) return;
1072
+ if (this.obWs?.readyState === WS_OPEN || this.obConnecting) return;
1073
+ this.doConnectOrderbook();
1074
+ }
986
1075
  doConnect() {
987
1076
  if (this.destroyed) return;
988
1077
  const Impl = this.webSocketImpl ?? globalThis.WebSocket;
@@ -1013,6 +1102,7 @@ var AggWebSocket = class {
1013
1102
  const wasConnected = this._connected;
1014
1103
  this._connected = false;
1015
1104
  this.cancelStaleTimer();
1105
+ if (!this.orderbookUrl) this.clearSnapshotRetries();
1016
1106
  if (wasConnected) this.callbacks.onConnectionStateChange?.(false);
1017
1107
  const code = event?.code ?? 0;
1018
1108
  if (NON_RETRYABLE_CLOSE_CODES.has(code)) {
@@ -1024,7 +1114,7 @@ var AggWebSocket = class {
1024
1114
  });
1025
1115
  return;
1026
1116
  }
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();
1117
+ if (!this.destroyed && this.needsPrimarySocket()) this.scheduleReconnect();
1028
1118
  };
1029
1119
  ws.onerror = () => {};
1030
1120
  }
@@ -1066,6 +1156,89 @@ var AggWebSocket = class {
1066
1156
  this.staleTimer = null;
1067
1157
  }
1068
1158
  }
1159
+ doConnectOrderbook() {
1160
+ if (this.destroyed || !this.orderbookUrl) return;
1161
+ const Impl = this.webSocketImpl ?? globalThis.WebSocket;
1162
+ if (!Impl) throw new Error("No WebSocket implementation available. Pass `webSocketImpl` (e.g. the `ws` package).");
1163
+ this.obConnecting = true;
1164
+ const ws = this.headers ? new Impl(this.orderbookUrl, { headers: this.headers }) : new Impl(this.orderbookUrl);
1165
+ this.obWs = ws;
1166
+ ws.onopen = () => {
1167
+ if (this.destroyed || this.obWs !== ws) return;
1168
+ this.obConnecting = false;
1169
+ this.resetOrderbookStaleTimer();
1170
+ };
1171
+ ws.onmessage = (event) => {
1172
+ if (this.destroyed || this.obWs !== ws) return;
1173
+ this.obLastMessageTime = Date.now();
1174
+ this.callbacks.onFrameReceived?.(this.obLastMessageTime);
1175
+ this.resetOrderbookStaleTimer();
1176
+ try {
1177
+ const raw = JSON.parse(event.data);
1178
+ this.handleOrderbookSocketMessage(raw);
1179
+ } catch {}
1180
+ };
1181
+ ws.onclose = (event) => {
1182
+ if (this.obWs !== ws) return;
1183
+ this.obWs = null;
1184
+ this.obConnecting = false;
1185
+ const wasConnected = this._obConnected;
1186
+ this._obConnected = false;
1187
+ this.cancelOrderbookStaleTimer();
1188
+ this.clearSnapshotRetries();
1189
+ if (wasConnected) this.callbacks.onOrderbookConnectionStateChange?.(false);
1190
+ const code = event?.code ?? 0;
1191
+ if (NON_RETRYABLE_CLOSE_CODES.has(code)) {
1192
+ this.obRejected = true;
1193
+ this.emitDiagnostic({
1194
+ type: "connection_rejected",
1195
+ code,
1196
+ reason: event?.reason ?? ""
1197
+ });
1198
+ return;
1199
+ }
1200
+ if (!this.destroyed && (this.activeOrderbookMarkets().length > 0 || this.pendingSubs.size > 0 || this.pendingResnapshots.size > 0)) this.scheduleOrderbookReconnect();
1201
+ };
1202
+ ws.onerror = () => {};
1203
+ }
1204
+ scheduleOrderbookReconnect() {
1205
+ this.cancelOrderbookReconnect();
1206
+ const step = Math.min(RECONNECT_BASE_MS * Math.pow(2, this.obReconnectAttempts), RECONNECT_MAX_MS);
1207
+ const delay = Math.round(step * (.5 + Math.random() * .5));
1208
+ this.obReconnectAttempts++;
1209
+ this.emitDiagnostic({
1210
+ type: "reconnect",
1211
+ attempt: this.obReconnectAttempts,
1212
+ delayMs: delay
1213
+ });
1214
+ this.obReconnectTimer = setTimeout(() => {
1215
+ this.obReconnectTimer = null;
1216
+ this.doConnectOrderbook();
1217
+ }, delay);
1218
+ }
1219
+ cancelOrderbookReconnect() {
1220
+ if (this.obReconnectTimer) {
1221
+ clearTimeout(this.obReconnectTimer);
1222
+ this.obReconnectTimer = null;
1223
+ }
1224
+ }
1225
+ resetOrderbookStaleTimer() {
1226
+ this.cancelOrderbookStaleTimer();
1227
+ this.obStaleTimer = setTimeout(() => {
1228
+ const age = Date.now() - this.obLastMessageTime;
1229
+ this.emitDiagnostic({
1230
+ type: "stale_connection",
1231
+ lastMessageAge: age
1232
+ });
1233
+ if (this.obWs) this.obWs.close();
1234
+ }, STALE_TIMEOUT_MS);
1235
+ }
1236
+ cancelOrderbookStaleTimer() {
1237
+ if (this.obStaleTimer) {
1238
+ clearTimeout(this.obStaleTimer);
1239
+ this.obStaleTimer = null;
1240
+ }
1241
+ }
1069
1242
  handleMessage(raw) {
1070
1243
  const type = raw.type;
1071
1244
  if (USER_EVENT_TYPES.has(type) && !this.acceptEvent(raw)) return;
@@ -1140,6 +1313,7 @@ var AggWebSocket = class {
1140
1313
  if (seq === this.catalogFrameSeq) this.catalogSentKey = null;
1141
1314
  for (const sub of [...this.catalogSubs]) if (sub.awaitingAck !== void 0 && sub.awaitingAck <= seq) sub.opts.onError?.(msg);
1142
1315
  }
1316
+ if (msg.code === "snapshot_unavailable") for (const outcomeId of msg.outcomeIds ?? []) this.queueSnapshotRetry(outcomeId);
1143
1317
  this.callbacks.onError?.(msg);
1144
1318
  break;
1145
1319
  }
@@ -1200,16 +1374,7 @@ var AggWebSocket = class {
1200
1374
  if (this.bookWatchdog && !this.watchdogTimer && !this.destroyed) this.watchdogTimer = setInterval(() => void this.checkSilentBooks(), this.bookWatchdog.checkIntervalMs ?? WATCHDOG_CHECK_INTERVAL_MS);
1201
1375
  this.callbacks.onConnectionStateChange?.(true);
1202
1376
  this.callbacks.onConnected?.(msg);
1203
- const orderbookSet = new Set(this.activeOrderbookMarkets());
1204
- const reconnectedAt = Date.now();
1205
- for (const id of orderbookSet) if (this.bookCheckedAt.has(id)) this.bookCheckedAt.set(id, reconnectedAt);
1206
- this.pendingSubs.forEach((id) => orderbookSet.add(id));
1207
- this.pendingFlushSubs.forEach((id) => orderbookSet.add(id));
1208
- const orderbookIds = Array.from(orderbookSet);
1209
- if (orderbookIds.length > 0) this.sendSubscribe(orderbookIds);
1210
- this.pendingSubs.clear();
1211
- this.pendingFlushSubs.clear();
1212
- this.pendingFlushUnsubs.clear();
1377
+ if (!this.orderbookUrl) this.flushOrderbookSubscriptions();
1213
1378
  const tradeSet = new Set(this.activeTradeMarkets());
1214
1379
  this.pendingTradeSubs.forEach((id) => tradeSet.add(id));
1215
1380
  this.pendingFlushTradeSubs.forEach((id) => tradeSet.add(id));
@@ -1222,13 +1387,7 @@ var AggWebSocket = class {
1222
1387
  this.pendingTradeSubs.clear();
1223
1388
  this.pendingFlushTradeSubs.clear();
1224
1389
  this.pendingFlushTradeUnsubs.clear();
1225
- const resnapshotIds = Array.from(this.pendingResnapshots);
1226
- if (resnapshotIds.length > 0) this.send({
1227
- action: "resnapshot",
1228
- channel: "orderbook",
1229
- outcomeIds: resnapshotIds
1230
- });
1231
- this.pendingResnapshots.clear();
1390
+ if (!this.orderbookUrl) this.flushOrderbookResnapshots();
1232
1391
  const arbSet = new Set(this.activeArbMarkets());
1233
1392
  this.pendingFlushArbSubs.forEach((id) => arbSet.add(id));
1234
1393
  const arbIds = Array.from(arbSet);
@@ -1248,6 +1407,89 @@ var AggWebSocket = class {
1248
1407
  this.catalogAckedSeq = this.catalogFrameSeq;
1249
1408
  if (this.catalogSubs.size > 0) this.sendCatalogSubscribe(...this.catalogSubs);
1250
1409
  }
1410
+ /** Resubscribe orderbooks on the socket that owns that channel. */
1411
+ flushOrderbookSubscriptions() {
1412
+ const orderbookSet = new Set(this.activeOrderbookMarkets());
1413
+ const reconnectedAt = Date.now();
1414
+ for (const id of orderbookSet) if (this.bookCheckedAt.has(id)) this.bookCheckedAt.set(id, reconnectedAt);
1415
+ this.pendingSubs.forEach((id) => orderbookSet.add(id));
1416
+ this.pendingFlushSubs.forEach((id) => orderbookSet.add(id));
1417
+ const orderbookIds = Array.from(orderbookSet);
1418
+ if (orderbookIds.length > 0) this.sendSubscribe(orderbookIds);
1419
+ this.pendingSubs.clear();
1420
+ this.pendingFlushSubs.clear();
1421
+ this.pendingFlushUnsubs.clear();
1422
+ }
1423
+ flushOrderbookResnapshots() {
1424
+ const resnapshotIds = Array.from(this.pendingResnapshots);
1425
+ if (resnapshotIds.length > 0) this.sendOrderbook({
1426
+ action: "resnapshot",
1427
+ channel: "orderbook",
1428
+ outcomeIds: resnapshotIds
1429
+ });
1430
+ this.pendingResnapshots.clear();
1431
+ }
1432
+ /**
1433
+ * The orderbook socket confirmed the app. It does not carry trades, arb,
1434
+ * catalog, or user events, and it does not change the primary connection flag.
1435
+ */
1436
+ handleOrderbookConnected() {
1437
+ this.obReconnectAttempts = 0;
1438
+ this._obConnected = true;
1439
+ this.callbacks.onOrderbookConnectionStateChange?.(true);
1440
+ if (this.bookWatchdog && !this.watchdogTimer && !this.destroyed) this.watchdogTimer = setInterval(() => void this.checkSilentBooks(), this.bookWatchdog.checkIntervalMs ?? WATCHDOG_CHECK_INTERVAL_MS);
1441
+ this.flushOrderbookSubscriptions();
1442
+ this.flushOrderbookResnapshots();
1443
+ }
1444
+ handleOrderbookSocketMessage(raw) {
1445
+ const type = raw.type;
1446
+ if ((type === "orderbook_snapshot" || type === "orderbook_delta") && !validMarketFrame(raw)) {
1447
+ this.callbacks.onError?.({
1448
+ type: "error",
1449
+ message: "Invalid market-data frame"
1450
+ });
1451
+ if (typeof raw.outcomeId === "string" && this.subs.has(raw.outcomeId)) this.requestResnapshot(raw.outcomeId);
1452
+ return;
1453
+ }
1454
+ switch (type) {
1455
+ case "orderbook_snapshot":
1456
+ this.handleSnapshot(raw);
1457
+ break;
1458
+ case "orderbook_delta":
1459
+ this.handleDelta(raw);
1460
+ break;
1461
+ case "heartbeat":
1462
+ this.handleHeartbeat(raw);
1463
+ break;
1464
+ case "subscribed":
1465
+ this.callbacks.onSubscribed?.(raw);
1466
+ break;
1467
+ case "unsubscribed":
1468
+ this.callbacks.onUnsubscribed?.(raw);
1469
+ break;
1470
+ case "connected":
1471
+ this.handleOrderbookConnected();
1472
+ break;
1473
+ case "market_resolved":
1474
+ this.handleMarketResolved(raw);
1475
+ break;
1476
+ case "error": {
1477
+ const msg = raw;
1478
+ if (msg.code === "snapshot_unavailable") for (const outcomeId of msg.outcomeIds ?? []) this.queueSnapshotRetry(outcomeId);
1479
+ this.callbacks.onError?.(msg);
1480
+ break;
1481
+ }
1482
+ case "stream_interrupted": {
1483
+ const msg = raw;
1484
+ this.emitDiagnostic({
1485
+ type: "stream_interrupted",
1486
+ channels: msg.channels
1487
+ });
1488
+ this.callbacks.onStreamInterrupted?.(msg);
1489
+ break;
1490
+ }
1491
+ }
1492
+ }
1251
1493
  /**
1252
1494
  * Track `eventId` per stream and drop duplicates a resume re-delivers.
1253
1495
  * App-stream frames go to `subscribeAppEvents` listeners only. Returns
@@ -1279,6 +1521,7 @@ var AggWebSocket = class {
1279
1521
  timestamp: raw.timestamp / 1e3
1280
1522
  };
1281
1523
  const book = snapshotToOrderbook(msg);
1524
+ this.snapshotRetries.delete(msg.outcomeId);
1282
1525
  this.books.set(msg.outcomeId, new IndexedOrderbook(book));
1283
1526
  this.bookCheckedAt.set(msg.outcomeId, Date.now());
1284
1527
  this.resyncing.delete(msg.outcomeId);
@@ -1385,6 +1628,7 @@ var AggWebSocket = class {
1385
1628
  timestamp: raw.timestamp / 1e3
1386
1629
  };
1387
1630
  this.books.delete(msg.outcomeId);
1631
+ this.snapshotRetries.delete(msg.outcomeId);
1388
1632
  this.bookCheckedAt.delete(msg.outcomeId);
1389
1633
  this.watchdogResnapshotted.delete(msg.outcomeId);
1390
1634
  this.pendingResnapshots.delete(msg.outcomeId);
@@ -1425,7 +1669,7 @@ var AggWebSocket = class {
1425
1669
  */
1426
1670
  async checkSilentBooks() {
1427
1671
  const opts = this.bookWatchdog;
1428
- if (!opts || !this._connected || this.watchdogAbort) return;
1672
+ if (!opts || !this.orderbookTransportReady() || this.watchdogAbort) return;
1429
1673
  const now = Date.now();
1430
1674
  if (now < this.watchdogPausedUntil) return;
1431
1675
  const silenceMs = opts.silenceMs ?? WATCHDOG_SILENCE_MS;
@@ -1480,7 +1724,7 @@ var AggWebSocket = class {
1480
1724
  this.watchdogResnapshotted.delete(outcomeId);
1481
1725
  this.books.delete(outcomeId);
1482
1726
  this.resyncing.add(outcomeId);
1483
- this.send({
1727
+ this.sendOrderbook({
1484
1728
  action: "unsubscribe",
1485
1729
  channel: "orderbook",
1486
1730
  outcomeIds: [outcomeId]
@@ -1501,11 +1745,56 @@ var AggWebSocket = class {
1501
1745
  if (this.watchdogAbort === abort) this.watchdogAbort = null;
1502
1746
  }
1503
1747
  }
1748
+ /** Retry only explicitly failed active books; never reconnect healthy sockets. */
1749
+ queueSnapshotRetry(outcomeId) {
1750
+ if (this.destroyed || !this.orderbookTransportReady() || !this.subs.get(outcomeId)?.refCount) return;
1751
+ const previous = this.snapshotRetries.get(outcomeId);
1752
+ if (previous && (Number.isFinite(previous.dueAt) || previous.attempts >= SNAPSHOT_RETRY_MAX_ATTEMPTS)) return;
1753
+ const attempts = (previous?.attempts ?? 0) + 1;
1754
+ const delay = SNAPSHOT_RETRY_TICK_MS * 2 ** (attempts - 1) * (.5 + Math.random() * .5);
1755
+ this.snapshotRetries.set(outcomeId, {
1756
+ attempts,
1757
+ dueAt: Date.now() + delay
1758
+ });
1759
+ this.scheduleSnapshotRetries();
1760
+ }
1761
+ scheduleSnapshotRetries() {
1762
+ if (this.snapshotRetryTimer || !this.orderbookTransportReady() || this.destroyed) return;
1763
+ let nextDueAt = Infinity;
1764
+ for (const retry of this.snapshotRetries.values()) nextDueAt = Math.min(nextDueAt, retry.dueAt);
1765
+ if (!Number.isFinite(nextDueAt)) return;
1766
+ const delay = Math.max(0, nextDueAt - Date.now(), this.snapshotRetryLastSentAt + SNAPSHOT_RETRY_TICK_MS - Date.now());
1767
+ this.snapshotRetryTimer = setTimeout(() => {
1768
+ this.snapshotRetryTimer = null;
1769
+ if (!this.orderbookTransportReady() || this.destroyed) return;
1770
+ const ids = [];
1771
+ for (const [id, retry] of [...this.snapshotRetries].sort((a, b) => a[1].dueAt - b[1].dueAt)) if (!this.subs.get(id)?.refCount) this.snapshotRetries.delete(id);
1772
+ else if (retry.dueAt <= Date.now() && ids.length < SNAPSHOT_RETRY_BATCH_SIZE) {
1773
+ retry.dueAt = Infinity;
1774
+ this.resyncing.add(id);
1775
+ ids.push(id);
1776
+ }
1777
+ if (ids.length) {
1778
+ this.snapshotRetryLastSentAt = Date.now();
1779
+ this.sendOrderbook({
1780
+ action: "resnapshot",
1781
+ channel: "orderbook",
1782
+ outcomeIds: ids
1783
+ });
1784
+ }
1785
+ if ([...this.snapshotRetries.values()].some((retry) => Number.isFinite(retry.dueAt))) this.scheduleSnapshotRetries();
1786
+ }, Math.ceil(delay));
1787
+ }
1788
+ clearSnapshotRetries() {
1789
+ if (this.snapshotRetryTimer) clearTimeout(this.snapshotRetryTimer);
1790
+ this.snapshotRetryTimer = null;
1791
+ this.snapshotRetries.clear();
1792
+ }
1504
1793
  requestResnapshot(outcomeId, force = false) {
1505
1794
  if (this.resyncing.has(outcomeId) && !force) return;
1506
1795
  this.resyncing.add(outcomeId);
1507
- if (this._connected) {
1508
- this.send({
1796
+ if (this.orderbookTransportReady()) {
1797
+ this.sendOrderbook({
1509
1798
  action: "resnapshot",
1510
1799
  channel: "orderbook",
1511
1800
  outcomeIds: [outcomeId]
@@ -1514,7 +1803,7 @@ var AggWebSocket = class {
1514
1803
  return;
1515
1804
  }
1516
1805
  this.pendingResnapshots.add(outcomeId);
1517
- this.ensureConnected();
1806
+ this.ensureOrderbookTransport();
1518
1807
  }
1519
1808
  /**
1520
1809
  * Schedule a microtask that flushes queued subscribe/unsubscribe frames.
@@ -1526,35 +1815,52 @@ var AggWebSocket = class {
1526
1815
  this.flushScheduled = true;
1527
1816
  queueMicrotask(() => {
1528
1817
  this.flushScheduled = false;
1529
- if (!this._connected) {
1818
+ if (!this.orderbookUrl) {
1819
+ if (!this._connected) {
1820
+ this.pendingFlushSubs.clear();
1821
+ this.pendingFlushUnsubs.clear();
1822
+ this.pendingFlushTradeSubs.clear();
1823
+ this.pendingFlushTradeUnsubs.clear();
1824
+ this.pendingFlushArbSubs.clear();
1825
+ this.pendingFlushArbUnsubs.clear();
1826
+ return;
1827
+ }
1828
+ this.flushPending();
1829
+ return;
1830
+ }
1831
+ if (!this._obConnected) {
1530
1832
  this.pendingFlushSubs.clear();
1531
1833
  this.pendingFlushUnsubs.clear();
1834
+ }
1835
+ if (!this._connected) {
1532
1836
  this.pendingFlushTradeSubs.clear();
1533
1837
  this.pendingFlushTradeUnsubs.clear();
1534
1838
  this.pendingFlushArbSubs.clear();
1535
1839
  this.pendingFlushArbUnsubs.clear();
1536
- return;
1537
1840
  }
1538
- this.flushPending();
1841
+ if (this._obConnected || this._connected) this.flushPending();
1539
1842
  });
1540
1843
  }
1541
1844
  flushPending() {
1542
- for (const id of [...this.pendingFlushSubs]) if (this.pendingFlushUnsubs.has(id)) {
1543
- this.pendingFlushSubs.delete(id);
1544
- this.pendingFlushUnsubs.delete(id);
1545
- }
1546
- if (this.pendingFlushSubs.size > 0) {
1547
- this.sendSubscribe(Array.from(this.pendingFlushSubs));
1548
- this.pendingFlushSubs.clear();
1549
- }
1550
- if (this.pendingFlushUnsubs.size > 0) {
1551
- this.send({
1552
- action: "unsubscribe",
1553
- channel: "orderbook",
1554
- outcomeIds: Array.from(this.pendingFlushUnsubs)
1555
- });
1556
- this.pendingFlushUnsubs.clear();
1845
+ if (!this.orderbookUrl || this._obConnected) {
1846
+ for (const id of [...this.pendingFlushSubs]) if (this.pendingFlushUnsubs.has(id)) {
1847
+ this.pendingFlushSubs.delete(id);
1848
+ this.pendingFlushUnsubs.delete(id);
1849
+ }
1850
+ if (this.pendingFlushSubs.size > 0) {
1851
+ this.sendSubscribe(Array.from(this.pendingFlushSubs));
1852
+ this.pendingFlushSubs.clear();
1853
+ }
1854
+ if (this.pendingFlushUnsubs.size > 0) {
1855
+ this.sendOrderbook({
1856
+ action: "unsubscribe",
1857
+ channel: "orderbook",
1858
+ outcomeIds: Array.from(this.pendingFlushUnsubs)
1859
+ });
1860
+ this.pendingFlushUnsubs.clear();
1861
+ }
1557
1862
  }
1863
+ if (!this._connected) return;
1558
1864
  for (const id of [...this.pendingFlushTradeSubs]) if (this.pendingFlushTradeUnsubs.has(id)) {
1559
1865
  this.pendingFlushTradeSubs.delete(id);
1560
1866
  this.pendingFlushTradeUnsubs.delete(id);
@@ -1597,25 +1903,33 @@ var AggWebSocket = class {
1597
1903
  }
1598
1904
  }
1599
1905
  sendSubscribe(outcomeIds) {
1600
- this.send({
1906
+ const msg = {
1601
1907
  action: "subscribe",
1602
1908
  channel: "orderbook",
1603
1909
  outcomeIds
1604
- });
1910
+ };
1911
+ if (this.orderbookLatency === "realtime") msg.latency = "realtime";
1912
+ this.sendOrderbook(msg);
1913
+ }
1914
+ sendOrderbook(msg) {
1915
+ this.sendOn(this.orderbookUrl ? this.obWs : this.ws, msg);
1605
1916
  }
1606
1917
  send(msg) {
1607
- if (this.ws?.readyState !== WS_OPEN) return;
1918
+ this.sendOn(this.ws, msg);
1919
+ }
1920
+ sendOn(ws, msg) {
1921
+ if (ws?.readyState !== WS_OPEN) return;
1608
1922
  const m = msg;
1609
1923
  const key = m.outcomeIds ? "outcomeIds" : m.marketIds ? "marketIds" : void 0;
1610
1924
  const ids = key ? m[key] : void 0;
1611
1925
  if (key && ids && ids.length > MAX_IDS_PER_FRAME) {
1612
- for (let i = 0; i < ids.length; i += MAX_IDS_PER_FRAME) this.ws.send(JSON.stringify({
1926
+ for (let i = 0; i < ids.length; i += MAX_IDS_PER_FRAME) ws.send(JSON.stringify({
1613
1927
  ...m,
1614
1928
  [key]: ids.slice(i, i + MAX_IDS_PER_FRAME)
1615
1929
  }));
1616
1930
  return;
1617
1931
  }
1618
- this.ws.send(JSON.stringify(msg));
1932
+ ws.send(JSON.stringify(msg));
1619
1933
  }
1620
1934
  activeOrderbookMarkets() {
1621
1935
  const markets = [];
@@ -1679,4 +1993,4 @@ function compareEventIds(a, b) {
1679
1993
  //#endregion
1680
1994
  export { loadImmutableHistory as a, orderbookCrc32 as c, snapshotToOrderbook as i, projectedOrderbookChecksum as l, applyOrderbookDelta as n, decodeChartHistory as o, computeChecksum as r, readChartHistoryText as s, AggWebSocket as t };
1681
1995
 
1682
- //# sourceMappingURL=agg-websocket-C2neMzGT.mjs.map
1996
+ //# sourceMappingURL=agg-websocket-C9ELOadG.mjs.map