@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.
- package/README.md +28 -24
- package/dist/{agg-websocket-CsURzWkQ.d.mts → agg-websocket-BRWnvGwU.d.mts} +139 -17
- package/dist/agg-websocket-BRWnvGwU.d.mts.map +1 -0
- package/dist/{agg-websocket-CsURzWkQ.d.ts → agg-websocket-BRWnvGwU.d.ts} +139 -17
- package/dist/agg-websocket-BRWnvGwU.d.ts.map +1 -0
- package/dist/{agg-websocket-C2neMzGT.mjs → agg-websocket-C9ELOadG.mjs} +377 -63
- package/dist/agg-websocket-C9ELOadG.mjs.map +1 -0
- package/dist/{agg-websocket-MYZiMRXn.js → agg-websocket-Cq4E0T0f.js} +376 -62
- package/dist/browser.d.mts +1 -1
- package/dist/browser.d.mts.map +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -1
- package/dist/browser.mjs +2 -1
- package/dist/browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +51 -10
- package/dist/index.mjs +49 -11
- package/dist/index.mjs.map +1 -1
- package/dist/market-data.d.mts +1 -1
- package/dist/market-data.d.ts +1 -1
- package/dist/market-data.js +2 -1
- package/dist/market-data.mjs +2 -1
- package/dist/market-data.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/agg-websocket-C2neMzGT.mjs.map +0 -1
- package/dist/agg-websocket-CsURzWkQ.d.mts.map +0 -1
- package/dist/agg-websocket-CsURzWkQ.d.ts.map +0 -1
|
@@ -472,9 +472,12 @@ var IndexedOrderbook = class {
|
|
|
472
472
|
/**
|
|
473
473
|
* Agg platform WebSocket client.
|
|
474
474
|
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
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.
|
|
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)
|
|
723
|
-
if (this.
|
|
724
|
-
this.
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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.
|
|
783
|
-
if (this.
|
|
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.
|
|
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.
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
1508
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1543
|
-
this.pendingFlushSubs.
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
this.
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
1932
|
+
ws.send(JSON.stringify(msg));
|
|
1619
1933
|
}
|
|
1620
1934
|
activeOrderbookMarkets() {
|
|
1621
1935
|
const markets = [];
|