@agg-build/sdk 4.25.0 → 4.27.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.
Files changed (42) hide show
  1. package/dist/agg-websocket-BstVWtLA.d.mts +6004 -0
  2. package/dist/agg-websocket-BstVWtLA.d.mts.map +1 -0
  3. package/dist/agg-websocket-BstVWtLA.d.ts +6004 -0
  4. package/dist/agg-websocket-BstVWtLA.d.ts.map +1 -0
  5. package/dist/agg-websocket-CK2iXfti.mjs +1427 -0
  6. package/dist/agg-websocket-CK2iXfti.mjs.map +1 -0
  7. package/dist/agg-websocket-DJbIfxKb.js +1478 -0
  8. package/dist/browser.d.mts +67 -66
  9. package/dist/browser.d.mts.map +1 -0
  10. package/dist/browser.d.ts +67 -66
  11. package/dist/browser.d.ts.map +1 -0
  12. package/dist/browser.js +720 -2047
  13. package/dist/browser.mjs +423 -799
  14. package/dist/browser.mjs.map +1 -0
  15. package/dist/candle-builder-BNewIe9K.mjs +148 -0
  16. package/dist/candle-builder-BNewIe9K.mjs.map +1 -0
  17. package/dist/candle-builder-BULYdxB4.js +151 -0
  18. package/dist/index.d.mts +2 -1342
  19. package/dist/index.d.ts +2 -1342
  20. package/dist/index.js +2794 -4656
  21. package/dist/index.mjs +2788 -2486
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/market-data.d.mts +6 -6
  24. package/dist/market-data.d.mts.map +1 -0
  25. package/dist/market-data.d.ts +6 -6
  26. package/dist/market-data.d.ts.map +1 -0
  27. package/dist/market-data.js +314 -1774
  28. package/dist/market-data.mjs +314 -350
  29. package/dist/market-data.mjs.map +1 -0
  30. package/dist/server.d.mts +235 -234
  31. package/dist/server.d.mts.map +1 -0
  32. package/dist/server.d.ts +235 -234
  33. package/dist/server.d.ts.map +1 -0
  34. package/dist/server.js +168 -296
  35. package/dist/server.mjs +163 -234
  36. package/dist/server.mjs.map +1 -0
  37. package/package.json +4 -4
  38. package/dist/agg-websocket-Bxt9PzTz.d.mts +0 -4479
  39. package/dist/agg-websocket-Bxt9PzTz.d.ts +0 -4479
  40. package/dist/chunk-QXBVA5TW.mjs +0 -2073
  41. package/dist/chunk-VD5UTAVD.mjs +0 -158
  42. package/dist/chunk-WNQUEZJF.mjs +0 -58
@@ -0,0 +1,1478 @@
1
+ //#region ../common/src/orderbook-checksum.ts
2
+ /** IEEE CRC32 used by the existing orderbook protocol. */
3
+ function orderbookCrc32(value) {
4
+ let crc = 4294967295;
5
+ for (let i = 0; i < value.length; i++) {
6
+ crc ^= value.charCodeAt(i);
7
+ for (let bit = 0; bit < 8; bit++) crc = crc >>> 1 ^ (crc & 1 ? 3988292384 : 0);
8
+ }
9
+ return (crc ^ 4294967295) >>> 0;
10
+ }
11
+ /** Checksum of exactly the supplied levels, independent of depth/venue attribution. */
12
+ function projectedOrderbookChecksum(book) {
13
+ let result = 0;
14
+ for (const level of book.bids) result ^= orderbookCrc32(`B${level.price}:${level.size}`);
15
+ for (const level of book.asks) result ^= orderbookCrc32(`A${level.price}:${level.size}`);
16
+ return result >>> 0;
17
+ }
18
+ //#endregion
19
+ //#region src/market-data/history-decoder.ts
20
+ /** Decode bounded dynamic history where processing lives; retain milliseconds and optional mean. */
21
+ function decodeChartHistory(payload) {
22
+ if (payload.length > 1048576) throw new Error("Chart history exceeds decode budget");
23
+ const body = JSON.parse(payload);
24
+ if (!body || !Array.isArray(body.data) || body.data.length > 5e3 || typeof body.venueMarketOutcomeId !== "string" || typeof body.venue !== "string" || ![
25
+ "1m",
26
+ "5m",
27
+ "1h",
28
+ "1d"
29
+ ].includes(body.resolution) || !body.meta) throw new Error("Invalid chart history response");
30
+ const price = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
31
+ for (const bar of body.data) if (!Number.isSafeInteger(bar.t) || bar.t < 0 || ![
32
+ bar.o,
33
+ bar.h,
34
+ bar.l,
35
+ bar.c
36
+ ].every(price) || bar.l > Math.min(bar.o, bar.c) || bar.h < Math.max(bar.o, bar.c) || bar.v !== null && (!Number.isFinite(bar.v) || bar.v < 0) || bar.mean !== void 0 && !price(bar.mean)) throw new Error("Invalid chart history candle");
37
+ const data = [...body.data].sort((a, b) => a.t - b.t);
38
+ if (data.some((bar, index) => index > 0 && data[index - 1].t === bar.t)) throw new Error("Duplicate chart history timestamp");
39
+ return {
40
+ ...body,
41
+ data
42
+ };
43
+ }
44
+ /** Bound received bytes before JSON parsing, including chunked responses without Content-Length. */
45
+ async function readChartHistoryText(response) {
46
+ const maxBytes = 1048576;
47
+ if (Number(response.headers.get("content-length")) > maxBytes) {
48
+ await response.body?.cancel();
49
+ throw new Error("Chart history exceeds decode budget");
50
+ }
51
+ if (!response.body?.getReader) {
52
+ const text = await response.text();
53
+ if (new TextEncoder().encode(text).byteLength > maxBytes) throw new Error("Chart history exceeds decode budget");
54
+ return text;
55
+ }
56
+ const reader = response.body.getReader();
57
+ const decoder = new TextDecoder();
58
+ let size = 0;
59
+ let text = "";
60
+ try {
61
+ while (true) {
62
+ const { value, done } = await reader.read();
63
+ if (done) return text + decoder.decode();
64
+ size += value.byteLength;
65
+ if (size > maxBytes) {
66
+ await reader.cancel();
67
+ throw new Error("Chart history exceeds decode budget");
68
+ }
69
+ text += decoder.decode(value, { stream: true });
70
+ }
71
+ } finally {
72
+ reader.releaseLock();
73
+ }
74
+ }
75
+ //#endregion
76
+ //#region src/market-data/validate.ts
77
+ /** Bounds work before processing untrusted market frames. Protocol semantics remain in AggWebSocket. */
78
+ const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
79
+ const finite = (value) => typeof value === "number" && Number.isFinite(value);
80
+ const nullable = (value) => value === null || finite(value);
81
+ const levels = (value, attributed) => Array.isArray(value) && value.length <= 1e4 && value.every((level) => Array.isArray(level) && finite(level[0]) && level[0] >= 0 && finite(level[1]) && level[1] >= 0 && (!attributed || object(level[2]) && Object.values(level[2]).every((size) => finite(size) && size >= 0)));
82
+ function validMarketFrame(frame) {
83
+ if (typeof frame.outcomeId !== "string" || !frame.outcomeId || !finite(frame.timestamp) || frame.timestamp < 0) return false;
84
+ if (frame.type === "trade") return finite(frame.price) && frame.price >= 0 && finite(frame.size) && frame.size >= 0 && typeof frame.venue === "string" && (frame.side === "buy" || frame.side === "sell");
85
+ if (!Number.isSafeInteger(frame.seq) || frame.seq < 0 || !Number.isInteger(frame.checksum) || frame.checksum < 0 || frame.checksum > 4294967295 || !nullable(frame.midpoint) || !nullable(frame.spread) || !object(frame.venues)) return false;
86
+ for (const info of Object.values(frame.venues)) if (!object(info) || !nullable(info.bestBid) || !nullable(info.bestAsk)) return false;
87
+ const snapshot = frame.type === "orderbook_snapshot";
88
+ if (!levels(frame[snapshot ? "bids" : "bidChanges"], true) || !levels(frame[snapshot ? "asks" : "askChanges"], true)) return false;
89
+ const books = frame[snapshot ? "venueOrderbooks" : "venueDeltaBooks"];
90
+ return object(books) && Object.values(books).every((book) => object(book) && levels(book[snapshot ? "bids" : "bidChanges"], false) && levels(book[snapshot ? "asks" : "askChanges"], false));
91
+ }
92
+ //#endregion
93
+ //#region src/market-history.ts
94
+ async function readBoundedBody(response, signal) {
95
+ if (!response.body) throw new Error("Missing history body");
96
+ const reader = response.body.getReader();
97
+ const chunks = [];
98
+ let size = 0;
99
+ const abort = () => {
100
+ reader.cancel(signal.reason).catch(() => void 0);
101
+ };
102
+ signal.addEventListener("abort", abort, { once: true });
103
+ try {
104
+ while (true) {
105
+ if (signal.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("History request aborted");
106
+ const { done, value } = await reader.read();
107
+ if (signal.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("History request aborted");
108
+ if (done) break;
109
+ size += value.byteLength;
110
+ if (size > 262144) {
111
+ await reader.cancel();
112
+ throw new Error("Oversized history object");
113
+ }
114
+ chunks.push(value);
115
+ }
116
+ const bytes = new Uint8Array(size);
117
+ let offset = 0;
118
+ for (const chunk of chunks) {
119
+ bytes.set(chunk, offset);
120
+ offset += chunk.byteLength;
121
+ }
122
+ return bytes;
123
+ } finally {
124
+ signal.removeEventListener("abort", abort);
125
+ reader.releaseLock();
126
+ }
127
+ }
128
+ /** Download immutable CDN objects directly. Safe to run in the browser Worker or Node. */
129
+ async function loadImmutableHistory({ manifest, fetch: fetcher = globalThis.fetch, signal, digest }) {
130
+ if (manifest.version !== 1 || ![
131
+ "venue-ohlcv-v1",
132
+ "polymarket-price-samples-v1",
133
+ "kalshi-ohlcv-quote-fallback-v2"
134
+ ].includes(manifest.semantics) || manifest.chunks.length > 8 || manifest.chunks.reduce((n, c) => n + c.points, 0) > 4096 || !manifest.meta.complete) throw new Error("History manifest is incomplete or unsupported");
135
+ const hash = digest ?? (async (bytes) => {
136
+ if (!globalThis.crypto?.subtle) throw new Error("History integrity requires WebCrypto or an injected SHA-256 implementation");
137
+ const value = await globalThis.crypto.subtle.digest("SHA-256", bytes);
138
+ return Array.from(new Uint8Array(value), (b) => b.toString(16).padStart(2, "0")).join("");
139
+ });
140
+ const results = new Array(manifest.chunks.length);
141
+ let next = 0;
142
+ const controller = new AbortController();
143
+ const deadline = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("History download budget exhausted")), 2e3);
144
+ const abort = () => controller.abort(signal?.reason);
145
+ signal?.addEventListener("abort", abort, { once: true });
146
+ if (signal?.aborted) abort();
147
+ try {
148
+ await Promise.all(Array.from({ length: Math.min(2, manifest.chunks.length) }, async () => {
149
+ while (next < manifest.chunks.length) {
150
+ if (controller.signal.aborted) throw controller.signal.reason ?? /* @__PURE__ */ new Error("History request aborted");
151
+ const index = next++;
152
+ const chunk = manifest.chunks[index];
153
+ if (chunk.points < 0 || chunk.points > 512 || chunk.from >= chunk.to || new URL(chunk.url).protocol !== "https:") throw new Error("Invalid history reference");
154
+ const response = await fetcher(chunk.url, {
155
+ signal: controller.signal,
156
+ credentials: "omit"
157
+ });
158
+ if (!response.ok) throw new Error(`History download failed (${response.status})`);
159
+ if (Number(response.headers.get("content-length")) > 262144) throw new Error("Oversized history object");
160
+ const bytes = await readBoundedBody(response, controller.signal);
161
+ if (bytes.byteLength > 262144 || await hash(bytes) !== chunk.sha256) throw new Error("History integrity mismatch");
162
+ const object = JSON.parse(new TextDecoder().decode(bytes));
163
+ if (object.version !== 1 || object.venueMarketOutcomeId !== manifest.venueMarketOutcomeId || object.venue !== manifest.venue || object.resolution !== manifest.resolution || object.semantics !== manifest.semantics || object.from !== chunk.from || object.to !== chunk.to || !Array.isArray(object.data) || object.data.length !== chunk.points) throw new Error("History identity mismatch");
164
+ let previous = -Infinity;
165
+ for (const bar of object.data) {
166
+ if (!Number.isSafeInteger(bar.t) || bar.t <= previous || bar.t < chunk.from || bar.t >= chunk.to || ![
167
+ bar.o,
168
+ bar.h,
169
+ bar.l,
170
+ bar.c
171
+ ].every((v) => Number.isFinite(v) && v >= 0 && v <= 1) || bar.l > Math.min(bar.o, bar.c) || bar.h < Math.max(bar.o, bar.c) || bar.v !== null && (!Number.isFinite(bar.v) || bar.v < 0) || bar.mean !== void 0 && (!Number.isFinite(bar.mean) || bar.mean < 0 || bar.mean > 1)) throw new Error("Invalid history candle");
172
+ previous = bar.t;
173
+ }
174
+ results[index] = object.data;
175
+ }
176
+ }));
177
+ const bars = results.flat().filter((b) => b.t >= manifest.from && b.t < manifest.to).sort((a, b) => a.t - b.t);
178
+ if (bars.some((b, i) => i > 0 && bars[i - 1].t === b.t)) throw new Error("Overlapping history chunks");
179
+ return bars;
180
+ } catch (error) {
181
+ controller.abort(error);
182
+ throw error;
183
+ } finally {
184
+ clearTimeout(deadline);
185
+ signal?.removeEventListener("abort", abort);
186
+ }
187
+ }
188
+ //#endregion
189
+ //#region src/orderbook-utils.ts
190
+ const PRICE_KEY_SCALE = 1e9;
191
+ /**
192
+ * Compute the XOR-based checksum of an orderbook.
193
+ * Matches the Rust engine's checksum: XOR of crc32("B{price}:{size}") for each
194
+ * bid and crc32("A{price}:{size}") for each ask, independently hashed then XOR'd.
195
+ */
196
+ function computeChecksum(book) {
197
+ return projectedOrderbookChecksum(book);
198
+ }
199
+ function attributedToLevel(wire) {
200
+ return {
201
+ price: wire[0],
202
+ size: wire[1],
203
+ venues: { ...wire[2] }
204
+ };
205
+ }
206
+ function venueLevelToLevel(wire) {
207
+ return {
208
+ price: wire[0],
209
+ size: wire[1]
210
+ };
211
+ }
212
+ function convertVenueBooks(raw) {
213
+ const result = {};
214
+ for (const [venue, book] of Object.entries(raw)) result[venue] = {
215
+ bids: book.bids.map(venueLevelToLevel),
216
+ asks: book.asks.map(venueLevelToLevel)
217
+ };
218
+ return result;
219
+ }
220
+ /**
221
+ * Convert a WS snapshot message into local OrderbookState.
222
+ * Timestamp is expected to already be in seconds (converted by AggWebSocket).
223
+ */
224
+ function snapshotToOrderbook(snapshot) {
225
+ return {
226
+ marketId: snapshot.outcomeId,
227
+ outcomeId: snapshot.outcomeId,
228
+ bids: snapshot.bids.map(attributedToLevel),
229
+ asks: snapshot.asks.map(attributedToLevel),
230
+ venueOrderbooks: convertVenueBooks(snapshot.venueOrderbooks),
231
+ venues: { ...snapshot.venues },
232
+ midpoint: snapshot.midpoint,
233
+ spread: snapshot.spread,
234
+ seq: snapshot.seq,
235
+ checksum: snapshot.checksum,
236
+ timestamp: snapshot.timestamp
237
+ };
238
+ }
239
+ /**
240
+ * Apply a WS delta to an existing OrderbookState, returning a new state.
241
+ * Bids sorted descending, asks ascending (maintained invariant).
242
+ */
243
+ function applyOrderbookDelta(state, delta) {
244
+ const bids = applyChanges([...state.bids], delta.bidChanges, "desc");
245
+ const asks = applyChanges([...state.asks], delta.askChanges, "asc");
246
+ const impliedBidChangesByVenue = deriveVenueChangesFromAttributedChanges(state.bids, delta.bidChanges);
247
+ const impliedAskChangesByVenue = deriveVenueChangesFromAttributedChanges(state.asks, delta.askChanges);
248
+ const venueOrderbooks = { ...state.venueOrderbooks };
249
+ const venuesWithDelta = /* @__PURE__ */ new Set([
250
+ ...Object.keys(delta.venueDeltaBooks),
251
+ ...Object.keys(impliedBidChangesByVenue),
252
+ ...Object.keys(impliedAskChangesByVenue)
253
+ ]);
254
+ for (const venue of venuesWithDelta) {
255
+ const explicitChanges = delta.venueDeltaBooks[venue];
256
+ const bidChanges = mergeVenueLevelChanges(explicitChanges?.bidChanges, impliedBidChangesByVenue[venue]);
257
+ const askChanges = mergeVenueLevelChanges(explicitChanges?.askChanges, impliedAskChangesByVenue[venue]);
258
+ if (bidChanges.length === 0 && askChanges.length === 0) continue;
259
+ const existing = venueOrderbooks[venue] ?? {
260
+ bids: [],
261
+ asks: []
262
+ };
263
+ venueOrderbooks[venue] = {
264
+ bids: applyVenueChanges([...existing.bids], bidChanges, "desc"),
265
+ asks: applyVenueChanges([...existing.asks], askChanges, "asc")
266
+ };
267
+ }
268
+ return {
269
+ marketId: state.marketId,
270
+ outcomeId: state.outcomeId ?? delta.outcomeId,
271
+ bids,
272
+ asks,
273
+ venueOrderbooks,
274
+ venues: {
275
+ ...state.venues,
276
+ ...delta.venues
277
+ },
278
+ midpoint: delta.midpoint,
279
+ spread: delta.spread,
280
+ seq: delta.seq,
281
+ checksum: delta.checksum,
282
+ timestamp: delta.timestamp
283
+ };
284
+ }
285
+ function toPriceKey(price) {
286
+ return String(Math.round(price * PRICE_KEY_SCALE));
287
+ }
288
+ function applyChanges(levels, changes, sort) {
289
+ const levelsByPrice = new Map(levels.map((level) => [toPriceKey(level.price), level]));
290
+ for (const [price, size, venues] of changes) {
291
+ const key = toPriceKey(price);
292
+ if (size === 0) {
293
+ levelsByPrice.delete(key);
294
+ continue;
295
+ }
296
+ levelsByPrice.set(key, {
297
+ price,
298
+ size,
299
+ venues: { ...venues ?? {} }
300
+ });
301
+ }
302
+ levels = [...levelsByPrice.values()];
303
+ levels.sort(sort === "desc" ? (a, b) => b.price - a.price : (a, b) => a.price - b.price);
304
+ return levels;
305
+ }
306
+ function applyVenueChanges(levels, changes, sort) {
307
+ const levelsByPrice = new Map(levels.map((level) => [toPriceKey(level.price), level]));
308
+ for (const [price, size] of changes) {
309
+ const key = toPriceKey(price);
310
+ if (size === 0) {
311
+ levelsByPrice.delete(key);
312
+ continue;
313
+ }
314
+ levelsByPrice.set(key, {
315
+ price,
316
+ size
317
+ });
318
+ }
319
+ levels = [...levelsByPrice.values()];
320
+ levels.sort(sort === "desc" ? (a, b) => b.price - a.price : (a, b) => a.price - b.price);
321
+ return levels;
322
+ }
323
+ function mergeVenueLevelChanges(primary, fallback) {
324
+ if (!primary?.length) return fallback ? [...fallback] : [];
325
+ if (!fallback?.length) return [...primary];
326
+ const changesByPrice = /* @__PURE__ */ new Map();
327
+ for (const change of fallback) changesByPrice.set(toPriceKey(change[0]), change);
328
+ for (const change of primary) changesByPrice.set(toPriceKey(change[0]), change);
329
+ return [...changesByPrice.values()];
330
+ }
331
+ function deriveVenueChangesFromAttributedChanges(previousLevels, changes) {
332
+ const previousLevelsByPrice = new Map(previousLevels.map((level) => [toPriceKey(level.price), level]));
333
+ const venueChanges = {};
334
+ for (const [price, size, nextVenuesRaw] of changes) {
335
+ const key = toPriceKey(price);
336
+ const previousVenues = previousLevelsByPrice.get(key)?.venues ?? {};
337
+ const nextVenues = nextVenuesRaw ?? {};
338
+ const affectedVenues = /* @__PURE__ */ new Set([...Object.keys(previousVenues), ...Object.keys(nextVenues)]);
339
+ for (const venue of affectedVenues) {
340
+ const nextSize = Number(nextVenues[venue] ?? 0);
341
+ if (!Number.isFinite(nextSize)) continue;
342
+ if (!venueChanges[venue]) venueChanges[venue] = [];
343
+ venueChanges[venue].push([price, nextSize]);
344
+ }
345
+ if (size === 0) {
346
+ previousLevelsByPrice.delete(key);
347
+ continue;
348
+ }
349
+ previousLevelsByPrice.set(key, {
350
+ price,
351
+ size,
352
+ venues: { ...nextVenues }
353
+ });
354
+ }
355
+ return venueChanges;
356
+ }
357
+ /** Private mutable representation for ordered feeds; immutable views are built only on demand. */
358
+ var IndexedOrderbook = class {
359
+ constructor(state) {
360
+ this.venueBooks = /* @__PURE__ */ new Map();
361
+ this.bids = new Map(state.bids.map((level) => [toPriceKey(level.price), level]));
362
+ this.asks = new Map(state.asks.map((level) => [toPriceKey(level.price), level]));
363
+ for (const [venue, book] of Object.entries(state.venueOrderbooks)) this.venueBooks.set(venue, {
364
+ bids: new Map(book.bids.map((level) => [toPriceKey(level.price), level])),
365
+ asks: new Map(book.asks.map((level) => [toPriceKey(level.price), level]))
366
+ });
367
+ const { bids: _bids, asks: _asks, venueOrderbooks: _venueBooks, ...metadata } = state;
368
+ this.metadata = metadata;
369
+ this.cached = state;
370
+ this.checksumValue = computeChecksum(state);
371
+ }
372
+ get seq() {
373
+ return this.metadata.seq;
374
+ }
375
+ get checksum() {
376
+ return this.checksumValue;
377
+ }
378
+ /** Verify before committing, including duplicate-price changes within a frame. */
379
+ apply(delta) {
380
+ let checksum = this.checksumValue;
381
+ for (const [levels, changes, side] of [[
382
+ this.bids,
383
+ delta.bidChanges,
384
+ "B"
385
+ ], [
386
+ this.asks,
387
+ delta.askChanges,
388
+ "A"
389
+ ]]) {
390
+ const changed = /* @__PURE__ */ new Map();
391
+ for (const [price, size, venues] of changes) {
392
+ const key = toPriceKey(price);
393
+ const previous = changed.has(key) ? changed.get(key) : levels.get(key);
394
+ if (previous) checksum ^= orderbookCrc32(`${side}${previous.price}:${previous.size}`);
395
+ if (size !== 0) checksum ^= orderbookCrc32(`${side}${price}:${size}`);
396
+ changed.set(key, size === 0 ? void 0 : {
397
+ price,
398
+ size,
399
+ venues: { ...venues }
400
+ });
401
+ }
402
+ }
403
+ checksum >>>= 0;
404
+ if (checksum !== delta.checksum) return checksum;
405
+ const previousBids = delta.bidChanges.flatMap(([price]) => this.bids.get(toPriceKey(price)) ?? []);
406
+ const previousAsks = delta.askChanges.flatMap(([price]) => this.asks.get(toPriceKey(price)) ?? []);
407
+ const impliedBids = deriveVenueChangesFromAttributedChanges(previousBids, delta.bidChanges);
408
+ const impliedAsks = deriveVenueChangesFromAttributedChanges(previousAsks, delta.askChanges);
409
+ for (const venue of /* @__PURE__ */ new Set([
410
+ ...Object.keys(delta.venueDeltaBooks),
411
+ ...Object.keys(impliedBids),
412
+ ...Object.keys(impliedAsks)
413
+ ])) {
414
+ let book = this.venueBooks.get(venue);
415
+ if (!book) {
416
+ book = {
417
+ bids: /* @__PURE__ */ new Map(),
418
+ asks: /* @__PURE__ */ new Map()
419
+ };
420
+ this.venueBooks.set(venue, book);
421
+ }
422
+ const explicit = delta.venueDeltaBooks[venue];
423
+ for (const [levels, changes] of [[book.bids, mergeVenueLevelChanges(explicit?.bidChanges, impliedBids[venue])], [book.asks, mergeVenueLevelChanges(explicit?.askChanges, impliedAsks[venue])]]) for (const [price, size] of changes) if (size === 0) levels.delete(toPriceKey(price));
424
+ else levels.set(toPriceKey(price), {
425
+ price,
426
+ size
427
+ });
428
+ }
429
+ for (const [levels, changes] of [[this.bids, delta.bidChanges], [this.asks, delta.askChanges]]) for (const [price, size, venues] of changes) if (size === 0) levels.delete(toPriceKey(price));
430
+ else levels.set(toPriceKey(price), {
431
+ price,
432
+ size,
433
+ venues: { ...venues }
434
+ });
435
+ this.metadata = {
436
+ ...this.metadata,
437
+ outcomeId: this.metadata.outcomeId ?? delta.outcomeId,
438
+ venues: {
439
+ ...this.metadata.venues,
440
+ ...delta.venues
441
+ },
442
+ midpoint: delta.midpoint,
443
+ spread: delta.spread,
444
+ seq: delta.seq,
445
+ checksum: delta.checksum,
446
+ timestamp: delta.timestamp
447
+ };
448
+ this.checksumValue = checksum;
449
+ this.cached = null;
450
+ return checksum;
451
+ }
452
+ view() {
453
+ if (this.cached) return this.cached;
454
+ const bids = (levels) => [...levels.values()].sort((a, b) => b.price - a.price);
455
+ const asks = (levels) => [...levels.values()].sort((a, b) => a.price - b.price);
456
+ this.cached = {
457
+ ...this.metadata,
458
+ bids: bids(this.bids),
459
+ asks: asks(this.asks),
460
+ venueOrderbooks: Object.fromEntries([...this.venueBooks].map(([venue, book]) => [venue, {
461
+ bids: bids(book.bids),
462
+ asks: asks(book.asks)
463
+ }]))
464
+ };
465
+ return this.cached;
466
+ }
467
+ };
468
+ //#endregion
469
+ //#region src/agg-websocket.ts
470
+ /**
471
+ * Agg platform WebSocket client.
472
+ *
473
+ * Single connection, refcounted subscriptions. Orderbook/trades subscriptions
474
+ * are keyed by venueMarketOutcomeId (the `outcomes[].id` on REST market
475
+ * responses — NOT a venueMarketId); arb subscriptions are keyed by marketId.
476
+ * Maintains in-memory orderbook state per subscribed outcome id.
477
+ * All timestamps converted from server ms to seconds before invoking callbacks.
478
+ *
479
+ * Recovery: seq gap or checksum mismatch → resnapshot request.
480
+ * Stale delta guard: deltas with seq <= current book seq are silently dropped.
481
+ * Liveness: no message for 45s → reconnect (server sends heartbeat every 30s).
482
+ * Book watchdog (optional): books silent for 5 min are checked against REST
483
+ * `seq`; a mismatch triggers a resnapshot, a repeat mismatch a resubscribe.
484
+ */
485
+ const RECONNECT_BASE_MS = 1e3;
486
+ const RECONNECT_MAX_MS = 3e4;
487
+ const STALE_TIMEOUT_MS = 45e3;
488
+ const ORDERBOOK_DEBUG_TOP_LEVELS = 5;
489
+ const WATCHDOG_SILENCE_MS = 3e5;
490
+ const WATCHDOG_CHECK_INTERVAL_MS = 6e4;
491
+ /** `GET /orderbooks/outcomes` accepts at most 100 ids per call. */
492
+ const WATCHDOG_BATCH_MAX = 100;
493
+ /** A watchdog REST check that takes longer than this is abandoned. */
494
+ const WATCHDOG_REQUEST_TIMEOUT_MS = 15e3;
495
+ /**
496
+ * Agg WebSocket client. Single WS connection with refcounted market subscriptions.
497
+ */
498
+ var AggWebSocket = class {
499
+ constructor(options) {
500
+ this.ws = null;
501
+ this.subs = /* @__PURE__ */ new Map();
502
+ this.pendingSubs = /* @__PURE__ */ new Set();
503
+ this.pendingTradeSubs = /* @__PURE__ */ new Set();
504
+ this.pendingResnapshots = /* @__PURE__ */ new Set();
505
+ this.pendingFlushSubs = /* @__PURE__ */ new Set();
506
+ this.pendingFlushUnsubs = /* @__PURE__ */ new Set();
507
+ this.pendingFlushTradeSubs = /* @__PURE__ */ new Set();
508
+ this.pendingFlushTradeUnsubs = /* @__PURE__ */ new Set();
509
+ this.pendingFlushArbSubs = /* @__PURE__ */ new Set();
510
+ this.pendingFlushArbUnsubs = /* @__PURE__ */ new Set();
511
+ this.flushScheduled = false;
512
+ this.arbSubs = /* @__PURE__ */ new Map();
513
+ this.arbFeedCbs = /* @__PURE__ */ new Set();
514
+ this.latestArb = /* @__PURE__ */ new Map();
515
+ this.books = /* @__PURE__ */ new Map();
516
+ this.resyncing = /* @__PURE__ */ new Set();
517
+ this.bookCheckedAt = /* @__PURE__ */ new Map();
518
+ this.watchdogResnapshotted = /* @__PURE__ */ new Set();
519
+ this.watchdogTimer = null;
520
+ this.watchdogAbort = null;
521
+ this.watchdogPausedUntil = 0;
522
+ this.connecting = false;
523
+ this._connected = false;
524
+ this.reconnectAttempts = 0;
525
+ this.reconnectTimer = null;
526
+ this.staleTimer = null;
527
+ this.lastMessageTime = 0;
528
+ this.destroyed = false;
529
+ this.historyControllers = /* @__PURE__ */ new Set();
530
+ this.deferBookViews = options.deferBookViews ?? false;
531
+ this.url = options.url;
532
+ this.callbacks = options.callbacks ?? {};
533
+ this.initialToken = options.initialToken;
534
+ this.enableLogs = options.enableLogs ?? false;
535
+ this.bookWatchdog = options.bookWatchdog;
536
+ }
537
+ get connected() {
538
+ return this._connected;
539
+ }
540
+ /** Decode dynamic authenticated REST bytes in the same processing environment as live data. */
541
+ async decodeHistory(payload) {
542
+ return decodeChartHistory(payload);
543
+ }
544
+ /** Immutable history decoder. Browser adapters execute this inside their Worker. */
545
+ async loadHistory(manifest, options) {
546
+ if (this.destroyed) throw new Error("Market-data client disposed");
547
+ const controller = new AbortController();
548
+ const abort = () => controller.abort(options?.signal?.reason);
549
+ options?.signal?.addEventListener("abort", abort, { once: true });
550
+ if (options?.signal?.aborted) abort();
551
+ this.historyControllers.add(controller);
552
+ try {
553
+ controller.signal.throwIfAborted();
554
+ const result = await loadImmutableHistory({
555
+ manifest,
556
+ signal: controller.signal
557
+ });
558
+ controller.signal.throwIfAborted();
559
+ return result;
560
+ } finally {
561
+ this.historyControllers.delete(controller);
562
+ options?.signal?.removeEventListener("abort", abort);
563
+ }
564
+ }
565
+ /** Optional framework-independent host diagnostics, cleared on disposal. */
566
+ setHostDiagnostics(provider) {
567
+ this.hostDiagnostics = provider;
568
+ }
569
+ /** On-demand ownership diagnostics; contains no credentials or user/account data. */
570
+ getDiagnostics() {
571
+ return {
572
+ connected: this.connected,
573
+ host: this.hostDiagnostics?.(),
574
+ transport: this.destroyed ? "disposed" : "direct",
575
+ orderbookOutcomeIds: this.activeOrderbookMarkets(),
576
+ tradeOutcomeIds: this.activeTradeMarkets(),
577
+ arbMarketIds: this.activeArbMarkets(),
578
+ arbFeedSubscribers: this.arbFeedCbs.size,
579
+ cachedBooks: this.books.size,
580
+ cachedArb: this.latestArb.size,
581
+ resyncingBooks: this.resyncing.size,
582
+ pendingResnapshots: this.pendingResnapshots.size,
583
+ pendingHistoryRequests: this.historyControllers.size
584
+ };
585
+ }
586
+ /** Get the current orderbook state for an outcome (or null if no snapshot received yet). */
587
+ getBook(outcomeId) {
588
+ return this.books.get(outcomeId)?.view() ?? null;
589
+ }
590
+ /** Whether an outcome is currently resyncing (waiting for fresh snapshot). */
591
+ isResyncing(outcomeId) {
592
+ return this.resyncing.has(outcomeId);
593
+ }
594
+ /** Get the latest arb update for a market (or null if none received yet). */
595
+ getArb(marketId) {
596
+ return this.latestArb.get(marketId) ?? null;
597
+ }
598
+ /**
599
+ * Subscribe to per-market arb updates for a given marketId.
600
+ * Ref-counted: multiple callers for the same marketId share one server subscription.
601
+ * Returns an unsubscribe function.
602
+ */
603
+ subscribeArb(marketId, cb) {
604
+ let entry = this.arbSubs.get(marketId);
605
+ if (!entry) {
606
+ entry = {
607
+ refCount: 0,
608
+ cbs: /* @__PURE__ */ new Set()
609
+ };
610
+ this.arbSubs.set(marketId, entry);
611
+ this.ensureConnected();
612
+ if (this._connected) {
613
+ this.pendingFlushArbUnsubs.delete(marketId);
614
+ this.pendingFlushArbSubs.add(marketId);
615
+ this.scheduleFlush();
616
+ }
617
+ }
618
+ entry.cbs.add(cb);
619
+ entry.refCount++;
620
+ return () => {
621
+ const e = this.arbSubs.get(marketId);
622
+ if (!e) return;
623
+ e.cbs.delete(cb);
624
+ e.refCount--;
625
+ if (e.refCount <= 0) {
626
+ this.arbSubs.delete(marketId);
627
+ if (this._connected) {
628
+ if (this.pendingFlushArbSubs.has(marketId)) this.pendingFlushArbSubs.delete(marketId);
629
+ else {
630
+ this.pendingFlushArbUnsubs.add(marketId);
631
+ this.scheduleFlush();
632
+ }
633
+ }
634
+ }
635
+ };
636
+ }
637
+ /**
638
+ * Subscribe to the global arb-feed batches.
639
+ * Only one server subscription is maintained regardless of how many callers.
640
+ * Returns an unsubscribe function.
641
+ */
642
+ subscribeArbFeed(cb) {
643
+ const wasEmpty = this.arbFeedCbs.size === 0;
644
+ this.arbFeedCbs.add(cb);
645
+ if (wasEmpty) {
646
+ this.ensureConnected();
647
+ if (this._connected) this.send({
648
+ action: "subscribe",
649
+ channel: "arb-feed"
650
+ });
651
+ }
652
+ return () => {
653
+ this.arbFeedCbs.delete(cb);
654
+ if (this.arbFeedCbs.size === 0 && this._connected) this.send({
655
+ action: "unsubscribe",
656
+ channel: "arb-feed"
657
+ });
658
+ };
659
+ }
660
+ /**
661
+ * Subscribe to a feed by outcome ID.
662
+ * @param outcomeId The venueMarketOutcomeId to subscribe to.
663
+ * @param channel "orderbook" (default) or "trades".
664
+ * Returns an unsubscribe function (ref-counted).
665
+ */
666
+ subscribe(outcomeId, channel = "orderbook") {
667
+ if (channel === "trades") return this._subscribeTrades(outcomeId);
668
+ let sub = this.subs.get(outcomeId);
669
+ if (!sub) {
670
+ sub = {
671
+ refCount: 0,
672
+ tradeRefs: 0
673
+ };
674
+ this.subs.set(outcomeId, sub);
675
+ }
676
+ if (sub.refCount === 0) {
677
+ this.bookCheckedAt.set(outcomeId, Date.now());
678
+ this.pendingSubs.add(outcomeId);
679
+ this.ensureConnected();
680
+ if (this._connected) {
681
+ this.pendingFlushUnsubs.delete(outcomeId);
682
+ this.pendingFlushSubs.add(outcomeId);
683
+ this.pendingSubs.delete(outcomeId);
684
+ this.scheduleFlush();
685
+ }
686
+ }
687
+ sub.refCount++;
688
+ let released = false;
689
+ return () => {
690
+ if (released) return;
691
+ released = true;
692
+ const s = this.subs.get(outcomeId);
693
+ if (!s) return;
694
+ s.refCount--;
695
+ if (s.refCount <= 0) {
696
+ s.refCount = 0;
697
+ if (s.tradeRefs <= 0) {
698
+ this.subs.delete(outcomeId);
699
+ this.books.delete(outcomeId);
700
+ }
701
+ this.resyncing.delete(outcomeId);
702
+ this.bookCheckedAt.delete(outcomeId);
703
+ this.watchdogResnapshotted.delete(outcomeId);
704
+ if (this._connected) {
705
+ if (this.pendingFlushSubs.has(outcomeId)) this.pendingFlushSubs.delete(outcomeId);
706
+ else {
707
+ this.pendingFlushUnsubs.add(outcomeId);
708
+ this.scheduleFlush();
709
+ }
710
+ }
711
+ this.pendingSubs.delete(outcomeId);
712
+ this.pendingResnapshots.delete(outcomeId);
713
+ }
714
+ };
715
+ }
716
+ /**
717
+ * Subscribe to an outcome's trade feed.
718
+ * @deprecated Use `subscribe(outcomeId, "trades")` instead.
719
+ * Returns an unsubscribe function.
720
+ */
721
+ subscribeTrades(outcomeId) {
722
+ return this._subscribeTrades(outcomeId);
723
+ }
724
+ _subscribeTrades(outcomeId) {
725
+ let sub = this.subs.get(outcomeId);
726
+ if (!sub) {
727
+ sub = {
728
+ refCount: 0,
729
+ tradeRefs: 0
730
+ };
731
+ this.subs.set(outcomeId, sub);
732
+ }
733
+ if (sub.tradeRefs === 0) {
734
+ this.pendingTradeSubs.add(outcomeId);
735
+ this.ensureConnected();
736
+ if (this._connected) {
737
+ this.pendingFlushTradeUnsubs.delete(outcomeId);
738
+ this.pendingFlushTradeSubs.add(outcomeId);
739
+ this.pendingTradeSubs.delete(outcomeId);
740
+ this.scheduleFlush();
741
+ }
742
+ }
743
+ sub.tradeRefs++;
744
+ let released = false;
745
+ return () => {
746
+ if (released) return;
747
+ released = true;
748
+ const s = this.subs.get(outcomeId);
749
+ if (!s) return;
750
+ s.tradeRefs--;
751
+ if (s.tradeRefs <= 0) {
752
+ s.tradeRefs = 0;
753
+ if (this._connected) {
754
+ if (this.pendingFlushTradeSubs.has(outcomeId)) this.pendingFlushTradeSubs.delete(outcomeId);
755
+ else {
756
+ this.pendingFlushTradeUnsubs.add(outcomeId);
757
+ this.scheduleFlush();
758
+ }
759
+ }
760
+ this.pendingTradeSubs.delete(outcomeId);
761
+ this.pendingResnapshots.delete(outcomeId);
762
+ if (s.refCount <= 0) {
763
+ this.subs.delete(outcomeId);
764
+ this.books.delete(outcomeId);
765
+ this.resyncing.delete(outcomeId);
766
+ }
767
+ }
768
+ };
769
+ }
770
+ /** Send a JWT to authenticate (or re-authenticate) the connection. */
771
+ authenticate(token) {
772
+ this.initialToken = token;
773
+ if (this._connected) this.send({
774
+ action: "authenticate",
775
+ token
776
+ });
777
+ }
778
+ /** Request a fresh snapshot for one or more subscribed outcomes. */
779
+ resnapshot(outcomeIdOrIds) {
780
+ (Array.isArray(outcomeIdOrIds) ? outcomeIdOrIds : [outcomeIdOrIds]).forEach((outcomeId) => {
781
+ this.requestResnapshot(outcomeId, true);
782
+ });
783
+ }
784
+ /** Connect (idempotent). Called automatically on first subscribe. */
785
+ connect() {
786
+ this.ensureConnected();
787
+ }
788
+ /** Disconnect and clean up. Reusable — call connect() to reconnect. */
789
+ disconnect() {
790
+ this.cancelReconnect();
791
+ this.cancelStaleTimer();
792
+ this._connected = false;
793
+ this.connecting = false;
794
+ if (this.ws) {
795
+ this.ws.onclose = null;
796
+ this.ws.close();
797
+ this.ws = null;
798
+ }
799
+ this.callbacks.onConnectionStateChange?.(false);
800
+ }
801
+ /** Permanently destroy. Cannot reconnect after this. */
802
+ destroy() {
803
+ this.destroyed = true;
804
+ for (const controller of this.historyControllers) controller.abort(/* @__PURE__ */ new Error("Market-data client disposed"));
805
+ this.historyControllers.clear();
806
+ this.disconnect();
807
+ this.hostDiagnostics = void 0;
808
+ this.callbacks = {};
809
+ this.subs.clear();
810
+ this.books.clear();
811
+ this.resyncing.clear();
812
+ if (this.watchdogTimer) clearInterval(this.watchdogTimer);
813
+ this.watchdogTimer = null;
814
+ this.watchdogAbort?.abort();
815
+ this.watchdogAbort = null;
816
+ this.bookCheckedAt.clear();
817
+ this.watchdogResnapshotted.clear();
818
+ this.pendingSubs.clear();
819
+ this.pendingTradeSubs.clear();
820
+ this.pendingResnapshots.clear();
821
+ this.pendingFlushSubs.clear();
822
+ this.pendingFlushUnsubs.clear();
823
+ this.pendingFlushTradeSubs.clear();
824
+ this.pendingFlushTradeUnsubs.clear();
825
+ this.arbSubs.clear();
826
+ this.arbFeedCbs.clear();
827
+ this.latestArb.clear();
828
+ this.pendingFlushArbSubs.clear();
829
+ this.pendingFlushArbUnsubs.clear();
830
+ this.flushScheduled = false;
831
+ }
832
+ /** Update callbacks (e.g., after React re-render). */
833
+ setCallbacks(callbacks) {
834
+ this.callbacks = callbacks;
835
+ }
836
+ ensureConnected() {
837
+ if (this.destroyed) return;
838
+ if (this.ws?.readyState === WebSocket.OPEN || this.connecting) return;
839
+ this.doConnect();
840
+ }
841
+ doConnect() {
842
+ if (this.destroyed) return;
843
+ this.connecting = true;
844
+ const ws = new WebSocket(this.url);
845
+ this.ws = ws;
846
+ ws.onopen = () => {
847
+ if (this.destroyed || this.ws !== ws) return;
848
+ this.connecting = false;
849
+ this.reconnectAttempts = 0;
850
+ this.resetStaleTimer();
851
+ if (this.initialToken) this.send({
852
+ action: "authenticate",
853
+ token: this.initialToken
854
+ });
855
+ };
856
+ ws.onmessage = (event) => {
857
+ if (this.destroyed || this.ws !== ws) return;
858
+ this.lastMessageTime = Date.now();
859
+ this.callbacks.onFrameReceived?.(this.lastMessageTime);
860
+ this.resetStaleTimer();
861
+ try {
862
+ const raw = JSON.parse(event.data);
863
+ this.handleMessage(raw);
864
+ } catch {}
865
+ };
866
+ ws.onclose = () => {
867
+ this.ws = null;
868
+ this.connecting = false;
869
+ const wasConnected = this._connected;
870
+ this._connected = false;
871
+ this.cancelStaleTimer();
872
+ if (wasConnected) this.callbacks.onConnectionStateChange?.(false);
873
+ if (!this.destroyed && (this.subs.size > 0 || this.arbSubs.size > 0 || this.arbFeedCbs.size > 0)) this.scheduleReconnect();
874
+ };
875
+ ws.onerror = () => {};
876
+ }
877
+ scheduleReconnect() {
878
+ this.cancelReconnect();
879
+ const delay = Math.min(RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempts), RECONNECT_MAX_MS);
880
+ this.reconnectAttempts++;
881
+ this.emitDiagnostic({
882
+ type: "reconnect",
883
+ attempt: this.reconnectAttempts,
884
+ delayMs: delay
885
+ });
886
+ this.reconnectTimer = setTimeout(() => {
887
+ this.reconnectTimer = null;
888
+ this.doConnect();
889
+ }, delay);
890
+ }
891
+ cancelReconnect() {
892
+ if (this.reconnectTimer) {
893
+ clearTimeout(this.reconnectTimer);
894
+ this.reconnectTimer = null;
895
+ }
896
+ }
897
+ resetStaleTimer() {
898
+ this.cancelStaleTimer();
899
+ this.staleTimer = setTimeout(() => {
900
+ const age = Date.now() - this.lastMessageTime;
901
+ this.emitDiagnostic({
902
+ type: "stale_connection",
903
+ lastMessageAge: age
904
+ });
905
+ if (this.ws) this.ws.close();
906
+ }, STALE_TIMEOUT_MS);
907
+ }
908
+ cancelStaleTimer() {
909
+ if (this.staleTimer) {
910
+ clearTimeout(this.staleTimer);
911
+ this.staleTimer = null;
912
+ }
913
+ }
914
+ handleMessage(raw) {
915
+ const type = raw.type;
916
+ if ([
917
+ "orderbook_snapshot",
918
+ "orderbook_delta",
919
+ "trade"
920
+ ].includes(type) && !validMarketFrame(raw)) {
921
+ this.callbacks.onError?.({
922
+ type: "error",
923
+ message: "Invalid market-data frame"
924
+ });
925
+ if (typeof raw.outcomeId === "string" && this.subs.has(raw.outcomeId)) this.requestResnapshot(raw.outcomeId);
926
+ return;
927
+ }
928
+ switch (type) {
929
+ case "orderbook_snapshot":
930
+ this.handleSnapshot(raw);
931
+ break;
932
+ case "orderbook_delta":
933
+ this.handleDelta(raw);
934
+ break;
935
+ case "trade":
936
+ this.handleTrade(raw);
937
+ break;
938
+ case "heartbeat":
939
+ this.handleHeartbeat(raw);
940
+ break;
941
+ case "subscribed":
942
+ this.callbacks.onSubscribed?.(raw);
943
+ break;
944
+ case "unsubscribed":
945
+ this.callbacks.onUnsubscribed?.(raw);
946
+ break;
947
+ case "connected":
948
+ this.handleConnected(raw);
949
+ break;
950
+ case "authenticated":
951
+ this.callbacks.onAuthenticated?.(raw);
952
+ break;
953
+ case "market_resolved":
954
+ this.handleMarketResolved(raw);
955
+ break;
956
+ case "error":
957
+ this.callbacks.onError?.(raw);
958
+ break;
959
+ case "order_submitted":
960
+ this.handleOrderSubmitted(raw);
961
+ break;
962
+ case "balance_update":
963
+ this.handleBalanceUpdate(raw);
964
+ break;
965
+ case "order_event":
966
+ if (this.callbacks.onOrderEventReceipt) {
967
+ const receivedAtMs = Date.now();
968
+ const serverTimestampMs = typeof raw.timestamp === "number" && Number.isFinite(raw.timestamp) && raw.timestamp > 0 ? raw.timestamp : null;
969
+ try {
970
+ this.callbacks.onOrderEventReceipt({
971
+ orderId: String(raw.orderId ?? ""),
972
+ event: raw.event,
973
+ serverTimestampMs,
974
+ receivedAtMs,
975
+ deliveryDelayMs: serverTimestampMs !== null && receivedAtMs >= serverTimestampMs ? receivedAtMs - serverTimestampMs : null
976
+ });
977
+ } catch {}
978
+ }
979
+ this.callbacks.onOrderEvent?.(raw);
980
+ break;
981
+ case "redeem_event":
982
+ this.handleRedeemEvent(raw);
983
+ break;
984
+ case "withdrawal_lifecycle":
985
+ this.callbacks.onWithdrawalLifecycle?.(raw);
986
+ break;
987
+ case "arb_market_update": {
988
+ const arbMsg = raw;
989
+ this.latestArb.set(arbMsg.marketId, arbMsg);
990
+ const arbEntry = this.arbSubs.get(arbMsg.marketId);
991
+ if (arbEntry) for (const cb of arbEntry.cbs) cb(arbMsg);
992
+ this.callbacks.onArbMarket?.(arbMsg);
993
+ break;
994
+ }
995
+ case "arb_feed_batch": {
996
+ const feedMsg = raw;
997
+ for (const cb of this.arbFeedCbs) cb(feedMsg);
998
+ this.callbacks.onArbFeed?.(feedMsg);
999
+ break;
1000
+ }
1001
+ }
1002
+ }
1003
+ /**
1004
+ * Server confirmed the connection — gateway has validated our appId and is
1005
+ * ready to accept subscriptions. Now flush all pending subs.
1006
+ */
1007
+ handleConnected(msg) {
1008
+ this._connected = true;
1009
+ if (this.bookWatchdog && !this.watchdogTimer && !this.destroyed) this.watchdogTimer = setInterval(() => void this.checkSilentBooks(), this.bookWatchdog.checkIntervalMs ?? WATCHDOG_CHECK_INTERVAL_MS);
1010
+ this.callbacks.onConnectionStateChange?.(true);
1011
+ this.callbacks.onConnected?.(msg);
1012
+ const orderbookSet = new Set(this.activeOrderbookMarkets());
1013
+ const reconnectedAt = Date.now();
1014
+ for (const id of orderbookSet) if (this.bookCheckedAt.has(id)) this.bookCheckedAt.set(id, reconnectedAt);
1015
+ this.pendingSubs.forEach((id) => orderbookSet.add(id));
1016
+ this.pendingFlushSubs.forEach((id) => orderbookSet.add(id));
1017
+ const orderbookIds = Array.from(orderbookSet);
1018
+ if (orderbookIds.length > 0) this.sendSubscribe(orderbookIds);
1019
+ this.pendingSubs.clear();
1020
+ this.pendingFlushSubs.clear();
1021
+ this.pendingFlushUnsubs.clear();
1022
+ const tradeSet = new Set(this.activeTradeMarkets());
1023
+ this.pendingTradeSubs.forEach((id) => tradeSet.add(id));
1024
+ this.pendingFlushTradeSubs.forEach((id) => tradeSet.add(id));
1025
+ const tradeIds = Array.from(tradeSet);
1026
+ if (tradeIds.length > 0) this.send({
1027
+ action: "subscribe",
1028
+ channel: "trades",
1029
+ outcomeIds: tradeIds
1030
+ });
1031
+ this.pendingTradeSubs.clear();
1032
+ this.pendingFlushTradeSubs.clear();
1033
+ this.pendingFlushTradeUnsubs.clear();
1034
+ const resnapshotIds = Array.from(this.pendingResnapshots);
1035
+ if (resnapshotIds.length > 0) this.send({
1036
+ action: "resnapshot",
1037
+ channel: "orderbook",
1038
+ outcomeIds: resnapshotIds
1039
+ });
1040
+ this.pendingResnapshots.clear();
1041
+ const arbSet = new Set(this.activeArbMarkets());
1042
+ this.pendingFlushArbSubs.forEach((id) => arbSet.add(id));
1043
+ const arbIds = Array.from(arbSet);
1044
+ if (arbIds.length > 0) this.send({
1045
+ action: "subscribe",
1046
+ channel: "arb",
1047
+ marketIds: arbIds
1048
+ });
1049
+ this.pendingFlushArbSubs.clear();
1050
+ this.pendingFlushArbUnsubs.clear();
1051
+ if (this.arbFeedCbs.size > 0) this.send({
1052
+ action: "subscribe",
1053
+ channel: "arb-feed"
1054
+ });
1055
+ }
1056
+ handleSnapshot(raw) {
1057
+ if (!this.subs.has(raw.outcomeId)) return;
1058
+ const msg = {
1059
+ ...raw,
1060
+ timestamp: raw.timestamp / 1e3
1061
+ };
1062
+ const book = snapshotToOrderbook(msg);
1063
+ this.books.set(msg.outcomeId, new IndexedOrderbook(book));
1064
+ this.bookCheckedAt.set(msg.outcomeId, Date.now());
1065
+ this.resyncing.delete(msg.outcomeId);
1066
+ this.pendingResnapshots.delete(msg.outcomeId);
1067
+ this.debugOrderbook("snapshot_applied", {
1068
+ outcomeId: msg.outcomeId,
1069
+ seq: msg.seq,
1070
+ checksum: msg.checksum,
1071
+ bidsTop: this.summarizeTopLevels(book.bids),
1072
+ asksTop: this.summarizeTopLevels(book.asks)
1073
+ });
1074
+ this.callbacks.onBookTick?.(msg.outcomeId, {
1075
+ midpoint: msg.midpoint,
1076
+ timestamp: msg.timestamp,
1077
+ source: "snapshot"
1078
+ });
1079
+ if (!this.deferBookViews) this.callbacks.onSnapshot?.(msg.outcomeId, book);
1080
+ }
1081
+ handleDelta(raw) {
1082
+ if (!this.subs.has(raw.outcomeId)) return;
1083
+ const outcomeId = raw.outcomeId;
1084
+ const current = this.books.get(outcomeId);
1085
+ if (!current) {
1086
+ this.debugOrderbook("delta_missing_snapshot", {
1087
+ outcomeId,
1088
+ seq: raw.seq
1089
+ });
1090
+ this.requestResnapshot(outcomeId);
1091
+ return;
1092
+ }
1093
+ if (raw.seq <= current.seq) {
1094
+ this.debugOrderbook("delta_stale_ignored", {
1095
+ outcomeId,
1096
+ currentSeq: current.seq,
1097
+ receivedSeq: raw.seq
1098
+ });
1099
+ return;
1100
+ }
1101
+ if (raw.seq > current.seq + 1) {
1102
+ this.debugOrderbook("delta_seq_gap", {
1103
+ outcomeId,
1104
+ expectedSeq: current.seq + 1,
1105
+ receivedSeq: raw.seq
1106
+ });
1107
+ this.emitDiagnostic({
1108
+ type: "seq_gap",
1109
+ marketId: outcomeId,
1110
+ outcomeId,
1111
+ expected: current.seq + 1,
1112
+ received: raw.seq
1113
+ });
1114
+ this.requestResnapshot(outcomeId);
1115
+ return;
1116
+ }
1117
+ const msg = {
1118
+ ...raw,
1119
+ timestamp: raw.timestamp / 1e3
1120
+ };
1121
+ const computed = current.apply(msg);
1122
+ if (computed !== msg.checksum) {
1123
+ this.debugOrderbook("delta_checksum_mismatch", {
1124
+ outcomeId,
1125
+ seq: msg.seq,
1126
+ expected: msg.checksum,
1127
+ computed
1128
+ });
1129
+ this.emitDiagnostic({
1130
+ type: "checksum_mismatch",
1131
+ marketId: outcomeId,
1132
+ outcomeId,
1133
+ expected: msg.checksum,
1134
+ computed
1135
+ });
1136
+ this.requestResnapshot(outcomeId);
1137
+ return;
1138
+ }
1139
+ this.bookCheckedAt.set(outcomeId, Date.now());
1140
+ this.watchdogResnapshotted.delete(outcomeId);
1141
+ this.callbacks.onBookTick?.(outcomeId, {
1142
+ midpoint: msg.midpoint,
1143
+ timestamp: msg.timestamp,
1144
+ source: "delta"
1145
+ });
1146
+ if (!this.deferBookViews) this.callbacks.onDelta?.(outcomeId, current.view());
1147
+ }
1148
+ handleTrade(raw) {
1149
+ if (!this.subs.has(raw.outcomeId)) return;
1150
+ const msg = {
1151
+ ...raw,
1152
+ timestamp: raw.timestamp / 1e3
1153
+ };
1154
+ this.callbacks.onTrade?.(msg);
1155
+ }
1156
+ handleHeartbeat(raw) {
1157
+ const msg = {
1158
+ ...raw,
1159
+ ts: raw.ts / 1e3
1160
+ };
1161
+ this.callbacks.onHeartbeat?.(msg);
1162
+ }
1163
+ handleMarketResolved(raw) {
1164
+ const msg = {
1165
+ ...raw,
1166
+ timestamp: raw.timestamp / 1e3
1167
+ };
1168
+ this.books.delete(msg.outcomeId);
1169
+ this.bookCheckedAt.delete(msg.outcomeId);
1170
+ this.watchdogResnapshotted.delete(msg.outcomeId);
1171
+ this.pendingResnapshots.delete(msg.outcomeId);
1172
+ this.resyncing.delete(msg.outcomeId);
1173
+ this.callbacks.onMarketResolved?.(msg);
1174
+ }
1175
+ handleOrderSubmitted(raw) {
1176
+ const msg = {
1177
+ ...raw,
1178
+ timestamp: raw.timestamp / 1e3
1179
+ };
1180
+ this.callbacks.onOrderSubmitted?.(msg);
1181
+ }
1182
+ handleBalanceUpdate(raw) {
1183
+ const msg = {
1184
+ ...raw,
1185
+ timestamp: raw.timestamp / 1e3
1186
+ };
1187
+ this.callbacks.onBalanceUpdate?.(msg);
1188
+ }
1189
+ handleRedeemEvent(raw) {
1190
+ const msg = {
1191
+ ...raw,
1192
+ timestamp: raw.timestamp / 1e3
1193
+ };
1194
+ this.callbacks.onRedeemEvent?.(msg);
1195
+ }
1196
+ /**
1197
+ * Heartbeats prove the socket, not each stream. Every subscribed outcome
1198
+ * with no progress for `silenceMs` is checked against the REST `seq`:
1199
+ *
1200
+ * - book present, not resyncing, same `seq` → the market is just quiet;
1201
+ * - different `seq`, no book (snapshot never arrived, or a resubscribe's
1202
+ * snapshot was lost), or stuck resyncing (any recovery whose snapshot
1203
+ * was lost) → resnapshot, then resubscribe on the next overdue check;
1204
+ * - REST can't serve it (`null`) → counted as an attempt, so the next
1205
+ * batch moves on to other outcomes instead of retrying the same 100.
1206
+ */
1207
+ async checkSilentBooks() {
1208
+ const opts = this.bookWatchdog;
1209
+ if (!opts || !this._connected || this.watchdogAbort) return;
1210
+ const now = Date.now();
1211
+ if (now < this.watchdogPausedUntil) return;
1212
+ const silenceMs = opts.silenceMs ?? WATCHDOG_SILENCE_MS;
1213
+ const silent = this.activeOrderbookMarkets().filter((id) => this.bookCheckedAt.has(id)).filter((id) => now - this.bookCheckedAt.get(id) >= silenceMs).sort((a, b) => this.bookCheckedAt.get(a) - this.bookCheckedAt.get(b)).slice(0, WATCHDOG_BATCH_MAX);
1214
+ if (silent.length === 0) return;
1215
+ const checkedAt = new Map(silent.map((id) => [id, this.bookCheckedAt.get(id)]));
1216
+ const abort = new AbortController();
1217
+ this.watchdogAbort = abort;
1218
+ const timeout = setTimeout(() => abort.abort(), WATCHDOG_REQUEST_TIMEOUT_MS);
1219
+ try {
1220
+ const url = new URL(`${opts.baseUrl.replace(/\/$/, "")}/orderbooks/outcomes`);
1221
+ url.searchParams.set("venueMarketOutcomeIds", silent.join(","));
1222
+ url.searchParams.set("depth", "1");
1223
+ const response = await fetch(url, {
1224
+ headers: opts.headers,
1225
+ signal: abort.signal
1226
+ });
1227
+ if (!response.ok) {
1228
+ const retryAfterS = Number(response.headers.get("retry-after"));
1229
+ if (response.status === 429 && retryAfterS > 0) this.watchdogPausedUntil = Date.now() + retryAfterS * 1e3;
1230
+ this.emitDiagnostic({
1231
+ type: "book_watchdog_error",
1232
+ status: response.status
1233
+ });
1234
+ return;
1235
+ }
1236
+ const body = await response.json();
1237
+ if (this.destroyed) return;
1238
+ silent.forEach((outcomeId, i) => {
1239
+ if (this.bookCheckedAt.get(outcomeId) !== checkedAt.get(outcomeId)) return;
1240
+ const server = body.data?.[i];
1241
+ if (!server) {
1242
+ this.bookCheckedAt.set(outcomeId, Date.now());
1243
+ return;
1244
+ }
1245
+ const book = this.books.get(outcomeId);
1246
+ if (book && !this.resyncing.has(outcomeId) && server.seq === book.seq) {
1247
+ this.bookCheckedAt.set(outcomeId, Date.now());
1248
+ this.watchdogResnapshotted.delete(outcomeId);
1249
+ return;
1250
+ }
1251
+ const action = this.watchdogResnapshotted.has(outcomeId) ? "resubscribe" : "resnapshot";
1252
+ this.emitDiagnostic({
1253
+ type: "book_watchdog_resync",
1254
+ outcomeId,
1255
+ localSeq: book?.seq ?? null,
1256
+ serverSeq: server.seq,
1257
+ action
1258
+ });
1259
+ this.bookCheckedAt.set(outcomeId, Date.now());
1260
+ if (action === "resubscribe") {
1261
+ this.watchdogResnapshotted.delete(outcomeId);
1262
+ this.books.delete(outcomeId);
1263
+ this.resyncing.add(outcomeId);
1264
+ this.send({
1265
+ action: "unsubscribe",
1266
+ channel: "orderbook",
1267
+ outcomeIds: [outcomeId]
1268
+ });
1269
+ this.sendSubscribe([outcomeId]);
1270
+ } else {
1271
+ this.watchdogResnapshotted.add(outcomeId);
1272
+ this.requestResnapshot(outcomeId, true);
1273
+ }
1274
+ });
1275
+ } catch {
1276
+ if (!this.destroyed) this.emitDiagnostic({
1277
+ type: "book_watchdog_error",
1278
+ status: null
1279
+ });
1280
+ } finally {
1281
+ clearTimeout(timeout);
1282
+ if (this.watchdogAbort === abort) this.watchdogAbort = null;
1283
+ }
1284
+ }
1285
+ requestResnapshot(outcomeId, force = false) {
1286
+ if (this.resyncing.has(outcomeId) && !force) return;
1287
+ this.resyncing.add(outcomeId);
1288
+ if (this._connected) {
1289
+ this.send({
1290
+ action: "resnapshot",
1291
+ channel: "orderbook",
1292
+ outcomeIds: [outcomeId]
1293
+ });
1294
+ this.pendingResnapshots.delete(outcomeId);
1295
+ return;
1296
+ }
1297
+ this.pendingResnapshots.add(outcomeId);
1298
+ this.ensureConnected();
1299
+ }
1300
+ /**
1301
+ * Schedule a microtask that flushes queued subscribe/unsubscribe frames.
1302
+ * Coalesces multiple same-tick sub/unsub calls into at most one frame per
1303
+ * channel+action pair.
1304
+ */
1305
+ scheduleFlush() {
1306
+ if (this.flushScheduled) return;
1307
+ this.flushScheduled = true;
1308
+ queueMicrotask(() => {
1309
+ this.flushScheduled = false;
1310
+ if (!this._connected) {
1311
+ this.pendingFlushSubs.clear();
1312
+ this.pendingFlushUnsubs.clear();
1313
+ this.pendingFlushTradeSubs.clear();
1314
+ this.pendingFlushTradeUnsubs.clear();
1315
+ this.pendingFlushArbSubs.clear();
1316
+ this.pendingFlushArbUnsubs.clear();
1317
+ return;
1318
+ }
1319
+ this.flushPending();
1320
+ });
1321
+ }
1322
+ flushPending() {
1323
+ for (const id of [...this.pendingFlushSubs]) if (this.pendingFlushUnsubs.has(id)) {
1324
+ this.pendingFlushSubs.delete(id);
1325
+ this.pendingFlushUnsubs.delete(id);
1326
+ }
1327
+ if (this.pendingFlushSubs.size > 0) {
1328
+ this.sendSubscribe(Array.from(this.pendingFlushSubs));
1329
+ this.pendingFlushSubs.clear();
1330
+ }
1331
+ if (this.pendingFlushUnsubs.size > 0) {
1332
+ this.send({
1333
+ action: "unsubscribe",
1334
+ channel: "orderbook",
1335
+ outcomeIds: Array.from(this.pendingFlushUnsubs)
1336
+ });
1337
+ this.pendingFlushUnsubs.clear();
1338
+ }
1339
+ for (const id of [...this.pendingFlushTradeSubs]) if (this.pendingFlushTradeUnsubs.has(id)) {
1340
+ this.pendingFlushTradeSubs.delete(id);
1341
+ this.pendingFlushTradeUnsubs.delete(id);
1342
+ }
1343
+ if (this.pendingFlushTradeSubs.size > 0) {
1344
+ this.send({
1345
+ action: "subscribe",
1346
+ channel: "trades",
1347
+ outcomeIds: Array.from(this.pendingFlushTradeSubs)
1348
+ });
1349
+ this.pendingFlushTradeSubs.clear();
1350
+ }
1351
+ if (this.pendingFlushTradeUnsubs.size > 0) {
1352
+ this.send({
1353
+ action: "unsubscribe",
1354
+ channel: "trades",
1355
+ outcomeIds: Array.from(this.pendingFlushTradeUnsubs)
1356
+ });
1357
+ this.pendingFlushTradeUnsubs.clear();
1358
+ }
1359
+ for (const id of [...this.pendingFlushArbSubs]) if (this.pendingFlushArbUnsubs.has(id)) {
1360
+ this.pendingFlushArbSubs.delete(id);
1361
+ this.pendingFlushArbUnsubs.delete(id);
1362
+ }
1363
+ if (this.pendingFlushArbSubs.size > 0) {
1364
+ this.send({
1365
+ action: "subscribe",
1366
+ channel: "arb",
1367
+ marketIds: Array.from(this.pendingFlushArbSubs)
1368
+ });
1369
+ this.pendingFlushArbSubs.clear();
1370
+ }
1371
+ if (this.pendingFlushArbUnsubs.size > 0) {
1372
+ this.send({
1373
+ action: "unsubscribe",
1374
+ channel: "arb",
1375
+ marketIds: Array.from(this.pendingFlushArbUnsubs)
1376
+ });
1377
+ this.pendingFlushArbUnsubs.clear();
1378
+ }
1379
+ }
1380
+ sendSubscribe(outcomeIds) {
1381
+ this.send({
1382
+ action: "subscribe",
1383
+ channel: "orderbook",
1384
+ outcomeIds
1385
+ });
1386
+ }
1387
+ send(msg) {
1388
+ if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(msg));
1389
+ }
1390
+ activeOrderbookMarkets() {
1391
+ const markets = [];
1392
+ this.subs.forEach((sub, id) => {
1393
+ if (sub.refCount > 0) markets.push(id);
1394
+ });
1395
+ return markets;
1396
+ }
1397
+ activeTradeMarkets() {
1398
+ const markets = [];
1399
+ this.subs.forEach((sub, id) => {
1400
+ if (sub.tradeRefs > 0) markets.push(id);
1401
+ });
1402
+ return markets;
1403
+ }
1404
+ activeArbMarkets() {
1405
+ return Array.from(this.arbSubs.keys());
1406
+ }
1407
+ emitDiagnostic(event) {
1408
+ this.callbacks.onDiagnostics?.(event);
1409
+ }
1410
+ summarizeTopLevels(levels) {
1411
+ return levels.slice(0, ORDERBOOK_DEBUG_TOP_LEVELS).map((level) => ({
1412
+ price: level.price,
1413
+ size: level.size
1414
+ }));
1415
+ }
1416
+ debugOrderbook(event, payload) {
1417
+ if (!this.isOrderbookDebugEnabled()) return;
1418
+ console.debug(`[AggWebSocket][orderbook] ${event}`, payload);
1419
+ }
1420
+ isOrderbookDebugEnabled() {
1421
+ return this.enableLogs;
1422
+ }
1423
+ };
1424
+ //#endregion
1425
+ Object.defineProperty(exports, "AggWebSocket", {
1426
+ enumerable: true,
1427
+ get: function() {
1428
+ return AggWebSocket;
1429
+ }
1430
+ });
1431
+ Object.defineProperty(exports, "applyOrderbookDelta", {
1432
+ enumerable: true,
1433
+ get: function() {
1434
+ return applyOrderbookDelta;
1435
+ }
1436
+ });
1437
+ Object.defineProperty(exports, "computeChecksum", {
1438
+ enumerable: true,
1439
+ get: function() {
1440
+ return computeChecksum;
1441
+ }
1442
+ });
1443
+ Object.defineProperty(exports, "decodeChartHistory", {
1444
+ enumerable: true,
1445
+ get: function() {
1446
+ return decodeChartHistory;
1447
+ }
1448
+ });
1449
+ Object.defineProperty(exports, "loadImmutableHistory", {
1450
+ enumerable: true,
1451
+ get: function() {
1452
+ return loadImmutableHistory;
1453
+ }
1454
+ });
1455
+ Object.defineProperty(exports, "orderbookCrc32", {
1456
+ enumerable: true,
1457
+ get: function() {
1458
+ return orderbookCrc32;
1459
+ }
1460
+ });
1461
+ Object.defineProperty(exports, "projectedOrderbookChecksum", {
1462
+ enumerable: true,
1463
+ get: function() {
1464
+ return projectedOrderbookChecksum;
1465
+ }
1466
+ });
1467
+ Object.defineProperty(exports, "readChartHistoryText", {
1468
+ enumerable: true,
1469
+ get: function() {
1470
+ return readChartHistoryText;
1471
+ }
1472
+ });
1473
+ Object.defineProperty(exports, "snapshotToOrderbook", {
1474
+ enumerable: true,
1475
+ get: function() {
1476
+ return snapshotToOrderbook;
1477
+ }
1478
+ });