@agg-build/sdk 4.17.4 → 4.19.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 +6 -0
- package/dist/agg-websocket-CE7v1_-O.d.mts +4358 -0
- package/dist/agg-websocket-CE7v1_-O.d.ts +4358 -0
- package/dist/browser.d.mts +69 -0
- package/dist/browser.d.ts +69 -0
- package/dist/browser.js +2049 -0
- package/dist/browser.mjs +804 -0
- package/dist/chunk-QXBVA5TW.mjs +2073 -0
- package/dist/chunk-VD5UTAVD.mjs +158 -0
- package/dist/index.d.mts +439 -4244
- package/dist/index.d.ts +439 -4244
- package/dist/index.js +744 -125
- package/dist/index.mjs +283 -1763
- package/dist/market-data.d.mts +7 -0
- package/dist/market-data.d.ts +7 -0
- package/dist/market-data.js +1776 -0
- package/dist/market-data.mjs +352 -0
- package/dist/server.d.mts +16 -2
- package/dist/server.d.ts +16 -2
- package/package.json +27 -1
|
@@ -0,0 +1,1776 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __defProps = Object.defineProperties;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
10
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
11
|
+
var __spreadValues = (a, b) => {
|
|
12
|
+
for (var prop in b || (b = {}))
|
|
13
|
+
if (__hasOwnProp.call(b, prop))
|
|
14
|
+
__defNormalProp(a, prop, b[prop]);
|
|
15
|
+
if (__getOwnPropSymbols)
|
|
16
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
17
|
+
if (__propIsEnum.call(b, prop))
|
|
18
|
+
__defNormalProp(a, prop, b[prop]);
|
|
19
|
+
}
|
|
20
|
+
return a;
|
|
21
|
+
};
|
|
22
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
23
|
+
var __objRest = (source, exclude) => {
|
|
24
|
+
var target = {};
|
|
25
|
+
for (var prop in source)
|
|
26
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
27
|
+
target[prop] = source[prop];
|
|
28
|
+
if (source != null && __getOwnPropSymbols)
|
|
29
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
30
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
31
|
+
target[prop] = source[prop];
|
|
32
|
+
}
|
|
33
|
+
return target;
|
|
34
|
+
};
|
|
35
|
+
var __export = (target, all) => {
|
|
36
|
+
for (var name in all)
|
|
37
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
38
|
+
};
|
|
39
|
+
var __copyProps = (to, from, except, desc) => {
|
|
40
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
41
|
+
for (let key of __getOwnPropNames(from))
|
|
42
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
43
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
44
|
+
}
|
|
45
|
+
return to;
|
|
46
|
+
};
|
|
47
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
48
|
+
var __async = (__this, __arguments, generator) => {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
var fulfilled = (value) => {
|
|
51
|
+
try {
|
|
52
|
+
step(generator.next(value));
|
|
53
|
+
} catch (e) {
|
|
54
|
+
reject(e);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var rejected = (value) => {
|
|
58
|
+
try {
|
|
59
|
+
step(generator.throw(value));
|
|
60
|
+
} catch (e) {
|
|
61
|
+
reject(e);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
65
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/market-data.ts
|
|
70
|
+
var market_data_exports = {};
|
|
71
|
+
__export(market_data_exports, {
|
|
72
|
+
MarketDataEngine: () => MarketDataEngine,
|
|
73
|
+
decodeChartHistory: () => decodeChartHistory
|
|
74
|
+
});
|
|
75
|
+
module.exports = __toCommonJS(market_data_exports);
|
|
76
|
+
|
|
77
|
+
// src/market-data/history-decoder.ts
|
|
78
|
+
function decodeChartHistory(payload) {
|
|
79
|
+
if (payload.length > 1048576) throw new Error("Chart history exceeds decode budget");
|
|
80
|
+
const body = JSON.parse(payload);
|
|
81
|
+
if (!body || !Array.isArray(body.data) || body.data.length > 5e3 || typeof body.venueMarketOutcomeId !== "string" || typeof body.venue !== "string" || !["1m", "5m", "1h", "1d"].includes(body.resolution) || !body.meta)
|
|
82
|
+
throw new Error("Invalid chart history response");
|
|
83
|
+
const price = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
84
|
+
for (const bar of body.data) {
|
|
85
|
+
if (!Number.isSafeInteger(bar.t) || bar.t < 0 || ![bar.o, bar.h, bar.l, bar.c].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))
|
|
86
|
+
throw new Error("Invalid chart history candle");
|
|
87
|
+
}
|
|
88
|
+
const data = [...body.data].sort((a, b) => a.t - b.t);
|
|
89
|
+
if (data.some((bar, index) => index > 0 && data[index - 1].t === bar.t))
|
|
90
|
+
throw new Error("Duplicate chart history timestamp");
|
|
91
|
+
return __spreadProps(__spreadValues({}, body), { data });
|
|
92
|
+
}
|
|
93
|
+
function readChartHistoryText(response) {
|
|
94
|
+
return __async(this, null, function* () {
|
|
95
|
+
var _a, _b;
|
|
96
|
+
const maxBytes = 1048576;
|
|
97
|
+
if (Number(response.headers.get("content-length")) > maxBytes) {
|
|
98
|
+
yield (_a = response.body) == null ? void 0 : _a.cancel();
|
|
99
|
+
throw new Error("Chart history exceeds decode budget");
|
|
100
|
+
}
|
|
101
|
+
if (!((_b = response.body) == null ? void 0 : _b.getReader)) {
|
|
102
|
+
const text2 = yield response.text();
|
|
103
|
+
if (new TextEncoder().encode(text2).byteLength > maxBytes)
|
|
104
|
+
throw new Error("Chart history exceeds decode budget");
|
|
105
|
+
return text2;
|
|
106
|
+
}
|
|
107
|
+
const reader = response.body.getReader();
|
|
108
|
+
const decoder = new TextDecoder();
|
|
109
|
+
let size = 0;
|
|
110
|
+
let text = "";
|
|
111
|
+
try {
|
|
112
|
+
while (true) {
|
|
113
|
+
const { value, done } = yield reader.read();
|
|
114
|
+
if (done) return text + decoder.decode();
|
|
115
|
+
size += value.byteLength;
|
|
116
|
+
if (size > maxBytes) {
|
|
117
|
+
yield reader.cancel();
|
|
118
|
+
throw new Error("Chart history exceeds decode budget");
|
|
119
|
+
}
|
|
120
|
+
text += decoder.decode(value, { stream: true });
|
|
121
|
+
}
|
|
122
|
+
} finally {
|
|
123
|
+
reader.releaseLock();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/market-data/validate.ts
|
|
129
|
+
var object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
130
|
+
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
131
|
+
var nullable = (value) => value === null || finite(value);
|
|
132
|
+
var levels = (value, attributed) => Array.isArray(value) && value.length <= 1e4 && value.every(
|
|
133
|
+
(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))
|
|
134
|
+
);
|
|
135
|
+
function validMarketFrame(frame) {
|
|
136
|
+
if (typeof frame.outcomeId !== "string" || !frame.outcomeId || !finite(frame.timestamp) || frame.timestamp < 0)
|
|
137
|
+
return false;
|
|
138
|
+
if (frame.type === "trade")
|
|
139
|
+
return finite(frame.price) && frame.price >= 0 && finite(frame.size) && frame.size >= 0 && typeof frame.venue === "string" && (frame.side === "buy" || frame.side === "sell");
|
|
140
|
+
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))
|
|
141
|
+
return false;
|
|
142
|
+
for (const info of Object.values(frame.venues))
|
|
143
|
+
if (!object(info) || !nullable(info.bestBid) || !nullable(info.bestAsk)) return false;
|
|
144
|
+
const snapshot = frame.type === "orderbook_snapshot";
|
|
145
|
+
if (!levels(frame[snapshot ? "bids" : "bidChanges"], true) || !levels(frame[snapshot ? "asks" : "askChanges"], true))
|
|
146
|
+
return false;
|
|
147
|
+
const books = frame[snapshot ? "venueOrderbooks" : "venueDeltaBooks"];
|
|
148
|
+
return object(books) && Object.values(books).every(
|
|
149
|
+
(book) => object(book) && levels(book[snapshot ? "bids" : "bidChanges"], false) && levels(book[snapshot ? "asks" : "askChanges"], false)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/market-history.ts
|
|
154
|
+
function readBoundedBody(response, signal) {
|
|
155
|
+
return __async(this, null, function* () {
|
|
156
|
+
var _a, _b;
|
|
157
|
+
if (!response.body) throw new Error("Missing history body");
|
|
158
|
+
const reader = response.body.getReader();
|
|
159
|
+
const chunks = [];
|
|
160
|
+
let size = 0;
|
|
161
|
+
const abort = () => {
|
|
162
|
+
void reader.cancel(signal.reason).catch(() => void 0);
|
|
163
|
+
};
|
|
164
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
165
|
+
try {
|
|
166
|
+
while (true) {
|
|
167
|
+
if (signal.aborted) throw (_a = signal.reason) != null ? _a : new Error("History request aborted");
|
|
168
|
+
const { done, value } = yield reader.read();
|
|
169
|
+
if (signal.aborted) throw (_b = signal.reason) != null ? _b : new Error("History request aborted");
|
|
170
|
+
if (done) break;
|
|
171
|
+
size += value.byteLength;
|
|
172
|
+
if (size > 262144) {
|
|
173
|
+
yield reader.cancel();
|
|
174
|
+
throw new Error("Oversized history object");
|
|
175
|
+
}
|
|
176
|
+
chunks.push(value);
|
|
177
|
+
}
|
|
178
|
+
const bytes = new Uint8Array(size);
|
|
179
|
+
let offset = 0;
|
|
180
|
+
for (const chunk of chunks) {
|
|
181
|
+
bytes.set(chunk, offset);
|
|
182
|
+
offset += chunk.byteLength;
|
|
183
|
+
}
|
|
184
|
+
return bytes;
|
|
185
|
+
} finally {
|
|
186
|
+
signal.removeEventListener("abort", abort);
|
|
187
|
+
reader.releaseLock();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function loadImmutableHistory(_0) {
|
|
192
|
+
return __async(this, arguments, function* ({
|
|
193
|
+
manifest,
|
|
194
|
+
fetch: fetcher = globalThis.fetch,
|
|
195
|
+
signal,
|
|
196
|
+
digest
|
|
197
|
+
}) {
|
|
198
|
+
if (manifest.version !== 1 || !["venue-ohlcv-v1", "polymarket-price-samples-v1", "kalshi-ohlcv-quote-fallback-v2"].includes(
|
|
199
|
+
manifest.semantics
|
|
200
|
+
) || manifest.chunks.length > 8 || manifest.chunks.reduce((n, c) => n + c.points, 0) > 4096 || !manifest.meta.complete) {
|
|
201
|
+
throw new Error("History manifest is incomplete or unsupported");
|
|
202
|
+
}
|
|
203
|
+
const hash = digest != null ? digest : ((bytes) => __async(null, null, function* () {
|
|
204
|
+
var _a;
|
|
205
|
+
if (!((_a = globalThis.crypto) == null ? void 0 : _a.subtle))
|
|
206
|
+
throw new Error(
|
|
207
|
+
"History integrity requires WebCrypto or an injected SHA-256 implementation"
|
|
208
|
+
);
|
|
209
|
+
const value = yield globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
210
|
+
return Array.from(new Uint8Array(value), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
211
|
+
}));
|
|
212
|
+
const results = new Array(manifest.chunks.length);
|
|
213
|
+
let next = 0;
|
|
214
|
+
const controller = new AbortController();
|
|
215
|
+
const deadline = setTimeout(
|
|
216
|
+
() => controller.abort(new Error("History download budget exhausted")),
|
|
217
|
+
2e3
|
|
218
|
+
);
|
|
219
|
+
const abort = () => controller.abort(signal == null ? void 0 : signal.reason);
|
|
220
|
+
signal == null ? void 0 : signal.addEventListener("abort", abort, { once: true });
|
|
221
|
+
if (signal == null ? void 0 : signal.aborted) abort();
|
|
222
|
+
try {
|
|
223
|
+
yield Promise.all(
|
|
224
|
+
Array.from({ length: Math.min(2, manifest.chunks.length) }, () => __async(null, null, function* () {
|
|
225
|
+
var _a;
|
|
226
|
+
while (next < manifest.chunks.length) {
|
|
227
|
+
if (controller.signal.aborted)
|
|
228
|
+
throw (_a = controller.signal.reason) != null ? _a : new Error("History request aborted");
|
|
229
|
+
const index = next++;
|
|
230
|
+
const chunk = manifest.chunks[index];
|
|
231
|
+
if (chunk.points < 0 || chunk.points > 512 || chunk.from >= chunk.to || new URL(chunk.url).protocol !== "https:")
|
|
232
|
+
throw new Error("Invalid history reference");
|
|
233
|
+
const response = yield fetcher(chunk.url, {
|
|
234
|
+
signal: controller.signal,
|
|
235
|
+
credentials: "omit"
|
|
236
|
+
});
|
|
237
|
+
if (!response.ok) throw new Error(`History download failed (${response.status})`);
|
|
238
|
+
if (Number(response.headers.get("content-length")) > 262144)
|
|
239
|
+
throw new Error("Oversized history object");
|
|
240
|
+
const bytes = yield readBoundedBody(response, controller.signal);
|
|
241
|
+
if (bytes.byteLength > 262144 || (yield hash(bytes)) !== chunk.sha256)
|
|
242
|
+
throw new Error("History integrity mismatch");
|
|
243
|
+
const object2 = JSON.parse(new TextDecoder().decode(bytes));
|
|
244
|
+
if (object2.version !== 1 || object2.venueMarketOutcomeId !== manifest.venueMarketOutcomeId || object2.venue !== manifest.venue || object2.resolution !== manifest.resolution || object2.semantics !== manifest.semantics || object2.from !== chunk.from || object2.to !== chunk.to || !Array.isArray(object2.data) || object2.data.length !== chunk.points) {
|
|
245
|
+
throw new Error("History identity mismatch");
|
|
246
|
+
}
|
|
247
|
+
let previous = -Infinity;
|
|
248
|
+
for (const bar of object2.data) {
|
|
249
|
+
if (!Number.isSafeInteger(bar.t) || bar.t <= previous || bar.t < chunk.from || bar.t >= chunk.to || ![bar.o, bar.h, bar.l, bar.c].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))
|
|
250
|
+
throw new Error("Invalid history candle");
|
|
251
|
+
previous = bar.t;
|
|
252
|
+
}
|
|
253
|
+
results[index] = object2.data;
|
|
254
|
+
}
|
|
255
|
+
}))
|
|
256
|
+
);
|
|
257
|
+
const bars = results.flat().filter((b) => b.t >= manifest.from && b.t < manifest.to).sort((a, b) => a.t - b.t);
|
|
258
|
+
if (bars.some((b, i) => i > 0 && bars[i - 1].t === b.t))
|
|
259
|
+
throw new Error("Overlapping history chunks");
|
|
260
|
+
return bars;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
controller.abort(error);
|
|
263
|
+
throw error;
|
|
264
|
+
} finally {
|
|
265
|
+
clearTimeout(deadline);
|
|
266
|
+
signal == null ? void 0 : signal.removeEventListener("abort", abort);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ../common/src/orderbook-checksum.ts
|
|
272
|
+
function orderbookCrc32(value) {
|
|
273
|
+
let crc = 4294967295;
|
|
274
|
+
for (let i = 0; i < value.length; i++) {
|
|
275
|
+
crc ^= value.charCodeAt(i);
|
|
276
|
+
for (let bit = 0; bit < 8; bit++) crc = crc >>> 1 ^ (crc & 1 ? 3988292384 : 0);
|
|
277
|
+
}
|
|
278
|
+
return (crc ^ 4294967295) >>> 0;
|
|
279
|
+
}
|
|
280
|
+
function projectedOrderbookChecksum(book) {
|
|
281
|
+
let result = 0;
|
|
282
|
+
for (const level of book.bids) result ^= orderbookCrc32(`B${level.price}:${level.size}`);
|
|
283
|
+
for (const level of book.asks) result ^= orderbookCrc32(`A${level.price}:${level.size}`);
|
|
284
|
+
return result >>> 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/orderbook-utils.ts
|
|
288
|
+
var PRICE_KEY_SCALE = 1e9;
|
|
289
|
+
function computeChecksum(book) {
|
|
290
|
+
return projectedOrderbookChecksum(book);
|
|
291
|
+
}
|
|
292
|
+
function attributedToLevel(wire) {
|
|
293
|
+
return { price: wire[0], size: wire[1], venues: __spreadValues({}, wire[2]) };
|
|
294
|
+
}
|
|
295
|
+
function venueLevelToLevel(wire) {
|
|
296
|
+
return { price: wire[0], size: wire[1] };
|
|
297
|
+
}
|
|
298
|
+
function convertVenueBooks(raw) {
|
|
299
|
+
const result = {};
|
|
300
|
+
for (const [venue, book] of Object.entries(raw)) {
|
|
301
|
+
result[venue] = {
|
|
302
|
+
bids: book.bids.map(venueLevelToLevel),
|
|
303
|
+
asks: book.asks.map(venueLevelToLevel)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
function snapshotToOrderbook(snapshot) {
|
|
309
|
+
return {
|
|
310
|
+
marketId: snapshot.outcomeId,
|
|
311
|
+
outcomeId: snapshot.outcomeId,
|
|
312
|
+
bids: snapshot.bids.map(attributedToLevel),
|
|
313
|
+
asks: snapshot.asks.map(attributedToLevel),
|
|
314
|
+
venueOrderbooks: convertVenueBooks(snapshot.venueOrderbooks),
|
|
315
|
+
venues: __spreadValues({}, snapshot.venues),
|
|
316
|
+
midpoint: snapshot.midpoint,
|
|
317
|
+
spread: snapshot.spread,
|
|
318
|
+
seq: snapshot.seq,
|
|
319
|
+
checksum: snapshot.checksum,
|
|
320
|
+
timestamp: snapshot.timestamp
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function toPriceKey(price) {
|
|
324
|
+
return String(Math.round(price * PRICE_KEY_SCALE));
|
|
325
|
+
}
|
|
326
|
+
function mergeVenueLevelChanges(primary, fallback) {
|
|
327
|
+
if (!(primary == null ? void 0 : primary.length)) return fallback ? [...fallback] : [];
|
|
328
|
+
if (!(fallback == null ? void 0 : fallback.length)) return [...primary];
|
|
329
|
+
const changesByPrice = /* @__PURE__ */ new Map();
|
|
330
|
+
for (const change of fallback) {
|
|
331
|
+
changesByPrice.set(toPriceKey(change[0]), change);
|
|
332
|
+
}
|
|
333
|
+
for (const change of primary) {
|
|
334
|
+
changesByPrice.set(toPriceKey(change[0]), change);
|
|
335
|
+
}
|
|
336
|
+
return [...changesByPrice.values()];
|
|
337
|
+
}
|
|
338
|
+
function deriveVenueChangesFromAttributedChanges(previousLevels, changes) {
|
|
339
|
+
var _a, _b;
|
|
340
|
+
const previousLevelsByPrice = new Map(
|
|
341
|
+
previousLevels.map((level) => [toPriceKey(level.price), level])
|
|
342
|
+
);
|
|
343
|
+
const venueChanges = {};
|
|
344
|
+
for (const [price, size, nextVenuesRaw] of changes) {
|
|
345
|
+
const key = toPriceKey(price);
|
|
346
|
+
const previousLevel = previousLevelsByPrice.get(key);
|
|
347
|
+
const previousVenues = (_a = previousLevel == null ? void 0 : previousLevel.venues) != null ? _a : {};
|
|
348
|
+
const nextVenues = nextVenuesRaw != null ? nextVenuesRaw : {};
|
|
349
|
+
const affectedVenues = /* @__PURE__ */ new Set([...Object.keys(previousVenues), ...Object.keys(nextVenues)]);
|
|
350
|
+
for (const venue of affectedVenues) {
|
|
351
|
+
const nextSize = Number((_b = nextVenues[venue]) != null ? _b : 0);
|
|
352
|
+
if (!Number.isFinite(nextSize)) continue;
|
|
353
|
+
if (!venueChanges[venue]) {
|
|
354
|
+
venueChanges[venue] = [];
|
|
355
|
+
}
|
|
356
|
+
venueChanges[venue].push([price, nextSize]);
|
|
357
|
+
}
|
|
358
|
+
if (size === 0) {
|
|
359
|
+
previousLevelsByPrice.delete(key);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
previousLevelsByPrice.set(key, {
|
|
363
|
+
price,
|
|
364
|
+
size,
|
|
365
|
+
venues: __spreadValues({}, nextVenues)
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return venueChanges;
|
|
369
|
+
}
|
|
370
|
+
var IndexedOrderbook = class {
|
|
371
|
+
constructor(state) {
|
|
372
|
+
this.venueBooks = /* @__PURE__ */ new Map();
|
|
373
|
+
this.bids = new Map(state.bids.map((level) => [toPriceKey(level.price), level]));
|
|
374
|
+
this.asks = new Map(state.asks.map((level) => [toPriceKey(level.price), level]));
|
|
375
|
+
for (const [venue, book] of Object.entries(state.venueOrderbooks)) {
|
|
376
|
+
this.venueBooks.set(venue, {
|
|
377
|
+
bids: new Map(book.bids.map((level) => [toPriceKey(level.price), level])),
|
|
378
|
+
asks: new Map(book.asks.map((level) => [toPriceKey(level.price), level]))
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
const _a = state, { bids: _bids, asks: _asks, venueOrderbooks: _venueBooks } = _a, metadata = __objRest(_a, ["bids", "asks", "venueOrderbooks"]);
|
|
382
|
+
this.metadata = metadata;
|
|
383
|
+
this.cached = state;
|
|
384
|
+
this.checksumValue = computeChecksum(state);
|
|
385
|
+
}
|
|
386
|
+
get seq() {
|
|
387
|
+
return this.metadata.seq;
|
|
388
|
+
}
|
|
389
|
+
get checksum() {
|
|
390
|
+
return this.checksumValue;
|
|
391
|
+
}
|
|
392
|
+
/** Verify before committing, including duplicate-price changes within a frame. */
|
|
393
|
+
apply(delta) {
|
|
394
|
+
var _a;
|
|
395
|
+
let checksum = this.checksumValue;
|
|
396
|
+
for (const [levels2, changes, side] of [
|
|
397
|
+
[this.bids, delta.bidChanges, "B"],
|
|
398
|
+
[this.asks, delta.askChanges, "A"]
|
|
399
|
+
]) {
|
|
400
|
+
const changed = /* @__PURE__ */ new Map();
|
|
401
|
+
for (const [price, size, venues] of changes) {
|
|
402
|
+
const key = toPriceKey(price);
|
|
403
|
+
const previous = changed.has(key) ? changed.get(key) : levels2.get(key);
|
|
404
|
+
if (previous) checksum ^= orderbookCrc32(`${side}${previous.price}:${previous.size}`);
|
|
405
|
+
if (size !== 0) checksum ^= orderbookCrc32(`${side}${price}:${size}`);
|
|
406
|
+
changed.set(key, size === 0 ? void 0 : { price, size, venues: __spreadValues({}, venues) });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
checksum >>>= 0;
|
|
410
|
+
if (checksum !== delta.checksum) return checksum;
|
|
411
|
+
const previousBids = delta.bidChanges.flatMap(
|
|
412
|
+
([price]) => {
|
|
413
|
+
var _a2;
|
|
414
|
+
return (_a2 = this.bids.get(toPriceKey(price))) != null ? _a2 : [];
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
const previousAsks = delta.askChanges.flatMap(
|
|
418
|
+
([price]) => {
|
|
419
|
+
var _a2;
|
|
420
|
+
return (_a2 = this.asks.get(toPriceKey(price))) != null ? _a2 : [];
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
const impliedBids = deriveVenueChangesFromAttributedChanges(previousBids, delta.bidChanges);
|
|
424
|
+
const impliedAsks = deriveVenueChangesFromAttributedChanges(previousAsks, delta.askChanges);
|
|
425
|
+
for (const venue of /* @__PURE__ */ new Set([
|
|
426
|
+
...Object.keys(delta.venueDeltaBooks),
|
|
427
|
+
...Object.keys(impliedBids),
|
|
428
|
+
...Object.keys(impliedAsks)
|
|
429
|
+
])) {
|
|
430
|
+
let book = this.venueBooks.get(venue);
|
|
431
|
+
if (!book) {
|
|
432
|
+
book = { bids: /* @__PURE__ */ new Map(), asks: /* @__PURE__ */ new Map() };
|
|
433
|
+
this.venueBooks.set(venue, book);
|
|
434
|
+
}
|
|
435
|
+
const explicit = delta.venueDeltaBooks[venue];
|
|
436
|
+
for (const [levels2, changes] of [
|
|
437
|
+
[book.bids, mergeVenueLevelChanges(explicit == null ? void 0 : explicit.bidChanges, impliedBids[venue])],
|
|
438
|
+
[book.asks, mergeVenueLevelChanges(explicit == null ? void 0 : explicit.askChanges, impliedAsks[venue])]
|
|
439
|
+
]) {
|
|
440
|
+
for (const [price, size] of changes) {
|
|
441
|
+
if (size === 0) levels2.delete(toPriceKey(price));
|
|
442
|
+
else levels2.set(toPriceKey(price), { price, size });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
for (const [levels2, changes] of [
|
|
447
|
+
[this.bids, delta.bidChanges],
|
|
448
|
+
[this.asks, delta.askChanges]
|
|
449
|
+
]) {
|
|
450
|
+
for (const [price, size, venues] of changes) {
|
|
451
|
+
if (size === 0) levels2.delete(toPriceKey(price));
|
|
452
|
+
else levels2.set(toPriceKey(price), { price, size, venues: __spreadValues({}, venues) });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
this.metadata = __spreadProps(__spreadValues({}, this.metadata), {
|
|
456
|
+
outcomeId: (_a = this.metadata.outcomeId) != null ? _a : delta.outcomeId,
|
|
457
|
+
venues: __spreadValues(__spreadValues({}, this.metadata.venues), delta.venues),
|
|
458
|
+
midpoint: delta.midpoint,
|
|
459
|
+
spread: delta.spread,
|
|
460
|
+
seq: delta.seq,
|
|
461
|
+
checksum: delta.checksum,
|
|
462
|
+
timestamp: delta.timestamp
|
|
463
|
+
});
|
|
464
|
+
this.checksumValue = checksum;
|
|
465
|
+
this.cached = null;
|
|
466
|
+
return checksum;
|
|
467
|
+
}
|
|
468
|
+
view() {
|
|
469
|
+
if (this.cached) return this.cached;
|
|
470
|
+
const bids = (levels2) => [...levels2.values()].sort((a, b) => b.price - a.price);
|
|
471
|
+
const asks = (levels2) => [...levels2.values()].sort((a, b) => a.price - b.price);
|
|
472
|
+
this.cached = __spreadProps(__spreadValues({}, this.metadata), {
|
|
473
|
+
bids: bids(this.bids),
|
|
474
|
+
asks: asks(this.asks),
|
|
475
|
+
venueOrderbooks: Object.fromEntries(
|
|
476
|
+
[...this.venueBooks].map(([venue, book]) => [
|
|
477
|
+
venue,
|
|
478
|
+
{ bids: bids(book.bids), asks: asks(book.asks) }
|
|
479
|
+
])
|
|
480
|
+
)
|
|
481
|
+
});
|
|
482
|
+
return this.cached;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// src/agg-websocket.ts
|
|
487
|
+
var RECONNECT_BASE_MS = 1e3;
|
|
488
|
+
var RECONNECT_MAX_MS = 3e4;
|
|
489
|
+
var STALE_TIMEOUT_MS = 45e3;
|
|
490
|
+
var ORDERBOOK_DEBUG_TOP_LEVELS = 5;
|
|
491
|
+
var AggWebSocket = class {
|
|
492
|
+
constructor(options) {
|
|
493
|
+
this.ws = null;
|
|
494
|
+
// ── Subscription tracking ──
|
|
495
|
+
this.subs = /* @__PURE__ */ new Map();
|
|
496
|
+
this.pendingSubs = /* @__PURE__ */ new Set();
|
|
497
|
+
this.pendingTradeSubs = /* @__PURE__ */ new Set();
|
|
498
|
+
this.pendingResnapshots = /* @__PURE__ */ new Set();
|
|
499
|
+
// ── Post-connect batching (coalesces same-tick sub/unsub calls into one frame) ──
|
|
500
|
+
this.pendingFlushSubs = /* @__PURE__ */ new Set();
|
|
501
|
+
this.pendingFlushUnsubs = /* @__PURE__ */ new Set();
|
|
502
|
+
this.pendingFlushTradeSubs = /* @__PURE__ */ new Set();
|
|
503
|
+
this.pendingFlushTradeUnsubs = /* @__PURE__ */ new Set();
|
|
504
|
+
this.pendingFlushArbSubs = /* @__PURE__ */ new Set();
|
|
505
|
+
this.pendingFlushArbUnsubs = /* @__PURE__ */ new Set();
|
|
506
|
+
this.flushScheduled = false;
|
|
507
|
+
// ── Arb subscriptions ──
|
|
508
|
+
this.arbSubs = /* @__PURE__ */ new Map();
|
|
509
|
+
this.arbFeedCbs = /* @__PURE__ */ new Set();
|
|
510
|
+
/** Latest arb market update per marketId (last-value-wins, loss-tolerant). */
|
|
511
|
+
this.latestArb = /* @__PURE__ */ new Map();
|
|
512
|
+
// ── Orderbook state per market ──
|
|
513
|
+
this.books = /* @__PURE__ */ new Map();
|
|
514
|
+
/** Markets currently resyncing (awaiting snapshot after seq gap / checksum mismatch). */
|
|
515
|
+
this.resyncing = /* @__PURE__ */ new Set();
|
|
516
|
+
// ── Connection state ──
|
|
517
|
+
this.connecting = false;
|
|
518
|
+
this._connected = false;
|
|
519
|
+
this.reconnectAttempts = 0;
|
|
520
|
+
this.reconnectTimer = null;
|
|
521
|
+
this.staleTimer = null;
|
|
522
|
+
this.lastMessageTime = 0;
|
|
523
|
+
this.destroyed = false;
|
|
524
|
+
this.historyControllers = /* @__PURE__ */ new Set();
|
|
525
|
+
var _a, _b, _c;
|
|
526
|
+
this.deferBookViews = (_a = options.deferBookViews) != null ? _a : false;
|
|
527
|
+
this.url = options.url;
|
|
528
|
+
this.callbacks = (_b = options.callbacks) != null ? _b : {};
|
|
529
|
+
this.initialToken = options.initialToken;
|
|
530
|
+
this.enableLogs = (_c = options.enableLogs) != null ? _c : false;
|
|
531
|
+
}
|
|
532
|
+
// ── Public API ──
|
|
533
|
+
get connected() {
|
|
534
|
+
return this._connected;
|
|
535
|
+
}
|
|
536
|
+
/** Decode dynamic authenticated REST bytes in the same processing environment as live data. */
|
|
537
|
+
decodeHistory(payload) {
|
|
538
|
+
return __async(this, null, function* () {
|
|
539
|
+
return decodeChartHistory(payload);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
/** Immutable history decoder. Browser adapters execute this inside their Worker. */
|
|
543
|
+
loadHistory(manifest, options) {
|
|
544
|
+
return __async(this, null, function* () {
|
|
545
|
+
var _a, _b, _c;
|
|
546
|
+
if (this.destroyed) throw new Error("Market-data client disposed");
|
|
547
|
+
const controller = new AbortController();
|
|
548
|
+
const abort = () => {
|
|
549
|
+
var _a2;
|
|
550
|
+
return controller.abort((_a2 = options == null ? void 0 : options.signal) == null ? void 0 : _a2.reason);
|
|
551
|
+
};
|
|
552
|
+
(_a = options == null ? void 0 : options.signal) == null ? void 0 : _a.addEventListener("abort", abort, { once: true });
|
|
553
|
+
if ((_b = options == null ? void 0 : options.signal) == null ? void 0 : _b.aborted) abort();
|
|
554
|
+
this.historyControllers.add(controller);
|
|
555
|
+
try {
|
|
556
|
+
controller.signal.throwIfAborted();
|
|
557
|
+
const result = yield loadImmutableHistory({ manifest, signal: controller.signal });
|
|
558
|
+
controller.signal.throwIfAborted();
|
|
559
|
+
return result;
|
|
560
|
+
} finally {
|
|
561
|
+
this.historyControllers.delete(controller);
|
|
562
|
+
(_c = options == null ? void 0 : options.signal) == null ? void 0 : _c.removeEventListener("abort", abort);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
/** Optional framework-independent host diagnostics, cleared on disposal. */
|
|
567
|
+
setHostDiagnostics(provider) {
|
|
568
|
+
this.hostDiagnostics = provider;
|
|
569
|
+
}
|
|
570
|
+
/** On-demand ownership diagnostics; contains no credentials or user/account data. */
|
|
571
|
+
getDiagnostics() {
|
|
572
|
+
var _a;
|
|
573
|
+
return {
|
|
574
|
+
connected: this.connected,
|
|
575
|
+
host: (_a = this.hostDiagnostics) == null ? void 0 : _a.call(this),
|
|
576
|
+
transport: this.destroyed ? "disposed" : "direct",
|
|
577
|
+
orderbookOutcomeIds: this.activeOrderbookMarkets(),
|
|
578
|
+
tradeOutcomeIds: this.activeTradeMarkets(),
|
|
579
|
+
arbMarketIds: this.activeArbMarkets(),
|
|
580
|
+
arbFeedSubscribers: this.arbFeedCbs.size,
|
|
581
|
+
cachedBooks: this.books.size,
|
|
582
|
+
cachedArb: this.latestArb.size,
|
|
583
|
+
resyncingBooks: this.resyncing.size,
|
|
584
|
+
pendingResnapshots: this.pendingResnapshots.size,
|
|
585
|
+
pendingHistoryRequests: this.historyControllers.size
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
/** Get the current orderbook state for an outcome (or null if no snapshot received yet). */
|
|
589
|
+
getBook(outcomeId) {
|
|
590
|
+
var _a, _b;
|
|
591
|
+
return (_b = (_a = this.books.get(outcomeId)) == null ? void 0 : _a.view()) != null ? _b : null;
|
|
592
|
+
}
|
|
593
|
+
/** Whether an outcome is currently resyncing (waiting for fresh snapshot). */
|
|
594
|
+
isResyncing(outcomeId) {
|
|
595
|
+
return this.resyncing.has(outcomeId);
|
|
596
|
+
}
|
|
597
|
+
/** Get the latest arb update for a market (or null if none received yet). */
|
|
598
|
+
getArb(marketId) {
|
|
599
|
+
var _a;
|
|
600
|
+
return (_a = this.latestArb.get(marketId)) != null ? _a : null;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Subscribe to per-market arb updates for a given marketId.
|
|
604
|
+
* Ref-counted: multiple callers for the same marketId share one server subscription.
|
|
605
|
+
* Returns an unsubscribe function.
|
|
606
|
+
*/
|
|
607
|
+
subscribeArb(marketId, cb) {
|
|
608
|
+
let entry = this.arbSubs.get(marketId);
|
|
609
|
+
if (!entry) {
|
|
610
|
+
entry = { refCount: 0, cbs: /* @__PURE__ */ new Set() };
|
|
611
|
+
this.arbSubs.set(marketId, entry);
|
|
612
|
+
this.ensureConnected();
|
|
613
|
+
if (this._connected) {
|
|
614
|
+
this.pendingFlushArbUnsubs.delete(marketId);
|
|
615
|
+
this.pendingFlushArbSubs.add(marketId);
|
|
616
|
+
this.scheduleFlush();
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
entry.cbs.add(cb);
|
|
620
|
+
entry.refCount++;
|
|
621
|
+
return () => {
|
|
622
|
+
const e = this.arbSubs.get(marketId);
|
|
623
|
+
if (!e) return;
|
|
624
|
+
e.cbs.delete(cb);
|
|
625
|
+
e.refCount--;
|
|
626
|
+
if (e.refCount <= 0) {
|
|
627
|
+
this.arbSubs.delete(marketId);
|
|
628
|
+
if (this._connected) {
|
|
629
|
+
if (this.pendingFlushArbSubs.has(marketId)) {
|
|
630
|
+
this.pendingFlushArbSubs.delete(marketId);
|
|
631
|
+
} else {
|
|
632
|
+
this.pendingFlushArbUnsubs.add(marketId);
|
|
633
|
+
this.scheduleFlush();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Subscribe to the global arb-feed batches.
|
|
641
|
+
* Only one server subscription is maintained regardless of how many callers.
|
|
642
|
+
* Returns an unsubscribe function.
|
|
643
|
+
*/
|
|
644
|
+
subscribeArbFeed(cb) {
|
|
645
|
+
const wasEmpty = this.arbFeedCbs.size === 0;
|
|
646
|
+
this.arbFeedCbs.add(cb);
|
|
647
|
+
if (wasEmpty) {
|
|
648
|
+
this.ensureConnected();
|
|
649
|
+
if (this._connected) {
|
|
650
|
+
this.send({ action: "subscribe", channel: "arb-feed" });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return () => {
|
|
654
|
+
this.arbFeedCbs.delete(cb);
|
|
655
|
+
if (this.arbFeedCbs.size === 0 && this._connected) {
|
|
656
|
+
this.send({ action: "unsubscribe", 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") {
|
|
668
|
+
return this._subscribeTrades(outcomeId);
|
|
669
|
+
}
|
|
670
|
+
let sub = this.subs.get(outcomeId);
|
|
671
|
+
if (!sub) {
|
|
672
|
+
sub = { refCount: 0, tradeRefs: 0 };
|
|
673
|
+
this.subs.set(outcomeId, sub);
|
|
674
|
+
}
|
|
675
|
+
if (sub.refCount === 0) {
|
|
676
|
+
this.pendingSubs.add(outcomeId);
|
|
677
|
+
this.ensureConnected();
|
|
678
|
+
if (this._connected) {
|
|
679
|
+
this.pendingFlushUnsubs.delete(outcomeId);
|
|
680
|
+
this.pendingFlushSubs.add(outcomeId);
|
|
681
|
+
this.pendingSubs.delete(outcomeId);
|
|
682
|
+
this.scheduleFlush();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
sub.refCount++;
|
|
686
|
+
let released = false;
|
|
687
|
+
return () => {
|
|
688
|
+
if (released) return;
|
|
689
|
+
released = true;
|
|
690
|
+
const s = this.subs.get(outcomeId);
|
|
691
|
+
if (!s) return;
|
|
692
|
+
s.refCount--;
|
|
693
|
+
if (s.refCount <= 0) {
|
|
694
|
+
s.refCount = 0;
|
|
695
|
+
if (s.tradeRefs <= 0) {
|
|
696
|
+
this.subs.delete(outcomeId);
|
|
697
|
+
this.books.delete(outcomeId);
|
|
698
|
+
}
|
|
699
|
+
this.resyncing.delete(outcomeId);
|
|
700
|
+
if (this._connected) {
|
|
701
|
+
if (this.pendingFlushSubs.has(outcomeId)) {
|
|
702
|
+
this.pendingFlushSubs.delete(outcomeId);
|
|
703
|
+
} else {
|
|
704
|
+
this.pendingFlushUnsubs.add(outcomeId);
|
|
705
|
+
this.scheduleFlush();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
this.pendingSubs.delete(outcomeId);
|
|
709
|
+
this.pendingResnapshots.delete(outcomeId);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Subscribe to an outcome's trade feed.
|
|
715
|
+
* @deprecated Use `subscribe(outcomeId, "trades")` instead.
|
|
716
|
+
* Returns an unsubscribe function.
|
|
717
|
+
*/
|
|
718
|
+
subscribeTrades(outcomeId) {
|
|
719
|
+
return this._subscribeTrades(outcomeId);
|
|
720
|
+
}
|
|
721
|
+
_subscribeTrades(outcomeId) {
|
|
722
|
+
let sub = this.subs.get(outcomeId);
|
|
723
|
+
if (!sub) {
|
|
724
|
+
sub = { refCount: 0, tradeRefs: 0 };
|
|
725
|
+
this.subs.set(outcomeId, sub);
|
|
726
|
+
}
|
|
727
|
+
if (sub.tradeRefs === 0) {
|
|
728
|
+
this.pendingTradeSubs.add(outcomeId);
|
|
729
|
+
this.ensureConnected();
|
|
730
|
+
if (this._connected) {
|
|
731
|
+
this.pendingFlushTradeUnsubs.delete(outcomeId);
|
|
732
|
+
this.pendingFlushTradeSubs.add(outcomeId);
|
|
733
|
+
this.pendingTradeSubs.delete(outcomeId);
|
|
734
|
+
this.scheduleFlush();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
sub.tradeRefs++;
|
|
738
|
+
let released = false;
|
|
739
|
+
return () => {
|
|
740
|
+
if (released) return;
|
|
741
|
+
released = true;
|
|
742
|
+
const s = this.subs.get(outcomeId);
|
|
743
|
+
if (!s) return;
|
|
744
|
+
s.tradeRefs--;
|
|
745
|
+
if (s.tradeRefs <= 0) {
|
|
746
|
+
s.tradeRefs = 0;
|
|
747
|
+
if (this._connected) {
|
|
748
|
+
if (this.pendingFlushTradeSubs.has(outcomeId)) {
|
|
749
|
+
this.pendingFlushTradeSubs.delete(outcomeId);
|
|
750
|
+
} else {
|
|
751
|
+
this.pendingFlushTradeUnsubs.add(outcomeId);
|
|
752
|
+
this.scheduleFlush();
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
this.pendingTradeSubs.delete(outcomeId);
|
|
756
|
+
this.pendingResnapshots.delete(outcomeId);
|
|
757
|
+
if (s.refCount <= 0) {
|
|
758
|
+
this.subs.delete(outcomeId);
|
|
759
|
+
this.books.delete(outcomeId);
|
|
760
|
+
this.resyncing.delete(outcomeId);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
/** Send a JWT to authenticate (or re-authenticate) the connection. */
|
|
766
|
+
authenticate(token) {
|
|
767
|
+
if (this._connected) {
|
|
768
|
+
this.send({ action: "authenticate", token });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/** Request a fresh snapshot for one or more subscribed outcomes. */
|
|
772
|
+
resnapshot(outcomeIdOrIds) {
|
|
773
|
+
const outcomeIds = Array.isArray(outcomeIdOrIds) ? outcomeIdOrIds : [outcomeIdOrIds];
|
|
774
|
+
outcomeIds.forEach((outcomeId) => {
|
|
775
|
+
this.requestResnapshot(outcomeId, true);
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
/** Connect (idempotent). Called automatically on first subscribe. */
|
|
779
|
+
connect() {
|
|
780
|
+
this.ensureConnected();
|
|
781
|
+
}
|
|
782
|
+
/** Disconnect and clean up. Reusable — call connect() to reconnect. */
|
|
783
|
+
disconnect() {
|
|
784
|
+
var _a, _b;
|
|
785
|
+
this.cancelReconnect();
|
|
786
|
+
this.cancelStaleTimer();
|
|
787
|
+
this._connected = false;
|
|
788
|
+
this.connecting = false;
|
|
789
|
+
if (this.ws) {
|
|
790
|
+
this.ws.onclose = null;
|
|
791
|
+
this.ws.close();
|
|
792
|
+
this.ws = null;
|
|
793
|
+
}
|
|
794
|
+
(_b = (_a = this.callbacks).onConnectionStateChange) == null ? void 0 : _b.call(_a, false);
|
|
795
|
+
}
|
|
796
|
+
/** Permanently destroy. Cannot reconnect after this. */
|
|
797
|
+
destroy() {
|
|
798
|
+
this.destroyed = true;
|
|
799
|
+
for (const controller of this.historyControllers)
|
|
800
|
+
controller.abort(new Error("Market-data client disposed"));
|
|
801
|
+
this.historyControllers.clear();
|
|
802
|
+
this.disconnect();
|
|
803
|
+
this.hostDiagnostics = void 0;
|
|
804
|
+
this.callbacks = {};
|
|
805
|
+
this.subs.clear();
|
|
806
|
+
this.books.clear();
|
|
807
|
+
this.resyncing.clear();
|
|
808
|
+
this.pendingSubs.clear();
|
|
809
|
+
this.pendingTradeSubs.clear();
|
|
810
|
+
this.pendingResnapshots.clear();
|
|
811
|
+
this.pendingFlushSubs.clear();
|
|
812
|
+
this.pendingFlushUnsubs.clear();
|
|
813
|
+
this.pendingFlushTradeSubs.clear();
|
|
814
|
+
this.pendingFlushTradeUnsubs.clear();
|
|
815
|
+
this.arbSubs.clear();
|
|
816
|
+
this.arbFeedCbs.clear();
|
|
817
|
+
this.latestArb.clear();
|
|
818
|
+
this.pendingFlushArbSubs.clear();
|
|
819
|
+
this.pendingFlushArbUnsubs.clear();
|
|
820
|
+
this.flushScheduled = false;
|
|
821
|
+
}
|
|
822
|
+
/** Update callbacks (e.g., after React re-render). */
|
|
823
|
+
setCallbacks(callbacks) {
|
|
824
|
+
this.callbacks = callbacks;
|
|
825
|
+
}
|
|
826
|
+
// ── Connection management ──
|
|
827
|
+
ensureConnected() {
|
|
828
|
+
var _a;
|
|
829
|
+
if (this.destroyed) return;
|
|
830
|
+
if (((_a = this.ws) == null ? void 0 : _a.readyState) === WebSocket.OPEN || this.connecting) return;
|
|
831
|
+
this.doConnect();
|
|
832
|
+
}
|
|
833
|
+
doConnect() {
|
|
834
|
+
if (this.destroyed) return;
|
|
835
|
+
this.connecting = true;
|
|
836
|
+
const ws = new WebSocket(this.url);
|
|
837
|
+
this.ws = ws;
|
|
838
|
+
ws.onopen = () => {
|
|
839
|
+
if (this.destroyed || this.ws !== ws) return;
|
|
840
|
+
this.connecting = false;
|
|
841
|
+
this.reconnectAttempts = 0;
|
|
842
|
+
this.resetStaleTimer();
|
|
843
|
+
if (this.initialToken) {
|
|
844
|
+
this.send({ action: "authenticate", token: this.initialToken });
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
ws.onmessage = (event) => {
|
|
848
|
+
var _a, _b;
|
|
849
|
+
if (this.destroyed || this.ws !== ws) return;
|
|
850
|
+
this.lastMessageTime = Date.now();
|
|
851
|
+
(_b = (_a = this.callbacks).onFrameReceived) == null ? void 0 : _b.call(_a, this.lastMessageTime);
|
|
852
|
+
this.resetStaleTimer();
|
|
853
|
+
try {
|
|
854
|
+
const raw = JSON.parse(event.data);
|
|
855
|
+
this.handleMessage(raw);
|
|
856
|
+
} catch (e) {
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
ws.onclose = () => {
|
|
860
|
+
var _a, _b;
|
|
861
|
+
this.ws = null;
|
|
862
|
+
this.connecting = false;
|
|
863
|
+
const wasConnected = this._connected;
|
|
864
|
+
this._connected = false;
|
|
865
|
+
this.cancelStaleTimer();
|
|
866
|
+
if (wasConnected) {
|
|
867
|
+
(_b = (_a = this.callbacks).onConnectionStateChange) == null ? void 0 : _b.call(_a, false);
|
|
868
|
+
}
|
|
869
|
+
if (!this.destroyed && (this.subs.size > 0 || this.arbSubs.size > 0 || this.arbFeedCbs.size > 0)) {
|
|
870
|
+
this.scheduleReconnect();
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
ws.onerror = () => {
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
scheduleReconnect() {
|
|
877
|
+
this.cancelReconnect();
|
|
878
|
+
const delay = Math.min(
|
|
879
|
+
RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempts),
|
|
880
|
+
RECONNECT_MAX_MS
|
|
881
|
+
);
|
|
882
|
+
this.reconnectAttempts++;
|
|
883
|
+
this.emitDiagnostic({ type: "reconnect", attempt: this.reconnectAttempts, delayMs: delay });
|
|
884
|
+
this.reconnectTimer = setTimeout(() => {
|
|
885
|
+
this.reconnectTimer = null;
|
|
886
|
+
this.doConnect();
|
|
887
|
+
}, delay);
|
|
888
|
+
}
|
|
889
|
+
cancelReconnect() {
|
|
890
|
+
if (this.reconnectTimer) {
|
|
891
|
+
clearTimeout(this.reconnectTimer);
|
|
892
|
+
this.reconnectTimer = null;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// ── Staleness detection ──
|
|
896
|
+
resetStaleTimer() {
|
|
897
|
+
this.cancelStaleTimer();
|
|
898
|
+
this.staleTimer = setTimeout(() => {
|
|
899
|
+
const age = Date.now() - this.lastMessageTime;
|
|
900
|
+
this.emitDiagnostic({ type: "stale_connection", lastMessageAge: age });
|
|
901
|
+
if (this.ws) {
|
|
902
|
+
this.ws.close();
|
|
903
|
+
}
|
|
904
|
+
}, STALE_TIMEOUT_MS);
|
|
905
|
+
}
|
|
906
|
+
cancelStaleTimer() {
|
|
907
|
+
if (this.staleTimer) {
|
|
908
|
+
clearTimeout(this.staleTimer);
|
|
909
|
+
this.staleTimer = null;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
// ── Message handling ──
|
|
913
|
+
handleMessage(raw) {
|
|
914
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r;
|
|
915
|
+
const type = raw.type;
|
|
916
|
+
if (["orderbook_snapshot", "orderbook_delta", "trade"].includes(type) && !validMarketFrame(raw)) {
|
|
917
|
+
(_b = (_a = this.callbacks).onError) == null ? void 0 : _b.call(_a, { type: "error", message: "Invalid market-data frame" });
|
|
918
|
+
if (typeof raw.outcomeId === "string" && this.subs.has(raw.outcomeId))
|
|
919
|
+
this.requestResnapshot(raw.outcomeId);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
switch (type) {
|
|
923
|
+
case "orderbook_snapshot":
|
|
924
|
+
this.handleSnapshot(raw);
|
|
925
|
+
break;
|
|
926
|
+
case "orderbook_delta":
|
|
927
|
+
this.handleDelta(raw);
|
|
928
|
+
break;
|
|
929
|
+
case "trade":
|
|
930
|
+
this.handleTrade(raw);
|
|
931
|
+
break;
|
|
932
|
+
case "heartbeat":
|
|
933
|
+
this.handleHeartbeat(raw);
|
|
934
|
+
break;
|
|
935
|
+
case "subscribed":
|
|
936
|
+
(_d = (_c = this.callbacks).onSubscribed) == null ? void 0 : _d.call(_c, raw);
|
|
937
|
+
break;
|
|
938
|
+
case "unsubscribed":
|
|
939
|
+
(_f = (_e = this.callbacks).onUnsubscribed) == null ? void 0 : _f.call(_e, raw);
|
|
940
|
+
break;
|
|
941
|
+
case "connected":
|
|
942
|
+
this.handleConnected(raw);
|
|
943
|
+
break;
|
|
944
|
+
case "authenticated":
|
|
945
|
+
(_h = (_g = this.callbacks).onAuthenticated) == null ? void 0 : _h.call(_g, raw);
|
|
946
|
+
break;
|
|
947
|
+
case "market_resolved":
|
|
948
|
+
this.handleMarketResolved(raw);
|
|
949
|
+
break;
|
|
950
|
+
case "error":
|
|
951
|
+
(_j = (_i = this.callbacks).onError) == null ? void 0 : _j.call(_i, raw);
|
|
952
|
+
break;
|
|
953
|
+
case "order_submitted":
|
|
954
|
+
this.handleOrderSubmitted(raw);
|
|
955
|
+
break;
|
|
956
|
+
case "balance_update":
|
|
957
|
+
this.handleBalanceUpdate(raw);
|
|
958
|
+
break;
|
|
959
|
+
case "order_event":
|
|
960
|
+
(_l = (_k = this.callbacks).onOrderEvent) == null ? void 0 : _l.call(_k, raw);
|
|
961
|
+
break;
|
|
962
|
+
case "redeem_event":
|
|
963
|
+
this.handleRedeemEvent(raw);
|
|
964
|
+
break;
|
|
965
|
+
case "withdrawal_lifecycle":
|
|
966
|
+
(_n = (_m = this.callbacks).onWithdrawalLifecycle) == null ? void 0 : _n.call(_m, raw);
|
|
967
|
+
break;
|
|
968
|
+
case "arb_market_update": {
|
|
969
|
+
const arbMsg = raw;
|
|
970
|
+
this.latestArb.set(arbMsg.marketId, arbMsg);
|
|
971
|
+
const arbEntry = this.arbSubs.get(arbMsg.marketId);
|
|
972
|
+
if (arbEntry) {
|
|
973
|
+
for (const cb of arbEntry.cbs) cb(arbMsg);
|
|
974
|
+
}
|
|
975
|
+
(_p = (_o = this.callbacks).onArbMarket) == null ? void 0 : _p.call(_o, arbMsg);
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
case "arb_feed_batch": {
|
|
979
|
+
const feedMsg = raw;
|
|
980
|
+
for (const cb of this.arbFeedCbs) cb(feedMsg);
|
|
981
|
+
(_r = (_q = this.callbacks).onArbFeed) == null ? void 0 : _r.call(_q, feedMsg);
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Server confirmed the connection — gateway has validated our appId and is
|
|
988
|
+
* ready to accept subscriptions. Now flush all pending subs.
|
|
989
|
+
*/
|
|
990
|
+
handleConnected(msg) {
|
|
991
|
+
var _a, _b, _c, _d;
|
|
992
|
+
this._connected = true;
|
|
993
|
+
(_b = (_a = this.callbacks).onConnectionStateChange) == null ? void 0 : _b.call(_a, true);
|
|
994
|
+
(_d = (_c = this.callbacks).onConnected) == null ? void 0 : _d.call(_c, msg);
|
|
995
|
+
const orderbookSet = new Set(this.activeOrderbookMarkets());
|
|
996
|
+
this.pendingSubs.forEach((id) => orderbookSet.add(id));
|
|
997
|
+
this.pendingFlushSubs.forEach((id) => orderbookSet.add(id));
|
|
998
|
+
const orderbookIds = Array.from(orderbookSet);
|
|
999
|
+
if (orderbookIds.length > 0) {
|
|
1000
|
+
this.sendSubscribe(orderbookIds);
|
|
1001
|
+
}
|
|
1002
|
+
this.pendingSubs.clear();
|
|
1003
|
+
this.pendingFlushSubs.clear();
|
|
1004
|
+
this.pendingFlushUnsubs.clear();
|
|
1005
|
+
const tradeSet = new Set(this.activeTradeMarkets());
|
|
1006
|
+
this.pendingTradeSubs.forEach((id) => tradeSet.add(id));
|
|
1007
|
+
this.pendingFlushTradeSubs.forEach((id) => tradeSet.add(id));
|
|
1008
|
+
const tradeIds = Array.from(tradeSet);
|
|
1009
|
+
if (tradeIds.length > 0) {
|
|
1010
|
+
this.send({ action: "subscribe", channel: "trades", outcomeIds: tradeIds });
|
|
1011
|
+
}
|
|
1012
|
+
this.pendingTradeSubs.clear();
|
|
1013
|
+
this.pendingFlushTradeSubs.clear();
|
|
1014
|
+
this.pendingFlushTradeUnsubs.clear();
|
|
1015
|
+
const resnapshotIds = Array.from(this.pendingResnapshots);
|
|
1016
|
+
if (resnapshotIds.length > 0) {
|
|
1017
|
+
this.send({ action: "resnapshot", channel: "orderbook", outcomeIds: resnapshotIds });
|
|
1018
|
+
}
|
|
1019
|
+
this.pendingResnapshots.clear();
|
|
1020
|
+
const arbSet = new Set(this.activeArbMarkets());
|
|
1021
|
+
this.pendingFlushArbSubs.forEach((id) => arbSet.add(id));
|
|
1022
|
+
const arbIds = Array.from(arbSet);
|
|
1023
|
+
if (arbIds.length > 0) {
|
|
1024
|
+
this.send({ action: "subscribe", channel: "arb", marketIds: arbIds });
|
|
1025
|
+
}
|
|
1026
|
+
this.pendingFlushArbSubs.clear();
|
|
1027
|
+
this.pendingFlushArbUnsubs.clear();
|
|
1028
|
+
if (this.arbFeedCbs.size > 0) {
|
|
1029
|
+
this.send({ action: "subscribe", channel: "arb-feed" });
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
handleSnapshot(raw) {
|
|
1033
|
+
var _a, _b, _c, _d;
|
|
1034
|
+
if (!this.subs.has(raw.outcomeId)) return;
|
|
1035
|
+
const msg = __spreadProps(__spreadValues({}, raw), { timestamp: raw.timestamp / 1e3 });
|
|
1036
|
+
const book = snapshotToOrderbook(msg);
|
|
1037
|
+
this.books.set(msg.outcomeId, new IndexedOrderbook(book));
|
|
1038
|
+
this.resyncing.delete(msg.outcomeId);
|
|
1039
|
+
this.pendingResnapshots.delete(msg.outcomeId);
|
|
1040
|
+
this.debugOrderbook("snapshot_applied", {
|
|
1041
|
+
outcomeId: msg.outcomeId,
|
|
1042
|
+
seq: msg.seq,
|
|
1043
|
+
checksum: msg.checksum,
|
|
1044
|
+
bidsTop: this.summarizeTopLevels(book.bids),
|
|
1045
|
+
asksTop: this.summarizeTopLevels(book.asks)
|
|
1046
|
+
});
|
|
1047
|
+
(_b = (_a = this.callbacks).onBookTick) == null ? void 0 : _b.call(_a, msg.outcomeId, {
|
|
1048
|
+
midpoint: msg.midpoint,
|
|
1049
|
+
timestamp: msg.timestamp,
|
|
1050
|
+
source: "snapshot"
|
|
1051
|
+
});
|
|
1052
|
+
if (!this.deferBookViews) (_d = (_c = this.callbacks).onSnapshot) == null ? void 0 : _d.call(_c, msg.outcomeId, book);
|
|
1053
|
+
}
|
|
1054
|
+
handleDelta(raw) {
|
|
1055
|
+
var _a, _b, _c, _d;
|
|
1056
|
+
if (!this.subs.has(raw.outcomeId)) return;
|
|
1057
|
+
const outcomeId = raw.outcomeId;
|
|
1058
|
+
const current = this.books.get(outcomeId);
|
|
1059
|
+
if (!current) {
|
|
1060
|
+
this.debugOrderbook("delta_missing_snapshot", { outcomeId, seq: raw.seq });
|
|
1061
|
+
this.requestResnapshot(outcomeId);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (raw.seq <= current.seq) {
|
|
1065
|
+
this.debugOrderbook("delta_stale_ignored", {
|
|
1066
|
+
outcomeId,
|
|
1067
|
+
currentSeq: current.seq,
|
|
1068
|
+
receivedSeq: raw.seq
|
|
1069
|
+
});
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (raw.seq > current.seq + 1) {
|
|
1073
|
+
this.debugOrderbook("delta_seq_gap", {
|
|
1074
|
+
outcomeId,
|
|
1075
|
+
expectedSeq: current.seq + 1,
|
|
1076
|
+
receivedSeq: raw.seq
|
|
1077
|
+
});
|
|
1078
|
+
this.emitDiagnostic({
|
|
1079
|
+
type: "seq_gap",
|
|
1080
|
+
marketId: outcomeId,
|
|
1081
|
+
outcomeId,
|
|
1082
|
+
expected: current.seq + 1,
|
|
1083
|
+
received: raw.seq
|
|
1084
|
+
});
|
|
1085
|
+
this.requestResnapshot(outcomeId);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const msg = __spreadProps(__spreadValues({}, raw), { timestamp: raw.timestamp / 1e3 });
|
|
1089
|
+
const computed = current.apply(msg);
|
|
1090
|
+
if (computed !== msg.checksum) {
|
|
1091
|
+
this.debugOrderbook("delta_checksum_mismatch", {
|
|
1092
|
+
outcomeId,
|
|
1093
|
+
seq: msg.seq,
|
|
1094
|
+
expected: msg.checksum,
|
|
1095
|
+
computed
|
|
1096
|
+
});
|
|
1097
|
+
this.emitDiagnostic({
|
|
1098
|
+
type: "checksum_mismatch",
|
|
1099
|
+
marketId: outcomeId,
|
|
1100
|
+
outcomeId,
|
|
1101
|
+
expected: msg.checksum,
|
|
1102
|
+
computed
|
|
1103
|
+
});
|
|
1104
|
+
this.requestResnapshot(outcomeId);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
(_b = (_a = this.callbacks).onBookTick) == null ? void 0 : _b.call(_a, outcomeId, {
|
|
1108
|
+
midpoint: msg.midpoint,
|
|
1109
|
+
timestamp: msg.timestamp,
|
|
1110
|
+
source: "delta"
|
|
1111
|
+
});
|
|
1112
|
+
if (!this.deferBookViews) (_d = (_c = this.callbacks).onDelta) == null ? void 0 : _d.call(_c, outcomeId, current.view());
|
|
1113
|
+
}
|
|
1114
|
+
handleTrade(raw) {
|
|
1115
|
+
var _a, _b;
|
|
1116
|
+
if (!this.subs.has(raw.outcomeId)) return;
|
|
1117
|
+
const msg = __spreadProps(__spreadValues({}, raw), { timestamp: raw.timestamp / 1e3 });
|
|
1118
|
+
(_b = (_a = this.callbacks).onTrade) == null ? void 0 : _b.call(_a, msg);
|
|
1119
|
+
}
|
|
1120
|
+
handleHeartbeat(raw) {
|
|
1121
|
+
var _a, _b;
|
|
1122
|
+
const msg = __spreadProps(__spreadValues({}, raw), { ts: raw.ts / 1e3 });
|
|
1123
|
+
(_b = (_a = this.callbacks).onHeartbeat) == null ? void 0 : _b.call(_a, msg);
|
|
1124
|
+
}
|
|
1125
|
+
handleMarketResolved(raw) {
|
|
1126
|
+
var _a, _b;
|
|
1127
|
+
const msg = __spreadProps(__spreadValues({}, raw), { timestamp: raw.timestamp / 1e3 });
|
|
1128
|
+
this.books.delete(msg.outcomeId);
|
|
1129
|
+
this.pendingResnapshots.delete(msg.outcomeId);
|
|
1130
|
+
this.resyncing.delete(msg.outcomeId);
|
|
1131
|
+
(_b = (_a = this.callbacks).onMarketResolved) == null ? void 0 : _b.call(_a, msg);
|
|
1132
|
+
}
|
|
1133
|
+
handleOrderSubmitted(raw) {
|
|
1134
|
+
var _a, _b;
|
|
1135
|
+
const msg = __spreadProps(__spreadValues({}, raw), { timestamp: raw.timestamp / 1e3 });
|
|
1136
|
+
(_b = (_a = this.callbacks).onOrderSubmitted) == null ? void 0 : _b.call(_a, msg);
|
|
1137
|
+
}
|
|
1138
|
+
handleBalanceUpdate(raw) {
|
|
1139
|
+
var _a, _b;
|
|
1140
|
+
const msg = __spreadProps(__spreadValues({}, raw), { timestamp: raw.timestamp / 1e3 });
|
|
1141
|
+
(_b = (_a = this.callbacks).onBalanceUpdate) == null ? void 0 : _b.call(_a, msg);
|
|
1142
|
+
}
|
|
1143
|
+
handleRedeemEvent(raw) {
|
|
1144
|
+
var _a, _b;
|
|
1145
|
+
const msg = __spreadProps(__spreadValues({}, raw), { timestamp: raw.timestamp / 1e3 });
|
|
1146
|
+
(_b = (_a = this.callbacks).onRedeemEvent) == null ? void 0 : _b.call(_a, msg);
|
|
1147
|
+
}
|
|
1148
|
+
// ── Resnapshot ──
|
|
1149
|
+
requestResnapshot(outcomeId, force = false) {
|
|
1150
|
+
if (this.resyncing.has(outcomeId) && !force) return;
|
|
1151
|
+
this.resyncing.add(outcomeId);
|
|
1152
|
+
if (this._connected) {
|
|
1153
|
+
this.send({ action: "resnapshot", channel: "orderbook", outcomeIds: [outcomeId] });
|
|
1154
|
+
this.pendingResnapshots.delete(outcomeId);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
this.pendingResnapshots.add(outcomeId);
|
|
1158
|
+
this.ensureConnected();
|
|
1159
|
+
}
|
|
1160
|
+
// ── Helpers ──
|
|
1161
|
+
/**
|
|
1162
|
+
* Schedule a microtask that flushes queued subscribe/unsubscribe frames.
|
|
1163
|
+
* Coalesces multiple same-tick sub/unsub calls into at most one frame per
|
|
1164
|
+
* channel+action pair.
|
|
1165
|
+
*/
|
|
1166
|
+
scheduleFlush() {
|
|
1167
|
+
if (this.flushScheduled) return;
|
|
1168
|
+
this.flushScheduled = true;
|
|
1169
|
+
queueMicrotask(() => {
|
|
1170
|
+
this.flushScheduled = false;
|
|
1171
|
+
if (!this._connected) {
|
|
1172
|
+
this.pendingFlushSubs.clear();
|
|
1173
|
+
this.pendingFlushUnsubs.clear();
|
|
1174
|
+
this.pendingFlushTradeSubs.clear();
|
|
1175
|
+
this.pendingFlushTradeUnsubs.clear();
|
|
1176
|
+
this.pendingFlushArbSubs.clear();
|
|
1177
|
+
this.pendingFlushArbUnsubs.clear();
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
this.flushPending();
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
flushPending() {
|
|
1184
|
+
for (const id of [...this.pendingFlushSubs]) {
|
|
1185
|
+
if (this.pendingFlushUnsubs.has(id)) {
|
|
1186
|
+
this.pendingFlushSubs.delete(id);
|
|
1187
|
+
this.pendingFlushUnsubs.delete(id);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (this.pendingFlushSubs.size > 0) {
|
|
1191
|
+
this.sendSubscribe(Array.from(this.pendingFlushSubs));
|
|
1192
|
+
this.pendingFlushSubs.clear();
|
|
1193
|
+
}
|
|
1194
|
+
if (this.pendingFlushUnsubs.size > 0) {
|
|
1195
|
+
this.send({
|
|
1196
|
+
action: "unsubscribe",
|
|
1197
|
+
channel: "orderbook",
|
|
1198
|
+
outcomeIds: Array.from(this.pendingFlushUnsubs)
|
|
1199
|
+
});
|
|
1200
|
+
this.pendingFlushUnsubs.clear();
|
|
1201
|
+
}
|
|
1202
|
+
for (const id of [...this.pendingFlushTradeSubs]) {
|
|
1203
|
+
if (this.pendingFlushTradeUnsubs.has(id)) {
|
|
1204
|
+
this.pendingFlushTradeSubs.delete(id);
|
|
1205
|
+
this.pendingFlushTradeUnsubs.delete(id);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (this.pendingFlushTradeSubs.size > 0) {
|
|
1209
|
+
this.send({
|
|
1210
|
+
action: "subscribe",
|
|
1211
|
+
channel: "trades",
|
|
1212
|
+
outcomeIds: Array.from(this.pendingFlushTradeSubs)
|
|
1213
|
+
});
|
|
1214
|
+
this.pendingFlushTradeSubs.clear();
|
|
1215
|
+
}
|
|
1216
|
+
if (this.pendingFlushTradeUnsubs.size > 0) {
|
|
1217
|
+
this.send({
|
|
1218
|
+
action: "unsubscribe",
|
|
1219
|
+
channel: "trades",
|
|
1220
|
+
outcomeIds: Array.from(this.pendingFlushTradeUnsubs)
|
|
1221
|
+
});
|
|
1222
|
+
this.pendingFlushTradeUnsubs.clear();
|
|
1223
|
+
}
|
|
1224
|
+
for (const id of [...this.pendingFlushArbSubs]) {
|
|
1225
|
+
if (this.pendingFlushArbUnsubs.has(id)) {
|
|
1226
|
+
this.pendingFlushArbSubs.delete(id);
|
|
1227
|
+
this.pendingFlushArbUnsubs.delete(id);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (this.pendingFlushArbSubs.size > 0) {
|
|
1231
|
+
this.send({
|
|
1232
|
+
action: "subscribe",
|
|
1233
|
+
channel: "arb",
|
|
1234
|
+
marketIds: Array.from(this.pendingFlushArbSubs)
|
|
1235
|
+
});
|
|
1236
|
+
this.pendingFlushArbSubs.clear();
|
|
1237
|
+
}
|
|
1238
|
+
if (this.pendingFlushArbUnsubs.size > 0) {
|
|
1239
|
+
this.send({
|
|
1240
|
+
action: "unsubscribe",
|
|
1241
|
+
channel: "arb",
|
|
1242
|
+
marketIds: Array.from(this.pendingFlushArbUnsubs)
|
|
1243
|
+
});
|
|
1244
|
+
this.pendingFlushArbUnsubs.clear();
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
sendSubscribe(outcomeIds) {
|
|
1248
|
+
this.send({ action: "subscribe", channel: "orderbook", outcomeIds });
|
|
1249
|
+
}
|
|
1250
|
+
send(msg) {
|
|
1251
|
+
var _a;
|
|
1252
|
+
if (((_a = this.ws) == null ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
1253
|
+
this.ws.send(JSON.stringify(msg));
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
activeOrderbookMarkets() {
|
|
1257
|
+
const markets = [];
|
|
1258
|
+
this.subs.forEach((sub, id) => {
|
|
1259
|
+
if (sub.refCount > 0) markets.push(id);
|
|
1260
|
+
});
|
|
1261
|
+
return markets;
|
|
1262
|
+
}
|
|
1263
|
+
activeTradeMarkets() {
|
|
1264
|
+
const markets = [];
|
|
1265
|
+
this.subs.forEach((sub, id) => {
|
|
1266
|
+
if (sub.tradeRefs > 0) markets.push(id);
|
|
1267
|
+
});
|
|
1268
|
+
return markets;
|
|
1269
|
+
}
|
|
1270
|
+
activeArbMarkets() {
|
|
1271
|
+
return Array.from(this.arbSubs.keys());
|
|
1272
|
+
}
|
|
1273
|
+
emitDiagnostic(event) {
|
|
1274
|
+
var _a, _b;
|
|
1275
|
+
(_b = (_a = this.callbacks).onDiagnostics) == null ? void 0 : _b.call(_a, event);
|
|
1276
|
+
}
|
|
1277
|
+
summarizeTopLevels(levels2) {
|
|
1278
|
+
return levels2.slice(0, ORDERBOOK_DEBUG_TOP_LEVELS).map((level) => ({ price: level.price, size: level.size }));
|
|
1279
|
+
}
|
|
1280
|
+
debugOrderbook(event, payload) {
|
|
1281
|
+
if (!this.isOrderbookDebugEnabled()) return;
|
|
1282
|
+
console.debug(`[AggWebSocket][orderbook] ${event}`, payload);
|
|
1283
|
+
}
|
|
1284
|
+
isOrderbookDebugEnabled() {
|
|
1285
|
+
return this.enableLogs;
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
// src/candle-builder.ts
|
|
1290
|
+
var INTERVAL_SECS = {
|
|
1291
|
+
"1m": 60,
|
|
1292
|
+
"5m": 300,
|
|
1293
|
+
"1h": 3600,
|
|
1294
|
+
"1d": 86400
|
|
1295
|
+
};
|
|
1296
|
+
var ALL_INTERVALS = ["1m", "5m", "1h", "1d"];
|
|
1297
|
+
var MAX_CLOSED = 500;
|
|
1298
|
+
var CandleBuilder = class {
|
|
1299
|
+
constructor() {
|
|
1300
|
+
this.forming = /* @__PURE__ */ new Map();
|
|
1301
|
+
this.closedMap = /* @__PURE__ */ new Map();
|
|
1302
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1303
|
+
this._midpoint = null;
|
|
1304
|
+
this.dirty = false;
|
|
1305
|
+
this.scheduled = false;
|
|
1306
|
+
for (const interval of ALL_INTERVALS) {
|
|
1307
|
+
this.closedMap.set(interval, []);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
/** Feed a midpoint (from orderbook delta). Updates O/H/L/C for all intervals. */
|
|
1311
|
+
addMidpoint(midpoint, timestampSecs) {
|
|
1312
|
+
if (!Number.isFinite(midpoint) || !Number.isFinite(timestampSecs)) return;
|
|
1313
|
+
this._midpoint = midpoint;
|
|
1314
|
+
for (const interval of ALL_INTERVALS) {
|
|
1315
|
+
this.updateCandle(interval, midpoint, timestampSecs);
|
|
1316
|
+
}
|
|
1317
|
+
this.markDirty();
|
|
1318
|
+
}
|
|
1319
|
+
/** Feed a trade. Updates volume + refines H/L/C from trade price. */
|
|
1320
|
+
addTrade(price, size, timestampSecs) {
|
|
1321
|
+
if (!Number.isFinite(price) || !Number.isFinite(size) || !Number.isFinite(timestampSecs))
|
|
1322
|
+
return;
|
|
1323
|
+
for (const interval of ALL_INTERVALS) {
|
|
1324
|
+
this.updateCandle(interval, price, timestampSecs, size);
|
|
1325
|
+
}
|
|
1326
|
+
this.markDirty();
|
|
1327
|
+
}
|
|
1328
|
+
/** Current forming candle for an interval. */
|
|
1329
|
+
getForming(interval) {
|
|
1330
|
+
var _a;
|
|
1331
|
+
return (_a = this.forming.get(interval)) != null ? _a : null;
|
|
1332
|
+
}
|
|
1333
|
+
/** Closed candles for an interval (ring buffer, max 500, newest last). */
|
|
1334
|
+
getClosed(interval) {
|
|
1335
|
+
var _a;
|
|
1336
|
+
return (_a = this.closedMap.get(interval)) != null ? _a : [];
|
|
1337
|
+
}
|
|
1338
|
+
/** Apply a bounded Worker view without recomputing candles from presentation samples. */
|
|
1339
|
+
replaceTail(tail) {
|
|
1340
|
+
var _a, _b;
|
|
1341
|
+
for (const interval of ALL_INTERVALS) {
|
|
1342
|
+
const view = tail[interval];
|
|
1343
|
+
if (view.forming) this.forming.set(interval, __spreadValues({}, view.forming));
|
|
1344
|
+
else this.forming.delete(interval);
|
|
1345
|
+
if (!view.closed.length) continue;
|
|
1346
|
+
const closed = new Map(this.getClosed(interval).map((candle) => [candle.openTime, candle]));
|
|
1347
|
+
for (const candle of view.closed) closed.set(candle.openTime, __spreadValues({}, candle));
|
|
1348
|
+
this.closedMap.set(
|
|
1349
|
+
interval,
|
|
1350
|
+
[...closed.values()].sort((a, b) => a.openTime - b.openTime).slice(-MAX_CLOSED)
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
this._midpoint = (_b = (_a = tail["1m"].forming) == null ? void 0 : _a.c) != null ? _b : null;
|
|
1354
|
+
this.markDirty();
|
|
1355
|
+
}
|
|
1356
|
+
/** Current midpoint (latest value fed). */
|
|
1357
|
+
get midpoint() {
|
|
1358
|
+
return this._midpoint;
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Subscribe to state changes. Returns unsubscribe function.
|
|
1362
|
+
* BATCHED: callbacks fire at most once per rAF frame (~16ms).
|
|
1363
|
+
* In non-browser environments, falls back to 16ms setTimeout.
|
|
1364
|
+
*/
|
|
1365
|
+
onChange(listener) {
|
|
1366
|
+
this.listeners.add(listener);
|
|
1367
|
+
return () => {
|
|
1368
|
+
this.listeners.delete(listener);
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
/** Clear all state (on reconnect). */
|
|
1372
|
+
reset() {
|
|
1373
|
+
this.forming.clear();
|
|
1374
|
+
for (const interval of ALL_INTERVALS) {
|
|
1375
|
+
this.closedMap.set(interval, []);
|
|
1376
|
+
}
|
|
1377
|
+
this._midpoint = null;
|
|
1378
|
+
this.dirty = false;
|
|
1379
|
+
}
|
|
1380
|
+
/** Clear all state AND unsubscribe all listeners. Called on store eviction. */
|
|
1381
|
+
dispose() {
|
|
1382
|
+
this.reset();
|
|
1383
|
+
this.listeners.clear();
|
|
1384
|
+
}
|
|
1385
|
+
updateCandle(interval, price, timestampSecs, tradeSize) {
|
|
1386
|
+
const secs = INTERVAL_SECS[interval];
|
|
1387
|
+
const openTime = Math.floor(timestampSecs / secs) * secs;
|
|
1388
|
+
const current = this.forming.get(interval);
|
|
1389
|
+
if (current && current.openTime !== openTime) {
|
|
1390
|
+
const closed = this.closedMap.get(interval);
|
|
1391
|
+
closed.push(__spreadProps(__spreadValues({}, current), { isFinal: true }));
|
|
1392
|
+
if (closed.length > MAX_CLOSED) closed.shift();
|
|
1393
|
+
this.forming.set(interval, {
|
|
1394
|
+
openTime,
|
|
1395
|
+
o: price,
|
|
1396
|
+
h: price,
|
|
1397
|
+
l: price,
|
|
1398
|
+
c: price,
|
|
1399
|
+
v: tradeSize != null ? tradeSize : 0,
|
|
1400
|
+
tradeCount: tradeSize != null ? 1 : 0,
|
|
1401
|
+
isFinal: false
|
|
1402
|
+
});
|
|
1403
|
+
} else if (!current) {
|
|
1404
|
+
this.forming.set(interval, {
|
|
1405
|
+
openTime,
|
|
1406
|
+
o: price,
|
|
1407
|
+
h: price,
|
|
1408
|
+
l: price,
|
|
1409
|
+
c: price,
|
|
1410
|
+
v: tradeSize != null ? tradeSize : 0,
|
|
1411
|
+
tradeCount: tradeSize != null ? 1 : 0,
|
|
1412
|
+
isFinal: false
|
|
1413
|
+
});
|
|
1414
|
+
} else {
|
|
1415
|
+
current.h = Math.max(current.h, price);
|
|
1416
|
+
current.l = Math.min(current.l, price);
|
|
1417
|
+
current.c = price;
|
|
1418
|
+
if (tradeSize != null) {
|
|
1419
|
+
current.v += tradeSize;
|
|
1420
|
+
current.tradeCount += 1;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
markDirty() {
|
|
1425
|
+
this.dirty = true;
|
|
1426
|
+
if (this.scheduled) return;
|
|
1427
|
+
this.scheduled = true;
|
|
1428
|
+
const tick = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (fn) => setTimeout(fn, 16);
|
|
1429
|
+
tick(() => {
|
|
1430
|
+
this.scheduled = false;
|
|
1431
|
+
if (this.dirty) {
|
|
1432
|
+
this.dirty = false;
|
|
1433
|
+
this.listeners.forEach((l) => l());
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
// src/market-data/engine.ts
|
|
1440
|
+
var intervals = ["1m", "5m", "1h", "1d"];
|
|
1441
|
+
var MarketDataEngine = class {
|
|
1442
|
+
constructor() {
|
|
1443
|
+
this.socket = null;
|
|
1444
|
+
this.generation = 0;
|
|
1445
|
+
this.receivedAtMs = 0;
|
|
1446
|
+
this.revision = 0;
|
|
1447
|
+
this.deliveredClosed = /* @__PURE__ */ new Map();
|
|
1448
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
1449
|
+
this.builders = /* @__PURE__ */ new Map();
|
|
1450
|
+
this.dirty = /* @__PURE__ */ new Map();
|
|
1451
|
+
this.inFlight = false;
|
|
1452
|
+
this.deliveryToken = null;
|
|
1453
|
+
this.disposed = false;
|
|
1454
|
+
this.history = /* @__PURE__ */ new Map();
|
|
1455
|
+
this.measurements = {
|
|
1456
|
+
received: 0,
|
|
1457
|
+
published: 0,
|
|
1458
|
+
coalesced: 0,
|
|
1459
|
+
maxPendingOutcomes: 0,
|
|
1460
|
+
maxInFlight: 0
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
start(options, event) {
|
|
1464
|
+
this.dispose();
|
|
1465
|
+
this.disposed = false;
|
|
1466
|
+
this.generation = 0;
|
|
1467
|
+
this.revision = 0;
|
|
1468
|
+
const callbacks = {};
|
|
1469
|
+
const names = [
|
|
1470
|
+
"onConnected",
|
|
1471
|
+
"onAuthenticated",
|
|
1472
|
+
"onHeartbeat",
|
|
1473
|
+
"onSubscribed",
|
|
1474
|
+
"onUnsubscribed",
|
|
1475
|
+
"onMarketResolved",
|
|
1476
|
+
"onError",
|
|
1477
|
+
"onOrderSubmitted",
|
|
1478
|
+
"onBalanceUpdate",
|
|
1479
|
+
"onOrderEvent",
|
|
1480
|
+
"onRedeemEvent",
|
|
1481
|
+
"onWithdrawalLifecycle",
|
|
1482
|
+
"onArbMarket",
|
|
1483
|
+
"onArbFeed",
|
|
1484
|
+
"onDiagnostics",
|
|
1485
|
+
"onConnectionStateChange"
|
|
1486
|
+
];
|
|
1487
|
+
for (const name of names) {
|
|
1488
|
+
Object.assign(callbacks, {
|
|
1489
|
+
[name]: (...args) => {
|
|
1490
|
+
var _a;
|
|
1491
|
+
if (name === "onConnectionStateChange" && args[0] === false) {
|
|
1492
|
+
for (const builder of this.builders.values()) builder.reset();
|
|
1493
|
+
this.dirty.clear();
|
|
1494
|
+
this.deliveredClosed.clear();
|
|
1495
|
+
}
|
|
1496
|
+
if (name === "onMarketResolved") {
|
|
1497
|
+
const id = args[0].outcomeId;
|
|
1498
|
+
(_a = this.builders.get(id)) == null ? void 0 : _a.dispose();
|
|
1499
|
+
this.builders.delete(id);
|
|
1500
|
+
this.dirty.delete(id);
|
|
1501
|
+
}
|
|
1502
|
+
if (name === "onDiagnostics") {
|
|
1503
|
+
const diagnostic = args[0];
|
|
1504
|
+
if (diagnostic.outcomeId && ["seq_gap", "checksum_mismatch"].includes(diagnostic.type))
|
|
1505
|
+
this.mark(diagnostic.outcomeId, false);
|
|
1506
|
+
}
|
|
1507
|
+
event({ name, args, revision: this.revision });
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
callbacks.onFrameReceived = (receivedAtMs) => {
|
|
1512
|
+
this.receivedAtMs = receivedAtMs;
|
|
1513
|
+
this.revision++;
|
|
1514
|
+
};
|
|
1515
|
+
callbacks.onBookTick = (id, tick) => {
|
|
1516
|
+
if (tick.midpoint != null) this.builder(id).addMidpoint(tick.midpoint, tick.timestamp);
|
|
1517
|
+
this.mark(id, tick.source === "snapshot");
|
|
1518
|
+
};
|
|
1519
|
+
callbacks.onTrade = (trade) => {
|
|
1520
|
+
this.builder(trade.outcomeId).addTrade(trade.price, trade.size, trade.timestamp);
|
|
1521
|
+
this.mark(trade.outcomeId, false, trade);
|
|
1522
|
+
};
|
|
1523
|
+
this.socket = new AggWebSocket(__spreadProps(__spreadValues({}, options), { callbacks, deferBookViews: true }));
|
|
1524
|
+
}
|
|
1525
|
+
observeViews(observer) {
|
|
1526
|
+
this.observer = observer;
|
|
1527
|
+
}
|
|
1528
|
+
setSubscriptions(subscriptions, generation) {
|
|
1529
|
+
var _a;
|
|
1530
|
+
if (generation < this.generation) return;
|
|
1531
|
+
this.generation = generation;
|
|
1532
|
+
const desired = new Map(subscriptions.map((sub) => [`${sub.channel}:${sub.id}`, sub]));
|
|
1533
|
+
for (const [key, unsubscribe] of this.subscriptions) {
|
|
1534
|
+
if (!desired.has(key)) {
|
|
1535
|
+
unsubscribe();
|
|
1536
|
+
this.subscriptions.delete(key);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
for (const [key, sub] of desired) {
|
|
1540
|
+
if (this.subscriptions.has(key) || !this.socket) continue;
|
|
1541
|
+
const unsubscribe = sub.channel === "arb" ? this.socket.subscribeArb(sub.id, () => {
|
|
1542
|
+
}) : sub.channel === "arb-feed" ? this.socket.subscribeArbFeed(() => {
|
|
1543
|
+
}) : this.socket.subscribe(sub.id, sub.channel);
|
|
1544
|
+
this.subscriptions.set(key, unsubscribe);
|
|
1545
|
+
}
|
|
1546
|
+
for (const sub of subscriptions)
|
|
1547
|
+
if (sub.channel === "orderbook" && ((_a = this.socket) == null ? void 0 : _a.getBook(sub.id))) this.mark(sub.id, false);
|
|
1548
|
+
const outcomes = new Set(
|
|
1549
|
+
subscriptions.filter((sub) => sub.channel === "orderbook" || sub.channel === "trades").map((sub) => sub.id)
|
|
1550
|
+
);
|
|
1551
|
+
for (const id of this.dirty.keys()) if (!outcomes.has(id)) this.dirty.delete(id);
|
|
1552
|
+
for (const id of this.deliveredClosed.keys())
|
|
1553
|
+
if (!outcomes.has(id)) this.deliveredClosed.delete(id);
|
|
1554
|
+
for (const [id, builder] of this.builders)
|
|
1555
|
+
if (!outcomes.has(id)) {
|
|
1556
|
+
builder.dispose();
|
|
1557
|
+
this.builders.delete(id);
|
|
1558
|
+
this.dirty.delete(id);
|
|
1559
|
+
this.deliveredClosed.delete(id);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
authenticate(token) {
|
|
1563
|
+
var _a;
|
|
1564
|
+
(_a = this.socket) == null ? void 0 : _a.authenticate(token);
|
|
1565
|
+
}
|
|
1566
|
+
resnapshot(ids) {
|
|
1567
|
+
var _a;
|
|
1568
|
+
(_a = this.socket) == null ? void 0 : _a.resnapshot(ids);
|
|
1569
|
+
}
|
|
1570
|
+
connect() {
|
|
1571
|
+
var _a;
|
|
1572
|
+
(_a = this.socket) == null ? void 0 : _a.connect();
|
|
1573
|
+
}
|
|
1574
|
+
disconnect() {
|
|
1575
|
+
var _a;
|
|
1576
|
+
(_a = this.socket) == null ? void 0 : _a.disconnect();
|
|
1577
|
+
}
|
|
1578
|
+
resume() {
|
|
1579
|
+
var _a;
|
|
1580
|
+
(_a = this.socket) == null ? void 0 : _a.resnapshot([...this.builders.keys()]);
|
|
1581
|
+
}
|
|
1582
|
+
getMeasurements() {
|
|
1583
|
+
var _a, _b, _c, _d, _e;
|
|
1584
|
+
const socket = (_b = (_a = this.socket) == null ? void 0 : _a.getDiagnostics()) != null ? _b : null;
|
|
1585
|
+
return __spreadProps(__spreadValues({}, this.measurements), {
|
|
1586
|
+
activeSubscriptions: this.subscriptions.size,
|
|
1587
|
+
activeOutcomes: (/* @__PURE__ */ new Set([
|
|
1588
|
+
...(_c = socket == null ? void 0 : socket.orderbookOutcomeIds) != null ? _c : [],
|
|
1589
|
+
...(_d = socket == null ? void 0 : socket.tradeOutcomeIds) != null ? _d : []
|
|
1590
|
+
])).size,
|
|
1591
|
+
cachedBooks: (_e = socket == null ? void 0 : socket.cachedBooks) != null ? _e : 0,
|
|
1592
|
+
builders: this.builders.size,
|
|
1593
|
+
historyTasks: this.history.size,
|
|
1594
|
+
pendingOutcomes: this.dirty.size,
|
|
1595
|
+
inFlightDeliveries: this.inFlight ? 1 : 0,
|
|
1596
|
+
retainedCandleOutcomes: this.deliveredClosed.size,
|
|
1597
|
+
socket
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
decodeHistory(payload) {
|
|
1601
|
+
return decodeChartHistory(payload);
|
|
1602
|
+
}
|
|
1603
|
+
/** Fetch/decode on the processing side; newer range generations abort obsolete fetches. */
|
|
1604
|
+
loadHistoryRange(request) {
|
|
1605
|
+
return __async(this, null, function* () {
|
|
1606
|
+
const previous = this.history.get(request.key);
|
|
1607
|
+
if (previous && previous.generation > request.generation) return null;
|
|
1608
|
+
previous == null ? void 0 : previous.controller.abort();
|
|
1609
|
+
const controller = new AbortController();
|
|
1610
|
+
const entry = { generation: request.generation, controller };
|
|
1611
|
+
this.history.set(request.key, entry);
|
|
1612
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
1613
|
+
try {
|
|
1614
|
+
const response = yield fetch(request.url, {
|
|
1615
|
+
headers: request.headers,
|
|
1616
|
+
signal: controller.signal
|
|
1617
|
+
});
|
|
1618
|
+
if (!response.ok) throw new Error(`History request failed (${response.status})`);
|
|
1619
|
+
const body = decodeChartHistory(yield readChartHistoryText(response));
|
|
1620
|
+
if (body.data.length > 4096) throw new Error("Invalid or unbounded history response");
|
|
1621
|
+
if (this.history.get(request.key) !== entry || this.disposed) return null;
|
|
1622
|
+
return __spreadProps(__spreadValues({}, body), { data: [...body.data].sort((a, b) => a.t - b.t) });
|
|
1623
|
+
} finally {
|
|
1624
|
+
clearTimeout(timeout);
|
|
1625
|
+
if (this.history.get(request.key) === entry) this.history.delete(request.key);
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
loadImmutableRange(key, generation, manifest) {
|
|
1630
|
+
return __async(this, null, function* () {
|
|
1631
|
+
const previous = this.history.get(key);
|
|
1632
|
+
if (previous && previous.generation > generation) throw new Error("Obsolete history range");
|
|
1633
|
+
previous == null ? void 0 : previous.controller.abort();
|
|
1634
|
+
const entry = { generation, controller: new AbortController() };
|
|
1635
|
+
this.history.set(key, entry);
|
|
1636
|
+
const timer = setTimeout(() => entry.controller.abort(), 1e4);
|
|
1637
|
+
try {
|
|
1638
|
+
const data = yield loadImmutableHistory({ manifest, signal: entry.controller.signal });
|
|
1639
|
+
if (this.history.get(key) !== entry || this.disposed)
|
|
1640
|
+
throw new Error("Obsolete history range");
|
|
1641
|
+
return data;
|
|
1642
|
+
} finally {
|
|
1643
|
+
clearTimeout(timer);
|
|
1644
|
+
if (this.history.get(key) === entry) this.history.delete(key);
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
cancelHistoryRange(key, generation) {
|
|
1649
|
+
const entry = this.history.get(key);
|
|
1650
|
+
if (!entry || generation !== void 0 && entry.generation !== generation) return;
|
|
1651
|
+
entry.controller.abort();
|
|
1652
|
+
this.history.delete(key);
|
|
1653
|
+
}
|
|
1654
|
+
dispose() {
|
|
1655
|
+
var _a, _b;
|
|
1656
|
+
this.disposed = true;
|
|
1657
|
+
(_a = this.cancelDelivery) == null ? void 0 : _a.call(this);
|
|
1658
|
+
this.cancelDelivery = void 0;
|
|
1659
|
+
this.deliveryToken = null;
|
|
1660
|
+
this.inFlight = false;
|
|
1661
|
+
this.observer = void 0;
|
|
1662
|
+
(_b = this.socket) == null ? void 0 : _b.destroy();
|
|
1663
|
+
this.socket = null;
|
|
1664
|
+
if (this.timer) clearTimeout(this.timer);
|
|
1665
|
+
this.timer = void 0;
|
|
1666
|
+
this.subscriptions.clear();
|
|
1667
|
+
this.dirty.clear();
|
|
1668
|
+
this.deliveredClosed.clear();
|
|
1669
|
+
for (const builder of this.builders.values()) builder.dispose();
|
|
1670
|
+
this.builders.clear();
|
|
1671
|
+
for (const request of this.history.values()) request.controller.abort();
|
|
1672
|
+
this.history.clear();
|
|
1673
|
+
}
|
|
1674
|
+
builder(id) {
|
|
1675
|
+
let builder = this.builders.get(id);
|
|
1676
|
+
if (!builder) {
|
|
1677
|
+
builder = new CandleBuilder();
|
|
1678
|
+
this.builders.set(id, builder);
|
|
1679
|
+
}
|
|
1680
|
+
return builder;
|
|
1681
|
+
}
|
|
1682
|
+
mark(id, snapshot, trade) {
|
|
1683
|
+
var _a, _b, _c;
|
|
1684
|
+
const previous = this.dirty.get(id);
|
|
1685
|
+
this.measurements.received++;
|
|
1686
|
+
if (previous) this.measurements.coalesced++;
|
|
1687
|
+
this.dirty.set(id, {
|
|
1688
|
+
snapshot: snapshot || !!(previous == null ? void 0 : previous.snapshot),
|
|
1689
|
+
receivedAtMs: (_a = previous == null ? void 0 : previous.receivedAtMs) != null ? _a : this.receivedAtMs || Date.now(),
|
|
1690
|
+
trades: trade ? [trade, ...(_b = previous == null ? void 0 : previous.trades) != null ? _b : []].slice(0, 50) : (_c = previous == null ? void 0 : previous.trades) != null ? _c : []
|
|
1691
|
+
});
|
|
1692
|
+
this.measurements.maxPendingOutcomes = Math.max(
|
|
1693
|
+
this.measurements.maxPendingOutcomes,
|
|
1694
|
+
this.dirty.size
|
|
1695
|
+
);
|
|
1696
|
+
this.schedule();
|
|
1697
|
+
}
|
|
1698
|
+
schedule() {
|
|
1699
|
+
if (this.timer || this.inFlight || this.disposed || !this.observer) return;
|
|
1700
|
+
this.timer = setTimeout(() => {
|
|
1701
|
+
this.timer = void 0;
|
|
1702
|
+
void this.publish();
|
|
1703
|
+
}, 50);
|
|
1704
|
+
}
|
|
1705
|
+
publish() {
|
|
1706
|
+
return __async(this, null, function* () {
|
|
1707
|
+
var _a, _b;
|
|
1708
|
+
if (this.inFlight || this.disposed || !this.observer || !this.dirty.size) return;
|
|
1709
|
+
const views = [...this.dirty].map(([outcomeId, dirty]) => {
|
|
1710
|
+
var _a2, _b2, _c;
|
|
1711
|
+
const builder = this.builder(outcomeId);
|
|
1712
|
+
return __spreadProps(__spreadValues({
|
|
1713
|
+
outcomeId,
|
|
1714
|
+
revision: this.revision,
|
|
1715
|
+
integrity: ((_a2 = this.socket) == null ? void 0 : _a2.isResyncing(outcomeId)) ? "resyncing" : "ok",
|
|
1716
|
+
book: (_c = (_b2 = this.socket) == null ? void 0 : _b2.getBook(outcomeId)) != null ? _c : null
|
|
1717
|
+
}, dirty), {
|
|
1718
|
+
candles: Object.fromEntries(
|
|
1719
|
+
intervals.map((interval) => [
|
|
1720
|
+
interval,
|
|
1721
|
+
{
|
|
1722
|
+
forming: builder.getForming(interval) ? __spreadValues({}, builder.getForming(interval)) : null,
|
|
1723
|
+
// Bounded recovery tail. Full historical ranges never travel per tick.
|
|
1724
|
+
closed: builder.getClosed(interval).filter(
|
|
1725
|
+
(candle) => {
|
|
1726
|
+
var _a3, _b3;
|
|
1727
|
+
return candle.openTime > ((_b3 = (_a3 = this.deliveredClosed.get(outcomeId)) == null ? void 0 : _a3[interval]) != null ? _b3 : -Infinity);
|
|
1728
|
+
}
|
|
1729
|
+
).map((candle) => __spreadValues({}, candle))
|
|
1730
|
+
}
|
|
1731
|
+
])
|
|
1732
|
+
)
|
|
1733
|
+
});
|
|
1734
|
+
});
|
|
1735
|
+
const generation = this.generation;
|
|
1736
|
+
this.dirty.clear();
|
|
1737
|
+
this.inFlight = true;
|
|
1738
|
+
const token = {};
|
|
1739
|
+
this.deliveryToken = token;
|
|
1740
|
+
const cancelled = new Promise((resolve) => {
|
|
1741
|
+
this.cancelDelivery = resolve;
|
|
1742
|
+
});
|
|
1743
|
+
this.measurements.maxInFlight = 1;
|
|
1744
|
+
try {
|
|
1745
|
+
yield Promise.race([
|
|
1746
|
+
this.observer({ generation, views, publishedAtMs: Date.now() }),
|
|
1747
|
+
cancelled
|
|
1748
|
+
]);
|
|
1749
|
+
if (this.disposed || this.deliveryToken !== token) return;
|
|
1750
|
+
for (const view of generation === this.generation ? views : []) {
|
|
1751
|
+
const delivered = (_a = this.deliveredClosed.get(view.outcomeId)) != null ? _a : {};
|
|
1752
|
+
for (const interval of intervals) {
|
|
1753
|
+
const last = view.candles[interval].closed.at(-1);
|
|
1754
|
+
if (last) delivered[interval] = last.openTime;
|
|
1755
|
+
}
|
|
1756
|
+
this.deliveredClosed.set(view.outcomeId, delivered);
|
|
1757
|
+
}
|
|
1758
|
+
this.measurements.published++;
|
|
1759
|
+
} catch (e) {
|
|
1760
|
+
if (!this.disposed && this.deliveryToken === token) (_b = this.socket) == null ? void 0 : _b.disconnect();
|
|
1761
|
+
} finally {
|
|
1762
|
+
if (this.deliveryToken === token) {
|
|
1763
|
+
this.deliveryToken = null;
|
|
1764
|
+
this.cancelDelivery = void 0;
|
|
1765
|
+
this.inFlight = false;
|
|
1766
|
+
if (this.dirty.size) this.schedule();
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1773
|
+
0 && (module.exports = {
|
|
1774
|
+
MarketDataEngine,
|
|
1775
|
+
decodeChartHistory
|
|
1776
|
+
});
|