@liberfi.io/react-predict 0.3.69 → 0.3.71
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/dist/index.d.mts +246 -3
- package/dist/index.d.ts +246 -3
- package/dist/index.js +1472 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1455 -9
- package/dist/index.mjs.map +1 -1
- package/dist/{server-CCZfNoWO.d.mts → server-1_Lm8FLS.d.mts} +143 -2
- package/dist/{server-CCZfNoWO.d.ts → server-1_Lm8FLS.d.ts} +143 -2
- package/dist/server.d.mts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +126 -0
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +126 -0
- package/dist/server.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { httpGet, httpPost, httpDelete } from '@liberfi.io/utils';
|
|
2
|
-
import { createContext, useMemo, useState, useRef, useCallback, useContext,
|
|
2
|
+
import { createContext, useMemo, useEffect, useState, useRef, useCallback, useContext, useSyncExternalStore } from 'react';
|
|
3
3
|
import { jsx } from 'react/jsx-runtime';
|
|
4
4
|
import { useQuery, useInfiniteQuery, useQueryClient, useQueries, useMutation } from '@tanstack/react-query';
|
|
5
5
|
import { OrderBuilder, SignatureTypeV2, Side, OrderType, orderToJsonV2, ClobClient } from '@polymarket/clob-client-v2';
|
|
6
6
|
|
|
7
7
|
// src/client/client.ts
|
|
8
|
+
|
|
9
|
+
// src/client/types.ts
|
|
10
|
+
var MARKET_STRUCTURE_MEDIA_TYPE_V1 = "application/vnd.liberfi.market-structure+json;v=1";
|
|
11
|
+
|
|
12
|
+
// src/client/client.ts
|
|
13
|
+
var MarketDataHttpError = class extends Error {
|
|
14
|
+
constructor(message, status, code, retryAfter, body) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.retryAfter = retryAfter;
|
|
19
|
+
this.body = body;
|
|
20
|
+
}
|
|
21
|
+
name = "MarketDataHttpError";
|
|
22
|
+
};
|
|
8
23
|
function buildQuery(params) {
|
|
9
24
|
const qs = new URLSearchParams();
|
|
10
25
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -31,6 +46,26 @@ var PredictClient = class {
|
|
|
31
46
|
}
|
|
32
47
|
};
|
|
33
48
|
}
|
|
49
|
+
async marketDataRequest(path, options) {
|
|
50
|
+
const response = await fetch(
|
|
51
|
+
`${this.endpoint}${path}`,
|
|
52
|
+
this.requestOptions(options)
|
|
53
|
+
);
|
|
54
|
+
if (response.ok) {
|
|
55
|
+
return await response.json();
|
|
56
|
+
}
|
|
57
|
+
const body = await parseMarketDataResponseBody(response);
|
|
58
|
+
const record = isRecord(body) ? body : void 0;
|
|
59
|
+
const code = typeof record?.error === "string" ? record.error : typeof record?.code === "string" ? record.code : void 0;
|
|
60
|
+
const message = typeof record?.message === "string" ? record.message : code ?? response.statusText ?? `HTTP ${response.status}`;
|
|
61
|
+
throw new MarketDataHttpError(
|
|
62
|
+
message,
|
|
63
|
+
response.status,
|
|
64
|
+
code,
|
|
65
|
+
response.headers.get("Retry-After") ?? void 0,
|
|
66
|
+
body
|
|
67
|
+
);
|
|
68
|
+
}
|
|
34
69
|
// -------------------------------------------------------------------------
|
|
35
70
|
// Events
|
|
36
71
|
// -------------------------------------------------------------------------
|
|
@@ -165,6 +200,85 @@ var PredictClient = class {
|
|
|
165
200
|
// -------------------------------------------------------------------------
|
|
166
201
|
// Markets
|
|
167
202
|
// -------------------------------------------------------------------------
|
|
203
|
+
/** Register or refresh the complete provider-neutral market-data demand set. */
|
|
204
|
+
async watchMarketData(request) {
|
|
205
|
+
return await this.marketDataRequest(
|
|
206
|
+
"/api/v1/market-data/watch",
|
|
207
|
+
{
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers: { "Content-Type": "application/json" },
|
|
210
|
+
body: JSON.stringify(request)
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
/** Read cache-only quote snapshots for at most 500 provider-neutral keys. */
|
|
215
|
+
async getMarketDataQuotes(markets) {
|
|
216
|
+
if (markets.length > 500) {
|
|
217
|
+
throw new RangeError(
|
|
218
|
+
"market data quote request accepts at most 500 markets"
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return await this.marketDataRequest(
|
|
222
|
+
"/api/v1/markets/quotes",
|
|
223
|
+
{
|
|
224
|
+
method: "POST",
|
|
225
|
+
cache: "no-store",
|
|
226
|
+
headers: { "Content-Type": "application/json" },
|
|
227
|
+
body: JSON.stringify({ markets })
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
/** Read all cached Top20 outcome books for one provider-neutral market. */
|
|
232
|
+
async getMarketDataOrderbooks(slug, source) {
|
|
233
|
+
const path = `/api/v1/markets/${encodeURIComponent(slug)}/orderbooks` + buildQuery({ source });
|
|
234
|
+
return await this.marketDataRequest(path, {
|
|
235
|
+
method: "GET",
|
|
236
|
+
cache: "no-store"
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Revalidate a page's allowlist-only structure representation.
|
|
241
|
+
*
|
|
242
|
+
* The path must remain inside the prediction API so callers cannot turn a
|
|
243
|
+
* configured authenticated client into a cross-origin request primitive.
|
|
244
|
+
*/
|
|
245
|
+
async getMarketStructure(path, ifNoneMatch) {
|
|
246
|
+
if (!path.startsWith("/api/v1/")) {
|
|
247
|
+
throw new TypeError("market structure path must start with /api/v1/");
|
|
248
|
+
}
|
|
249
|
+
const response = await fetch(
|
|
250
|
+
`${this.endpoint}${path}`,
|
|
251
|
+
this.requestOptions({
|
|
252
|
+
method: "GET",
|
|
253
|
+
cache: "no-store",
|
|
254
|
+
headers: {
|
|
255
|
+
Accept: MARKET_STRUCTURE_MEDIA_TYPE_V1,
|
|
256
|
+
...ifNoneMatch ? { "If-None-Match": ifNoneMatch } : {}
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
);
|
|
260
|
+
const etag = response.headers.get("ETag") ?? "";
|
|
261
|
+
if (response.status === 304) {
|
|
262
|
+
return { status: 304, etag };
|
|
263
|
+
}
|
|
264
|
+
if (response.ok) {
|
|
265
|
+
return {
|
|
266
|
+
status: 200,
|
|
267
|
+
etag,
|
|
268
|
+
body: await response.json()
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const body = await parseMarketDataResponseBody(response);
|
|
272
|
+
const record = isRecord(body) ? body : void 0;
|
|
273
|
+
const code = typeof record?.error === "string" ? record.error : void 0;
|
|
274
|
+
throw new MarketDataHttpError(
|
|
275
|
+
typeof record?.message === "string" ? record.message : code ?? response.statusText,
|
|
276
|
+
response.status,
|
|
277
|
+
code,
|
|
278
|
+
response.headers.get("Retry-After") ?? void 0,
|
|
279
|
+
body
|
|
280
|
+
);
|
|
281
|
+
}
|
|
168
282
|
/**
|
|
169
283
|
* Fetch a single prediction market by its slug.
|
|
170
284
|
*
|
|
@@ -634,6 +748,18 @@ var PredictClient = class {
|
|
|
634
748
|
function createPredictClient(endpoint, options) {
|
|
635
749
|
return new PredictClient(endpoint, options);
|
|
636
750
|
}
|
|
751
|
+
async function parseMarketDataResponseBody(response) {
|
|
752
|
+
const text = await response.text().catch(() => "");
|
|
753
|
+
if (!text) return void 0;
|
|
754
|
+
try {
|
|
755
|
+
return JSON.parse(text);
|
|
756
|
+
} catch {
|
|
757
|
+
return void 0;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function isRecord(value) {
|
|
761
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
762
|
+
}
|
|
637
763
|
|
|
638
764
|
// src/client/ws.ts
|
|
639
765
|
var DEFAULT_RECONNECT_BASE = 1e3;
|
|
@@ -947,19 +1073,19 @@ var PredictWsClient = class {
|
|
|
947
1073
|
JSON.stringify({ type, channels, market_slugs: [] })
|
|
948
1074
|
);
|
|
949
1075
|
const budget = MAX_SUBSCRIPTION_FRAME_BYTES - envelopeBytes;
|
|
950
|
-
let
|
|
1076
|
+
let chunk2 = [];
|
|
951
1077
|
let chunkBytes = 0;
|
|
952
1078
|
const flush = () => {
|
|
953
|
-
if (
|
|
954
|
-
this.send({ type, channels, market_slugs:
|
|
955
|
-
|
|
1079
|
+
if (chunk2.length > 0) {
|
|
1080
|
+
this.send({ type, channels, market_slugs: chunk2 });
|
|
1081
|
+
chunk2 = [];
|
|
956
1082
|
chunkBytes = 0;
|
|
957
1083
|
}
|
|
958
1084
|
};
|
|
959
1085
|
for (const slug of marketSlugs) {
|
|
960
1086
|
const slugBytes = byteLength(JSON.stringify(slug)) + 1;
|
|
961
|
-
if (
|
|
962
|
-
|
|
1087
|
+
if (chunk2.length > 0 && chunkBytes + slugBytes > budget) flush();
|
|
1088
|
+
chunk2.push(slug);
|
|
963
1089
|
chunkBytes += slugBytes;
|
|
964
1090
|
}
|
|
965
1091
|
flush();
|
|
@@ -1002,6 +1128,1258 @@ var PredictWsClient = class {
|
|
|
1002
1128
|
function createPredictWsClient(config) {
|
|
1003
1129
|
return new PredictWsClient(config);
|
|
1004
1130
|
}
|
|
1131
|
+
|
|
1132
|
+
// src/market-data/contract.ts
|
|
1133
|
+
var MARKET_DATA_BACKEND_CONTRACT_COMMIT = "0d0bab5c38dda76658890395bd992de84477267a";
|
|
1134
|
+
var MARKET_DATA_CONTRACT_MANIFEST_SHA256 = "81c503ed6f1bb5b6f8f20ff9f448549d0b89553b36dbc422d09c3e4a898e4aaf";
|
|
1135
|
+
|
|
1136
|
+
// src/market-data/initialQuote.ts
|
|
1137
|
+
function parseInitialQuotes(input, structure) {
|
|
1138
|
+
if (input === void 0) return { kind: "missing" };
|
|
1139
|
+
try {
|
|
1140
|
+
validateInitialQuotes(input, structure);
|
|
1141
|
+
return { kind: "valid", value: input };
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
return {
|
|
1144
|
+
kind: "malformed",
|
|
1145
|
+
reason: error instanceof Error ? error.message : "invalid initial_quotes"
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function validateInitialQuotes(input, structure) {
|
|
1150
|
+
const root = requireRecord(input, "initial_quotes");
|
|
1151
|
+
if (root.schema_version !== 1) {
|
|
1152
|
+
throw new Error("initial_quotes schema_version must be 1");
|
|
1153
|
+
}
|
|
1154
|
+
const markets = requireArray(root.markets, "initial_quotes markets");
|
|
1155
|
+
if (markets.length !== structure.length) {
|
|
1156
|
+
throw new Error("initial_quotes markets must match structural markets");
|
|
1157
|
+
}
|
|
1158
|
+
const structuralMarkets = new Map(
|
|
1159
|
+
structure.map((market) => [
|
|
1160
|
+
marketIdentity(market.source, market.market_slug),
|
|
1161
|
+
market
|
|
1162
|
+
])
|
|
1163
|
+
);
|
|
1164
|
+
if (structuralMarkets.size !== structure.length) {
|
|
1165
|
+
throw new Error("structural markets must have unique identities");
|
|
1166
|
+
}
|
|
1167
|
+
const seenMarkets = /* @__PURE__ */ new Set();
|
|
1168
|
+
markets.forEach((marketValue, marketIndex) => {
|
|
1169
|
+
const market = requireRecord(
|
|
1170
|
+
marketValue,
|
|
1171
|
+
`initial_quotes market ${marketIndex}`
|
|
1172
|
+
);
|
|
1173
|
+
if (typeof market.source !== "string" || typeof market.market_slug !== "string") {
|
|
1174
|
+
throw new Error(
|
|
1175
|
+
`initial_quotes market ${marketIndex} requires source and market_slug`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
const identity = marketIdentity(market.source, market.market_slug);
|
|
1179
|
+
const structuralMarket = structuralMarkets.get(identity);
|
|
1180
|
+
if (!structuralMarket || seenMarkets.has(identity)) {
|
|
1181
|
+
throw new Error(
|
|
1182
|
+
`initial_quotes market ${marketIndex} must match structural market`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
seenMarkets.add(identity);
|
|
1186
|
+
if (typeof market.realtime_supported !== "boolean") {
|
|
1187
|
+
throw new Error(
|
|
1188
|
+
`initial_quotes market ${marketIndex} requires realtime_supported`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
const outcomes = requireArray(
|
|
1192
|
+
market.outcomes,
|
|
1193
|
+
`initial_quotes market ${marketIndex} outcomes`
|
|
1194
|
+
);
|
|
1195
|
+
if (outcomes.length !== structuralMarket.outcomes.length) {
|
|
1196
|
+
throw new Error(
|
|
1197
|
+
`initial_quotes market ${marketIndex} outcomes must match structure`
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
const structuralOutcomes = new Set(structuralMarket.outcomes);
|
|
1201
|
+
if (structuralOutcomes.size !== structuralMarket.outcomes.length) {
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
`structural market ${marketIndex} outcomes must have unique identities`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
const seenOutcomes = /* @__PURE__ */ new Set();
|
|
1207
|
+
outcomes.forEach((outcomeValue, outcomeIndex) => {
|
|
1208
|
+
const outcome = requireRecord(
|
|
1209
|
+
outcomeValue,
|
|
1210
|
+
`initial_quotes market ${marketIndex} outcome ${outcomeIndex}`
|
|
1211
|
+
);
|
|
1212
|
+
if (outcome.source !== structuralMarket.source || outcome.market_slug !== structuralMarket.market_slug || typeof outcome.outcome !== "string" || !structuralOutcomes.has(outcome.outcome) || seenOutcomes.has(outcome.outcome)) {
|
|
1213
|
+
throw new Error(
|
|
1214
|
+
`initial_quotes market ${marketIndex} outcome ${outcomeIndex} must match structure`
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
seenOutcomes.add(outcome.outcome);
|
|
1218
|
+
assertMarketDataBBAComponent(
|
|
1219
|
+
outcome.bba,
|
|
1220
|
+
`initial_quotes market ${marketIndex} outcome ${outcomeIndex} bba`
|
|
1221
|
+
);
|
|
1222
|
+
assertMarketDataValueComponent(
|
|
1223
|
+
outcome.last_trade,
|
|
1224
|
+
`initial_quotes market ${marketIndex} outcome ${outcomeIndex} last_trade`,
|
|
1225
|
+
false
|
|
1226
|
+
);
|
|
1227
|
+
assertMarketDataValueComponent(
|
|
1228
|
+
outcome.tick_size,
|
|
1229
|
+
`initial_quotes market ${marketIndex} outcome ${outcomeIndex} tick_size`,
|
|
1230
|
+
true
|
|
1231
|
+
);
|
|
1232
|
+
if (!market.realtime_supported && (outcome.bba.available || outcome.last_trade.available || outcome.tick_size.available)) {
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`unsupported initial_quotes market ${marketIndex} must be unavailable`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
function marketIdentity(source, marketSlug) {
|
|
1241
|
+
return `${source}\0${marketSlug}`;
|
|
1242
|
+
}
|
|
1243
|
+
function assertMarketDataBBAComponent(input, name) {
|
|
1244
|
+
const component = requireRecord(input, name);
|
|
1245
|
+
if (typeof component.available !== "boolean" || typeof component.empty !== "boolean") {
|
|
1246
|
+
throw new Error(`${name} requires available and empty`);
|
|
1247
|
+
}
|
|
1248
|
+
validateTimestampFields(component, name);
|
|
1249
|
+
if (!component.available) {
|
|
1250
|
+
if (component.empty || component.best_bid !== void 0 || component.best_ask !== void 0 || component.midpoint !== void 0) {
|
|
1251
|
+
throw new Error(`${name} unavailable component must omit quote values`);
|
|
1252
|
+
}
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
requireObservedAt(component, name);
|
|
1256
|
+
if (component.empty) {
|
|
1257
|
+
if (component.best_bid !== void 0 || component.best_ask !== void 0 || component.midpoint !== void 0) {
|
|
1258
|
+
throw new Error(`${name} empty component must omit quote values`);
|
|
1259
|
+
}
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
const bid = optionalProbability(component.best_bid, `${name} best_bid`);
|
|
1263
|
+
const ask = optionalProbability(component.best_ask, `${name} best_ask`);
|
|
1264
|
+
const midpoint = optionalProbability(component.midpoint, `${name} midpoint`);
|
|
1265
|
+
if (bid === void 0 && ask === void 0) {
|
|
1266
|
+
throw new Error(`${name} non-empty component requires a bid or ask`);
|
|
1267
|
+
}
|
|
1268
|
+
if (bid !== void 0 && ask !== void 0) {
|
|
1269
|
+
if (bid >= ask) throw new Error(`${name} best bid must be lower than ask`);
|
|
1270
|
+
if (midpoint === void 0 || Math.abs(midpoint - (bid + ask) / 2) > 1e-12) {
|
|
1271
|
+
throw new Error(`${name} midpoint must match bid and ask`);
|
|
1272
|
+
}
|
|
1273
|
+
} else if (midpoint !== void 0) {
|
|
1274
|
+
throw new Error(`${name} midpoint requires bid and ask`);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function assertMarketDataValueComponent(input, name, positive) {
|
|
1278
|
+
const component = requireRecord(input, name);
|
|
1279
|
+
if (typeof component.available !== "boolean") {
|
|
1280
|
+
throw new Error(`${name} requires available`);
|
|
1281
|
+
}
|
|
1282
|
+
validateTimestampFields(component, name);
|
|
1283
|
+
if (!component.available) {
|
|
1284
|
+
if (component.value !== void 0) {
|
|
1285
|
+
throw new Error(`${name} unavailable component must omit value`);
|
|
1286
|
+
}
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
const value = optionalProbability(component.value, `${name} value`);
|
|
1290
|
+
if (value === void 0) throw new Error(`${name} requires value`);
|
|
1291
|
+
if (positive && value === 0) throw new Error(`${name} must be positive`);
|
|
1292
|
+
requireObservedAt(component, name);
|
|
1293
|
+
}
|
|
1294
|
+
function validateTimestampFields(component, name) {
|
|
1295
|
+
if (component.provider_timestamp !== void 0 && !isRFC3339(component.provider_timestamp)) {
|
|
1296
|
+
throw new Error(`${name} provider_timestamp must be RFC3339`);
|
|
1297
|
+
}
|
|
1298
|
+
if (component.observed_at !== void 0 && !isRFC3339(component.observed_at)) {
|
|
1299
|
+
throw new Error(`${name} observed_at must be RFC3339`);
|
|
1300
|
+
}
|
|
1301
|
+
if (component.provider_timestamp !== void 0 && component.observed_at === void 0) {
|
|
1302
|
+
throw new Error(`${name} provider_timestamp requires observed_at`);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
function requireObservedAt(component, name) {
|
|
1306
|
+
if (!isRFC3339(component.observed_at)) {
|
|
1307
|
+
throw new Error(`${name} available component requires observed_at`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
function optionalProbability(value, name) {
|
|
1311
|
+
if (value === void 0) return void 0;
|
|
1312
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
1313
|
+
throw new Error(`${name} must be a finite probability`);
|
|
1314
|
+
}
|
|
1315
|
+
return value;
|
|
1316
|
+
}
|
|
1317
|
+
function isRFC3339(value) {
|
|
1318
|
+
return typeof value === "string" && value.length > 0 && Number.isFinite(Date.parse(value));
|
|
1319
|
+
}
|
|
1320
|
+
function requireRecord(value, name) {
|
|
1321
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1322
|
+
throw new Error(`${name} must be an object`);
|
|
1323
|
+
}
|
|
1324
|
+
return value;
|
|
1325
|
+
}
|
|
1326
|
+
function requireArray(value, name) {
|
|
1327
|
+
if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
|
|
1328
|
+
return value;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/market-data/protocol.ts
|
|
1332
|
+
function comparePublicationCursor(candidate, current) {
|
|
1333
|
+
if (candidate.generation < current.generation) return "older";
|
|
1334
|
+
if (candidate.generation > current.generation) return "newer_generation";
|
|
1335
|
+
if (candidate.epoch !== current.epoch) return "epoch_changed";
|
|
1336
|
+
if (candidate.offset < current.offset) return "older";
|
|
1337
|
+
if (candidate.offset > current.offset) return "newer";
|
|
1338
|
+
return "same";
|
|
1339
|
+
}
|
|
1340
|
+
function decodeItemEnvelope(input) {
|
|
1341
|
+
const root = asRecord(input);
|
|
1342
|
+
if (root?.schema_version !== 1) {
|
|
1343
|
+
return failure("incompatible_schema", "item schema_version must be 1");
|
|
1344
|
+
}
|
|
1345
|
+
try {
|
|
1346
|
+
const changes = root.changes === void 0 ? void 0 : requireArray2(root.changes, "changes").map(decodeItemChange);
|
|
1347
|
+
const invalidation = root.resource_invalidated === void 0 ? void 0 : decodeInvalidation(root.resource_invalidated);
|
|
1348
|
+
if ((!changes || changes.length === 0) && !invalidation) {
|
|
1349
|
+
throw new Error("item envelope requires changes or invalidation");
|
|
1350
|
+
}
|
|
1351
|
+
return {
|
|
1352
|
+
ok: true,
|
|
1353
|
+
value: {
|
|
1354
|
+
schema_version: 1,
|
|
1355
|
+
...changes ? { changes } : {},
|
|
1356
|
+
...invalidation ? { resource_invalidated: invalidation } : {}
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
return failure(
|
|
1361
|
+
"malformed",
|
|
1362
|
+
error instanceof Error ? error.message : "malformed item envelope"
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
function decodeBookEnvelope(input) {
|
|
1367
|
+
const root = asRecord(input);
|
|
1368
|
+
if (root?.schema_version !== 1) {
|
|
1369
|
+
return failure("incompatible_schema", "book schema_version must be 1");
|
|
1370
|
+
}
|
|
1371
|
+
try {
|
|
1372
|
+
const source = requireSource(root.source);
|
|
1373
|
+
const marketSlug = requireIdentity(root.market_slug, "market_slug");
|
|
1374
|
+
const outcome = requireIdentity(root.outcome, "outcome");
|
|
1375
|
+
const observedAt = requireTimestamp(root.observed_at, "observed_at");
|
|
1376
|
+
if (typeof root.available !== "boolean") {
|
|
1377
|
+
throw new Error("book available must be boolean");
|
|
1378
|
+
}
|
|
1379
|
+
const providerTimestamp = root.provider_timestamp === void 0 ? void 0 : requireTimestamp(root.provider_timestamp, "provider_timestamp");
|
|
1380
|
+
if (!root.available) {
|
|
1381
|
+
if (root.bids !== void 0 || root.asks !== void 0) {
|
|
1382
|
+
throw new Error("unavailable book must omit levels");
|
|
1383
|
+
}
|
|
1384
|
+
return {
|
|
1385
|
+
ok: true,
|
|
1386
|
+
value: {
|
|
1387
|
+
schema_version: 1,
|
|
1388
|
+
available: false,
|
|
1389
|
+
source,
|
|
1390
|
+
market_slug: marketSlug,
|
|
1391
|
+
outcome,
|
|
1392
|
+
observed_at: observedAt,
|
|
1393
|
+
...providerTimestamp ? { provider_timestamp: providerTimestamp } : {}
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
const bids = decodeBookLevels(root.bids, "bids");
|
|
1398
|
+
const asks = decodeBookLevels(root.asks, "asks");
|
|
1399
|
+
return {
|
|
1400
|
+
ok: true,
|
|
1401
|
+
value: {
|
|
1402
|
+
schema_version: 1,
|
|
1403
|
+
available: true,
|
|
1404
|
+
source,
|
|
1405
|
+
market_slug: marketSlug,
|
|
1406
|
+
outcome,
|
|
1407
|
+
observed_at: observedAt,
|
|
1408
|
+
bids,
|
|
1409
|
+
asks,
|
|
1410
|
+
...providerTimestamp ? { provider_timestamp: providerTimestamp } : {}
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
return failure(
|
|
1415
|
+
"malformed",
|
|
1416
|
+
error instanceof Error ? error.message : "malformed book envelope"
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
function decodeItemChange(value) {
|
|
1421
|
+
const change = requireRecord2(value, "change");
|
|
1422
|
+
const source = requireSource(change.source);
|
|
1423
|
+
const marketSlug = requireIdentity(change.market_slug, "market_slug");
|
|
1424
|
+
const outcome = requireIdentity(change.outcome, "outcome");
|
|
1425
|
+
const decoded = {
|
|
1426
|
+
source,
|
|
1427
|
+
market_slug: marketSlug,
|
|
1428
|
+
outcome
|
|
1429
|
+
};
|
|
1430
|
+
if (change.bba !== void 0) {
|
|
1431
|
+
assertMarketDataBBAComponent(change.bba, "bba");
|
|
1432
|
+
decoded.bba = change.bba;
|
|
1433
|
+
}
|
|
1434
|
+
if (change.last_trade !== void 0) {
|
|
1435
|
+
assertMarketDataValueComponent(change.last_trade, "last_trade", false);
|
|
1436
|
+
decoded.last_trade = change.last_trade;
|
|
1437
|
+
}
|
|
1438
|
+
if (change.tick_size !== void 0) {
|
|
1439
|
+
assertMarketDataValueComponent(change.tick_size, "tick_size", true);
|
|
1440
|
+
decoded.tick_size = change.tick_size;
|
|
1441
|
+
}
|
|
1442
|
+
if (change.market_status !== void 0) {
|
|
1443
|
+
decoded.market_status = decodeStatus(change.market_status);
|
|
1444
|
+
}
|
|
1445
|
+
if (decoded.bba === void 0 && decoded.last_trade === void 0 && decoded.tick_size === void 0 && decoded.market_status === void 0) {
|
|
1446
|
+
throw new Error("item change requires a component replacement");
|
|
1447
|
+
}
|
|
1448
|
+
return decoded;
|
|
1449
|
+
}
|
|
1450
|
+
function decodeStatus(value) {
|
|
1451
|
+
const status = requireRecord2(value, "market_status");
|
|
1452
|
+
if (status.value !== "pending" && status.value !== "open" && status.value !== "closed" && status.value !== "voided") {
|
|
1453
|
+
throw new Error("unsupported market_status");
|
|
1454
|
+
}
|
|
1455
|
+
return {
|
|
1456
|
+
value: status.value,
|
|
1457
|
+
observed_at: requireTimestamp(
|
|
1458
|
+
status.observed_at,
|
|
1459
|
+
"market_status observed_at"
|
|
1460
|
+
)
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
function decodeInvalidation(value) {
|
|
1464
|
+
const invalidation = requireRecord2(value, "resource_invalidated");
|
|
1465
|
+
if (invalidation.reason !== "market_membership_changed") {
|
|
1466
|
+
throw new Error("unsupported invalidation reason");
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
reason: invalidation.reason,
|
|
1470
|
+
observed_at: requireTimestamp(
|
|
1471
|
+
invalidation.observed_at,
|
|
1472
|
+
"resource_invalidated observed_at"
|
|
1473
|
+
)
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
function decodeBookLevels(value, name) {
|
|
1477
|
+
return requireArray2(value, name).map((levelValue, index) => {
|
|
1478
|
+
const level = requireRecord2(levelValue, `${name} ${index}`);
|
|
1479
|
+
if (typeof level.price !== "string" || !isDecimal(level.price) || typeof level.size !== "string" || !isDecimal(level.size)) {
|
|
1480
|
+
throw new Error(
|
|
1481
|
+
`${name} ${index} requires decimal string price and size`
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
return { price: level.price, size: level.size };
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
function requireSource(value) {
|
|
1488
|
+
if (value !== "polymarket" && value !== "kalshi") {
|
|
1489
|
+
throw new Error("unsupported source");
|
|
1490
|
+
}
|
|
1491
|
+
return value;
|
|
1492
|
+
}
|
|
1493
|
+
function requireIdentity(value, name) {
|
|
1494
|
+
if (typeof value !== "string" || value.trim() === "" || /[\u0000-\u001f]/.test(value)) {
|
|
1495
|
+
throw new Error(`${name} must be a non-empty identity`);
|
|
1496
|
+
}
|
|
1497
|
+
return value;
|
|
1498
|
+
}
|
|
1499
|
+
function requireTimestamp(value, name) {
|
|
1500
|
+
if (typeof value !== "string" || value.length === 0 || !Number.isFinite(Date.parse(value))) {
|
|
1501
|
+
throw new Error(`${name} must be RFC3339`);
|
|
1502
|
+
}
|
|
1503
|
+
return value;
|
|
1504
|
+
}
|
|
1505
|
+
function isDecimal(value) {
|
|
1506
|
+
return /^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value);
|
|
1507
|
+
}
|
|
1508
|
+
function requireRecord2(value, name) {
|
|
1509
|
+
const record = asRecord(value);
|
|
1510
|
+
if (!record) throw new Error(`${name} must be an object`);
|
|
1511
|
+
return record;
|
|
1512
|
+
}
|
|
1513
|
+
function asRecord(value) {
|
|
1514
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1515
|
+
}
|
|
1516
|
+
function requireArray2(value, name) {
|
|
1517
|
+
if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
|
|
1518
|
+
return value;
|
|
1519
|
+
}
|
|
1520
|
+
function failure(reason, message) {
|
|
1521
|
+
return { ok: false, reason, message };
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// src/market-data/recoveryBarrier.ts
|
|
1525
|
+
var RecoveryBarrier = class {
|
|
1526
|
+
generation;
|
|
1527
|
+
maxBufferedPublications;
|
|
1528
|
+
commitValue;
|
|
1529
|
+
buffered = [];
|
|
1530
|
+
currentCursor;
|
|
1531
|
+
currentEpoch;
|
|
1532
|
+
state = "buffering";
|
|
1533
|
+
constructor(options) {
|
|
1534
|
+
if (options.maxBufferedPublications < 1) {
|
|
1535
|
+
throw new RangeError("maxBufferedPublications must be positive");
|
|
1536
|
+
}
|
|
1537
|
+
this.generation = options.generation;
|
|
1538
|
+
this.maxBufferedPublications = options.maxBufferedPublications;
|
|
1539
|
+
this.commitValue = options.commit;
|
|
1540
|
+
}
|
|
1541
|
+
acknowledge(input) {
|
|
1542
|
+
if (this.state === "invalidated") return this.state;
|
|
1543
|
+
if (input.epoch.length === 0) {
|
|
1544
|
+
return this.requireRecovery();
|
|
1545
|
+
}
|
|
1546
|
+
this.currentEpoch = input.epoch;
|
|
1547
|
+
this.currentCursor = void 0;
|
|
1548
|
+
this.state = "catching_up";
|
|
1549
|
+
return this.state;
|
|
1550
|
+
}
|
|
1551
|
+
completeCatchUp(cursor) {
|
|
1552
|
+
if (this.state !== "catching_up" || cursor.generation !== this.generation || cursor.epoch !== this.currentEpoch) {
|
|
1553
|
+
return this.requireRecovery();
|
|
1554
|
+
}
|
|
1555
|
+
this.currentCursor = cursor;
|
|
1556
|
+
const pending = this.buffered;
|
|
1557
|
+
this.buffered = [];
|
|
1558
|
+
let recoveryRequired = false;
|
|
1559
|
+
for (const publication of pending) {
|
|
1560
|
+
if (this.commitIfNewer(publication) === "recovery_required") {
|
|
1561
|
+
recoveryRequired = true;
|
|
1562
|
+
break;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
if (!recoveryRequired) this.state = "live";
|
|
1566
|
+
return this.state;
|
|
1567
|
+
}
|
|
1568
|
+
push(value, cursor) {
|
|
1569
|
+
if (this.state === "invalidated" || cursor.generation < this.generation) {
|
|
1570
|
+
return "ignored";
|
|
1571
|
+
}
|
|
1572
|
+
if (cursor.generation > this.generation) {
|
|
1573
|
+
this.requireRecovery();
|
|
1574
|
+
return "recovery_required";
|
|
1575
|
+
}
|
|
1576
|
+
if (this.state === "live") {
|
|
1577
|
+
return this.commitIfNewer({ value, cursor });
|
|
1578
|
+
}
|
|
1579
|
+
if (this.state === "recovery_required") return "recovery_required";
|
|
1580
|
+
if (this.buffered.length >= this.maxBufferedPublications) {
|
|
1581
|
+
this.requireRecovery();
|
|
1582
|
+
return "recovery_required";
|
|
1583
|
+
}
|
|
1584
|
+
this.buffered.push({ value, cursor });
|
|
1585
|
+
return "buffered";
|
|
1586
|
+
}
|
|
1587
|
+
invalidate() {
|
|
1588
|
+
this.buffered = [];
|
|
1589
|
+
this.currentCursor = void 0;
|
|
1590
|
+
this.state = "invalidated";
|
|
1591
|
+
}
|
|
1592
|
+
commitIfNewer(publication) {
|
|
1593
|
+
if (!this.currentCursor) {
|
|
1594
|
+
this.requireRecovery();
|
|
1595
|
+
return "recovery_required";
|
|
1596
|
+
}
|
|
1597
|
+
const order = comparePublicationCursor(
|
|
1598
|
+
publication.cursor,
|
|
1599
|
+
this.currentCursor
|
|
1600
|
+
);
|
|
1601
|
+
if (order === "older" || order === "same") return "ignored";
|
|
1602
|
+
if (order === "epoch_changed" || order === "newer_generation") {
|
|
1603
|
+
this.requireRecovery();
|
|
1604
|
+
return "recovery_required";
|
|
1605
|
+
}
|
|
1606
|
+
this.commitValue(publication.value, publication.cursor);
|
|
1607
|
+
this.currentCursor = publication.cursor;
|
|
1608
|
+
return "committed";
|
|
1609
|
+
}
|
|
1610
|
+
requireRecovery() {
|
|
1611
|
+
this.buffered = [];
|
|
1612
|
+
this.currentCursor = void 0;
|
|
1613
|
+
this.state = "recovery_required";
|
|
1614
|
+
return this.state;
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
// src/market-data/watchScheduler.ts
|
|
1619
|
+
function chunkWatchRequest(request, maxRawQuoteSelectors) {
|
|
1620
|
+
if (maxRawQuoteSelectors < 1) {
|
|
1621
|
+
throw new RangeError("maxRawQuoteSelectors must be positive");
|
|
1622
|
+
}
|
|
1623
|
+
const selectors = [
|
|
1624
|
+
...(request.quote_events ?? []).map((value) => ({
|
|
1625
|
+
kind: "event",
|
|
1626
|
+
value
|
|
1627
|
+
})),
|
|
1628
|
+
...(request.quote_markets ?? []).map((value) => ({
|
|
1629
|
+
kind: "market",
|
|
1630
|
+
value
|
|
1631
|
+
}))
|
|
1632
|
+
];
|
|
1633
|
+
if (selectors.length === 0) {
|
|
1634
|
+
return request.orderbook_market ? [{ ...request }] : [];
|
|
1635
|
+
}
|
|
1636
|
+
const chunks = [];
|
|
1637
|
+
for (let index = 0; index < selectors.length; index += maxRawQuoteSelectors) {
|
|
1638
|
+
const chunk2 = selectors.slice(index, index + maxRawQuoteSelectors);
|
|
1639
|
+
const quoteEvents = chunk2.filter(
|
|
1640
|
+
(selector) => selector.kind === "event"
|
|
1641
|
+
).map((selector) => selector.value);
|
|
1642
|
+
const quoteMarkets = chunk2.filter(
|
|
1643
|
+
(selector) => selector.kind === "market"
|
|
1644
|
+
).map((selector) => selector.value);
|
|
1645
|
+
chunks.push({
|
|
1646
|
+
...quoteEvents.length ? { quote_events: quoteEvents } : {},
|
|
1647
|
+
...quoteMarkets.length ? { quote_markets: quoteMarkets } : {},
|
|
1648
|
+
...request.orderbook_market ? { orderbook_market: request.orderbook_market } : {}
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
return chunks;
|
|
1652
|
+
}
|
|
1653
|
+
var MarketDataWatchScheduler = class {
|
|
1654
|
+
touch;
|
|
1655
|
+
maxRawQuoteSelectors;
|
|
1656
|
+
onTouchSuccess;
|
|
1657
|
+
onTouchError;
|
|
1658
|
+
timer;
|
|
1659
|
+
revision = 0;
|
|
1660
|
+
retryAttempt = 0;
|
|
1661
|
+
current;
|
|
1662
|
+
disposed = false;
|
|
1663
|
+
constructor(options) {
|
|
1664
|
+
this.touch = options.touch;
|
|
1665
|
+
this.maxRawQuoteSelectors = options.maxRawQuoteSelectors;
|
|
1666
|
+
this.onTouchSuccess = options.onTouchSuccess;
|
|
1667
|
+
this.onTouchError = options.onTouchError;
|
|
1668
|
+
}
|
|
1669
|
+
async replace(request) {
|
|
1670
|
+
if (this.disposed) throw new Error("watch scheduler is disposed");
|
|
1671
|
+
if (chunkWatchRequest(request, this.maxRawQuoteSelectors).length === 0) {
|
|
1672
|
+
this.clear();
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
this.current = request;
|
|
1676
|
+
const revision = ++this.revision;
|
|
1677
|
+
this.retryAttempt = 0;
|
|
1678
|
+
this.clearTimer();
|
|
1679
|
+
try {
|
|
1680
|
+
await this.touchCurrent(revision);
|
|
1681
|
+
} catch (error) {
|
|
1682
|
+
if (isRetryableWatchError(error)) {
|
|
1683
|
+
this.scheduleRetry(revision, error);
|
|
1684
|
+
}
|
|
1685
|
+
throw error;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
dispose() {
|
|
1689
|
+
this.disposed = true;
|
|
1690
|
+
this.clear();
|
|
1691
|
+
}
|
|
1692
|
+
clear() {
|
|
1693
|
+
this.current = void 0;
|
|
1694
|
+
this.revision++;
|
|
1695
|
+
this.retryAttempt = 0;
|
|
1696
|
+
this.clearTimer();
|
|
1697
|
+
}
|
|
1698
|
+
async touchCurrent(revision) {
|
|
1699
|
+
if (!this.current || revision !== this.revision || this.disposed) return;
|
|
1700
|
+
const chunks = chunkWatchRequest(this.current, this.maxRawQuoteSelectors);
|
|
1701
|
+
if (chunks.length === 0) throw new Error("watch request must not be empty");
|
|
1702
|
+
const responses = [];
|
|
1703
|
+
try {
|
|
1704
|
+
for (const chunk2 of chunks) {
|
|
1705
|
+
responses.push(await this.touch(chunk2));
|
|
1706
|
+
}
|
|
1707
|
+
} catch (error) {
|
|
1708
|
+
if (this.current && revision === this.revision && !this.disposed) {
|
|
1709
|
+
this.onTouchError?.(error);
|
|
1710
|
+
}
|
|
1711
|
+
throw error;
|
|
1712
|
+
}
|
|
1713
|
+
if (!this.current || revision !== this.revision || this.disposed) return;
|
|
1714
|
+
const refreshAfterMS = Math.min(
|
|
1715
|
+
...responses.map((response) => response.refresh_after_ms)
|
|
1716
|
+
);
|
|
1717
|
+
if (!Number.isFinite(refreshAfterMS) || refreshAfterMS <= 0) {
|
|
1718
|
+
throw new Error("refresh_after_ms must be positive");
|
|
1719
|
+
}
|
|
1720
|
+
this.retryAttempt = 0;
|
|
1721
|
+
this.onTouchSuccess?.();
|
|
1722
|
+
this.timer = setTimeout(() => {
|
|
1723
|
+
void this.touchCurrent(revision).catch((error) => {
|
|
1724
|
+
if (isRetryableWatchError(error)) {
|
|
1725
|
+
this.scheduleRetry(revision, error);
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
}, refreshAfterMS);
|
|
1729
|
+
}
|
|
1730
|
+
scheduleRetry(revision, error) {
|
|
1731
|
+
if (!this.current || revision !== this.revision || this.disposed) return;
|
|
1732
|
+
this.clearTimer();
|
|
1733
|
+
const retryAfterMS = retryAfterMilliseconds(error);
|
|
1734
|
+
const delay = retryAfterMS ?? Math.min(3e4, 1e3 * 2 ** Math.min(this.retryAttempt, 5));
|
|
1735
|
+
this.retryAttempt++;
|
|
1736
|
+
this.timer = setTimeout(() => {
|
|
1737
|
+
void this.touchCurrent(revision).catch((nextError) => {
|
|
1738
|
+
if (isRetryableWatchError(nextError)) {
|
|
1739
|
+
this.scheduleRetry(revision, nextError);
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
}, delay);
|
|
1743
|
+
}
|
|
1744
|
+
clearTimer() {
|
|
1745
|
+
if (this.timer !== void 0) clearTimeout(this.timer);
|
|
1746
|
+
this.timer = void 0;
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
function isRetryableWatchError(error) {
|
|
1750
|
+
if (typeof error !== "object" || error === null || !("status" in error) || typeof error.status !== "number") {
|
|
1751
|
+
return true;
|
|
1752
|
+
}
|
|
1753
|
+
return error.status === 408 || error.status === 429 || error.status >= 500;
|
|
1754
|
+
}
|
|
1755
|
+
function retryAfterMilliseconds(error) {
|
|
1756
|
+
if (typeof error !== "object" || error === null || !("retryAfter" in error) || typeof error.retryAfter !== "string") {
|
|
1757
|
+
return void 0;
|
|
1758
|
+
}
|
|
1759
|
+
const seconds = Number(error.retryAfter);
|
|
1760
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : void 0;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// src/market-data/marketDataRuntime.ts
|
|
1764
|
+
var MarketDataRuntime = class {
|
|
1765
|
+
enabled;
|
|
1766
|
+
client;
|
|
1767
|
+
store;
|
|
1768
|
+
transportFactory;
|
|
1769
|
+
resources = /* @__PURE__ */ new Map();
|
|
1770
|
+
transport;
|
|
1771
|
+
watchScheduler;
|
|
1772
|
+
disposing = false;
|
|
1773
|
+
constructor(options) {
|
|
1774
|
+
this.enabled = options.enabled;
|
|
1775
|
+
this.client = options.client;
|
|
1776
|
+
this.store = options.store;
|
|
1777
|
+
this.transportFactory = options.transportFactory;
|
|
1778
|
+
}
|
|
1779
|
+
mount(input) {
|
|
1780
|
+
if (!this.enabled) return () => void 0;
|
|
1781
|
+
if (!this.transportFactory) {
|
|
1782
|
+
throw new Error(
|
|
1783
|
+
"market data transportFactory is required when capability is enabled"
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
this.release(input.key);
|
|
1787
|
+
const started = this.store.begin(input.key, input.structureETag);
|
|
1788
|
+
this.store.update(input.key, started.generation, {
|
|
1789
|
+
phase: "buffering",
|
|
1790
|
+
initialQuotes: input.initialQuotes
|
|
1791
|
+
});
|
|
1792
|
+
const resource = {
|
|
1793
|
+
generation: started.generation,
|
|
1794
|
+
input,
|
|
1795
|
+
subscriptions: [],
|
|
1796
|
+
barriers: /* @__PURE__ */ new Map(),
|
|
1797
|
+
acknowledgements: /* @__PURE__ */ new Map(),
|
|
1798
|
+
initialQuotes: input.initialQuotes,
|
|
1799
|
+
bootstrapStarted: false,
|
|
1800
|
+
catchUpInFlight: false,
|
|
1801
|
+
bookRetryAttempt: 0,
|
|
1802
|
+
released: false
|
|
1803
|
+
};
|
|
1804
|
+
this.resources.set(input.key, resource);
|
|
1805
|
+
if (Boolean(input.bookChannel) !== Boolean(input.watch.orderbook_market)) {
|
|
1806
|
+
this.fail(
|
|
1807
|
+
resource,
|
|
1808
|
+
"incomplete_book_target",
|
|
1809
|
+
"book channel and orderbook watch selector must be provided together"
|
|
1810
|
+
);
|
|
1811
|
+
return () => this.releaseGeneration(input.key, resource.generation);
|
|
1812
|
+
}
|
|
1813
|
+
const channels = new Set(
|
|
1814
|
+
input.structure.items.map((item) => item.item_channel).filter((channel) => Boolean(channel))
|
|
1815
|
+
);
|
|
1816
|
+
if (input.bookChannel) {
|
|
1817
|
+
const declared = input.structure.items.some(
|
|
1818
|
+
(item) => item.markets.some(
|
|
1819
|
+
(market) => market.outcomes.some(
|
|
1820
|
+
(outcome) => outcome.book_channel === input.bookChannel
|
|
1821
|
+
)
|
|
1822
|
+
)
|
|
1823
|
+
);
|
|
1824
|
+
if (!declared) {
|
|
1825
|
+
this.fail(
|
|
1826
|
+
resource,
|
|
1827
|
+
"unknown_book_channel",
|
|
1828
|
+
"book channel is absent from structure"
|
|
1829
|
+
);
|
|
1830
|
+
return () => this.releaseGeneration(input.key, resource.generation);
|
|
1831
|
+
}
|
|
1832
|
+
channels.add(input.bookChannel);
|
|
1833
|
+
}
|
|
1834
|
+
if (channels.size === 0) {
|
|
1835
|
+
this.fail(resource, "missing_channel", "structure has no channel target");
|
|
1836
|
+
return () => this.releaseGeneration(input.key, resource.generation);
|
|
1837
|
+
}
|
|
1838
|
+
for (const channel of channels) this.subscribe(resource, channel);
|
|
1839
|
+
return () => this.releaseGeneration(input.key, resource.generation);
|
|
1840
|
+
}
|
|
1841
|
+
dispose() {
|
|
1842
|
+
this.disposing = true;
|
|
1843
|
+
for (const key of [...this.resources.keys()]) this.release(key);
|
|
1844
|
+
this.watchScheduler?.dispose();
|
|
1845
|
+
this.watchScheduler = void 0;
|
|
1846
|
+
this.transport?.close?.();
|
|
1847
|
+
this.transport = void 0;
|
|
1848
|
+
}
|
|
1849
|
+
subscribe(resource, channel) {
|
|
1850
|
+
const barrier = new RecoveryBarrier({
|
|
1851
|
+
generation: resource.generation,
|
|
1852
|
+
maxBufferedPublications: 1e3,
|
|
1853
|
+
commit: (value) => this.commitPublication(resource, channel, value)
|
|
1854
|
+
});
|
|
1855
|
+
resource.barriers.set(channel, barrier);
|
|
1856
|
+
const subscription = this.getTransport().subscribe(channel, {
|
|
1857
|
+
onPublication: (publication) => this.onPublication(resource, channel, publication),
|
|
1858
|
+
onSubscribed: (ack) => this.onSubscribed(resource, channel, ack),
|
|
1859
|
+
onError: (error) => this.store.update(resource.input.key, resource.generation, {
|
|
1860
|
+
phase: channel === resource.input.bookChannel ? "degraded_book" : "degraded_quote",
|
|
1861
|
+
...channel === resource.input.bookChannel ? { bookError: error } : { quoteError: error }
|
|
1862
|
+
})
|
|
1863
|
+
});
|
|
1864
|
+
resource.subscriptions.push(subscription);
|
|
1865
|
+
}
|
|
1866
|
+
onPublication(resource, channel, publication) {
|
|
1867
|
+
if (resource.released) return;
|
|
1868
|
+
const result = resource.barriers.get(channel)?.push(publication.data, {
|
|
1869
|
+
generation: resource.generation,
|
|
1870
|
+
epoch: publication.epoch,
|
|
1871
|
+
offset: publication.offset
|
|
1872
|
+
});
|
|
1873
|
+
const acknowledged = resource.acknowledgements.get(channel);
|
|
1874
|
+
if (result === "recovery_required" && acknowledged?.epoch === publication.epoch) {
|
|
1875
|
+
this.restartCatchUp(resource);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
onSubscribed(resource, channel, ack) {
|
|
1879
|
+
if (resource.released) return;
|
|
1880
|
+
const barrier = resource.barriers.get(channel);
|
|
1881
|
+
const previousState = barrier?.state;
|
|
1882
|
+
if (resource.bootstrapStarted && (previousState === "live" || previousState === "recovery_required")) {
|
|
1883
|
+
resource.bootstrapStarted = false;
|
|
1884
|
+
resource.acknowledgements.clear();
|
|
1885
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
1886
|
+
phase: "buffering"
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
const state = barrier?.acknowledge(ack);
|
|
1890
|
+
if (state === "recovery_required") {
|
|
1891
|
+
this.fail(
|
|
1892
|
+
resource,
|
|
1893
|
+
"recovery_required",
|
|
1894
|
+
"subscription was not recovered"
|
|
1895
|
+
);
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
resource.acknowledgements.set(channel, ack);
|
|
1899
|
+
if (resource.acknowledgements.size === resource.barriers.size && !resource.bootstrapStarted) {
|
|
1900
|
+
resource.bootstrapStarted = true;
|
|
1901
|
+
void this.bootstrap(resource);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
async bootstrap(resource) {
|
|
1905
|
+
if (resource.catchUpInFlight || resource.released) return;
|
|
1906
|
+
resource.catchUpInFlight = true;
|
|
1907
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
1908
|
+
phase: "catching_up"
|
|
1909
|
+
});
|
|
1910
|
+
try {
|
|
1911
|
+
const structure = await this.client.getMarketStructure(
|
|
1912
|
+
resource.input.structurePath,
|
|
1913
|
+
resource.input.structureETag
|
|
1914
|
+
);
|
|
1915
|
+
if (resource.released) return;
|
|
1916
|
+
if (structure.status === 200 || structure.etag !== resource.input.structureETag) {
|
|
1917
|
+
this.fail(
|
|
1918
|
+
resource,
|
|
1919
|
+
"structure_changed",
|
|
1920
|
+
"resource structure changed during recovery"
|
|
1921
|
+
);
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
const keys = realtimeMarketKeys(resource.input);
|
|
1925
|
+
const quoteChunks = chunk(keys, 500);
|
|
1926
|
+
let initialQuotes = resource.initialQuotes;
|
|
1927
|
+
for (const quoteChunk of quoteChunks) {
|
|
1928
|
+
const response = await this.client.getMarketDataQuotes(quoteChunk);
|
|
1929
|
+
if (resource.released) return;
|
|
1930
|
+
const parsed = parseInitialQuotes(
|
|
1931
|
+
response,
|
|
1932
|
+
structuralMarketsForKeys(resource.input, quoteChunk)
|
|
1933
|
+
);
|
|
1934
|
+
if (parsed.kind !== "valid") {
|
|
1935
|
+
throw new Error(
|
|
1936
|
+
parsed.kind === "malformed" ? parsed.reason : "quote catch-up response is missing"
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
initialQuotes = mergeQuoteCatchUp(initialQuotes, parsed.value);
|
|
1940
|
+
}
|
|
1941
|
+
resource.initialQuotes = initialQuotes;
|
|
1942
|
+
let bookError;
|
|
1943
|
+
if (resource.input.watch.orderbook_market) {
|
|
1944
|
+
const key = resource.input.watch.orderbook_market;
|
|
1945
|
+
try {
|
|
1946
|
+
const orderbooks = await this.client.getMarketDataOrderbooks(
|
|
1947
|
+
key.market_slug,
|
|
1948
|
+
key.source
|
|
1949
|
+
);
|
|
1950
|
+
if (resource.released) return;
|
|
1951
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
1952
|
+
orderbooks,
|
|
1953
|
+
bookError: void 0
|
|
1954
|
+
});
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
bookError = error;
|
|
1957
|
+
if (this.handleBookError(resource, error)) return;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
if (resource.released) return;
|
|
1961
|
+
await this.getWatchScheduler().replace(this.combinedWatchRequest());
|
|
1962
|
+
if (resource.released) return;
|
|
1963
|
+
for (const [channel, barrier] of resource.barriers) {
|
|
1964
|
+
const ack = resource.acknowledgements.get(channel);
|
|
1965
|
+
if (!ack) throw new Error(`missing subscription ack for ${channel}`);
|
|
1966
|
+
if (barrier.completeCatchUp({
|
|
1967
|
+
generation: resource.generation,
|
|
1968
|
+
epoch: ack.epoch,
|
|
1969
|
+
offset: ack.offset
|
|
1970
|
+
}) === "recovery_required") {
|
|
1971
|
+
throw new Error(`recovery fence failed for ${channel}`);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
1975
|
+
phase: bookError ? "degraded_book" : "live",
|
|
1976
|
+
initialQuotes: resource.initialQuotes,
|
|
1977
|
+
quoteError: void 0
|
|
1978
|
+
});
|
|
1979
|
+
} catch (error) {
|
|
1980
|
+
const state = this.store.get(resource.input.key);
|
|
1981
|
+
if (state.generation === resource.generation && (state.phase === "terminal" || state.phase === "degraded_book" && resource.watchFailure === "book" || resource.watchFailure === "rate_limit")) {
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
1985
|
+
phase: "degraded_quote",
|
|
1986
|
+
initialQuotes: resource.initialQuotes,
|
|
1987
|
+
quoteError: error
|
|
1988
|
+
});
|
|
1989
|
+
} finally {
|
|
1990
|
+
resource.catchUpInFlight = false;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
restartCatchUp(resource) {
|
|
1994
|
+
if (resource.released || resource.catchUpInFlight) return;
|
|
1995
|
+
resource.bootstrapStarted = true;
|
|
1996
|
+
for (const [channel, barrier] of resource.barriers) {
|
|
1997
|
+
const ack = resource.acknowledgements.get(channel);
|
|
1998
|
+
if (!ack) {
|
|
1999
|
+
resource.bootstrapStarted = false;
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
barrier.acknowledge({ ...ack, recovered: false });
|
|
2003
|
+
}
|
|
2004
|
+
void this.bootstrap(resource);
|
|
2005
|
+
}
|
|
2006
|
+
commitPublication(resource, channel, value) {
|
|
2007
|
+
if (channel === resource.input.bookChannel) {
|
|
2008
|
+
const decoded2 = decodeBookEnvelope(value);
|
|
2009
|
+
if (!decoded2.ok) {
|
|
2010
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
2011
|
+
phase: "degraded_book",
|
|
2012
|
+
bookError: decoded2
|
|
2013
|
+
});
|
|
2014
|
+
} else {
|
|
2015
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
2016
|
+
phase: "live",
|
|
2017
|
+
liveBook: decoded2.value,
|
|
2018
|
+
bookError: void 0
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
const decoded = decodeItemEnvelope(value);
|
|
2024
|
+
if (!decoded.ok) {
|
|
2025
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
2026
|
+
phase: "degraded_quote",
|
|
2027
|
+
quoteError: decoded
|
|
2028
|
+
});
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
if (decoded.value.resource_invalidated) {
|
|
2032
|
+
this.fail(
|
|
2033
|
+
resource,
|
|
2034
|
+
"structure_invalidated",
|
|
2035
|
+
"resource membership was invalidated"
|
|
2036
|
+
);
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
if (decoded.value.changes && resource.initialQuotes) {
|
|
2040
|
+
resource.initialQuotes = mergeItemChanges(
|
|
2041
|
+
resource.initialQuotes,
|
|
2042
|
+
decoded.value.changes
|
|
2043
|
+
);
|
|
2044
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
2045
|
+
initialQuotes: resource.initialQuotes
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
getTransport() {
|
|
2050
|
+
if (!this.transport) this.transport = this.transportFactory();
|
|
2051
|
+
return this.transport;
|
|
2052
|
+
}
|
|
2053
|
+
getWatchScheduler() {
|
|
2054
|
+
if (!this.watchScheduler) {
|
|
2055
|
+
this.watchScheduler = new MarketDataWatchScheduler({
|
|
2056
|
+
touch: (request) => this.client.watchMarketData(request),
|
|
2057
|
+
maxRawQuoteSelectors: 500,
|
|
2058
|
+
onTouchSuccess: () => this.onWatchTouchSuccess(),
|
|
2059
|
+
onTouchError: (error) => this.onWatchTouchError(error)
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
return this.watchScheduler;
|
|
2063
|
+
}
|
|
2064
|
+
onWatchTouchSuccess() {
|
|
2065
|
+
for (const resource of this.resources.values()) {
|
|
2066
|
+
if (resource.released || !resource.watchFailure) continue;
|
|
2067
|
+
const state = this.store.get(resource.input.key);
|
|
2068
|
+
if (state.generation !== resource.generation) continue;
|
|
2069
|
+
const watchError = resource.watchError;
|
|
2070
|
+
const quoteError = state.quoteError === watchError ? void 0 : state.quoteError;
|
|
2071
|
+
const bookError = state.bookError === watchError ? void 0 : state.bookError;
|
|
2072
|
+
resource.watchFailure = void 0;
|
|
2073
|
+
resource.watchError = void 0;
|
|
2074
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
2075
|
+
phase: quoteError ? "degraded_quote" : bookError ? "degraded_book" : "live",
|
|
2076
|
+
quoteError,
|
|
2077
|
+
bookError
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
onWatchTouchError(error) {
|
|
2082
|
+
const status = httpStatus(error);
|
|
2083
|
+
for (const resource of this.resources.values()) {
|
|
2084
|
+
if (resource.released) continue;
|
|
2085
|
+
if (status === 401 || status === 403 || status === 404 || status === 422) {
|
|
2086
|
+
this.fail(
|
|
2087
|
+
resource,
|
|
2088
|
+
status === 404 ? "structure_invalidated" : "watch_terminal",
|
|
2089
|
+
error instanceof Error ? error.message : "Watch request failed"
|
|
2090
|
+
);
|
|
2091
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
2092
|
+
quoteError: error
|
|
2093
|
+
});
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
if (status === 429) {
|
|
2097
|
+
resource.watchFailure = resource.input.watch.orderbook_market ? "book" : "rate_limit";
|
|
2098
|
+
resource.watchError = error;
|
|
2099
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
2100
|
+
phase: resource.input.watch.orderbook_market ? "degraded_book" : "live",
|
|
2101
|
+
initialQuotes: resource.initialQuotes,
|
|
2102
|
+
quoteError: void 0,
|
|
2103
|
+
...resource.input.watch.orderbook_market ? { bookError: error } : {}
|
|
2104
|
+
});
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
resource.watchFailure = "quote";
|
|
2108
|
+
resource.watchError = error;
|
|
2109
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
2110
|
+
phase: "degraded_quote",
|
|
2111
|
+
initialQuotes: resource.initialQuotes,
|
|
2112
|
+
quoteError: error
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
handleBookError(resource, error) {
|
|
2117
|
+
if (resource.released) return false;
|
|
2118
|
+
const status = httpStatus(error);
|
|
2119
|
+
if (status === 401 || status === 403 || status === 404 || status === 422) {
|
|
2120
|
+
this.fail(
|
|
2121
|
+
resource,
|
|
2122
|
+
status === 404 ? "structure_invalidated" : "book_terminal",
|
|
2123
|
+
error instanceof Error ? error.message : "Book request failed"
|
|
2124
|
+
);
|
|
2125
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
2126
|
+
bookError: error
|
|
2127
|
+
});
|
|
2128
|
+
return true;
|
|
2129
|
+
}
|
|
2130
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
2131
|
+
phase: "degraded_book",
|
|
2132
|
+
bookError: error
|
|
2133
|
+
});
|
|
2134
|
+
if (!isRetryableBookError(error)) return false;
|
|
2135
|
+
if (resource.bookRetryTimer !== void 0) {
|
|
2136
|
+
clearTimeout(resource.bookRetryTimer);
|
|
2137
|
+
}
|
|
2138
|
+
const delay = retryAfterMilliseconds2(error) ?? Math.min(3e4, 1e3 * 2 ** Math.min(resource.bookRetryAttempt, 5));
|
|
2139
|
+
resource.bookRetryAttempt++;
|
|
2140
|
+
resource.bookRetryTimer = setTimeout(() => {
|
|
2141
|
+
resource.bookRetryTimer = void 0;
|
|
2142
|
+
void this.retryBookCatchUp(resource);
|
|
2143
|
+
}, delay);
|
|
2144
|
+
return false;
|
|
2145
|
+
}
|
|
2146
|
+
async retryBookCatchUp(resource) {
|
|
2147
|
+
const key = resource.input.watch.orderbook_market;
|
|
2148
|
+
if (resource.released || !key) return;
|
|
2149
|
+
try {
|
|
2150
|
+
const orderbooks = await this.client.getMarketDataOrderbooks(
|
|
2151
|
+
key.market_slug,
|
|
2152
|
+
key.source
|
|
2153
|
+
);
|
|
2154
|
+
resource.bookRetryAttempt = 0;
|
|
2155
|
+
const state = this.store.get(resource.input.key);
|
|
2156
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
2157
|
+
phase: state.quoteError ? "degraded_quote" : "live",
|
|
2158
|
+
orderbooks,
|
|
2159
|
+
bookError: void 0
|
|
2160
|
+
});
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
this.handleBookError(resource, error);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
combinedWatchRequest() {
|
|
2166
|
+
const quoteEvents = /* @__PURE__ */ new Map();
|
|
2167
|
+
const quoteMarkets = /* @__PURE__ */ new Map();
|
|
2168
|
+
const orderbookMarkets = /* @__PURE__ */ new Map();
|
|
2169
|
+
for (const resource of this.resources.values()) {
|
|
2170
|
+
if (resource.released) continue;
|
|
2171
|
+
for (const event of resource.input.watch.quote_events ?? []) {
|
|
2172
|
+
quoteEvents.set(
|
|
2173
|
+
`${event.source}\0${event.event_slug}\0${event.market_view}`,
|
|
2174
|
+
event
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
for (const market of resource.input.watch.quote_markets ?? []) {
|
|
2178
|
+
quoteMarkets.set(`${market.source}\0${market.market_slug}`, market);
|
|
2179
|
+
}
|
|
2180
|
+
const book = resource.input.watch.orderbook_market;
|
|
2181
|
+
if (book) {
|
|
2182
|
+
orderbookMarkets.set(`${book.source}\0${book.market_slug}`, book);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
if (orderbookMarkets.size > 1) {
|
|
2186
|
+
throw new Error("only one active orderbook market is supported");
|
|
2187
|
+
}
|
|
2188
|
+
return {
|
|
2189
|
+
...quoteEvents.size ? { quote_events: [...quoteEvents.values()] } : {},
|
|
2190
|
+
...quoteMarkets.size ? { quote_markets: [...quoteMarkets.values()] } : {},
|
|
2191
|
+
...orderbookMarkets.size ? { orderbook_market: [...orderbookMarkets.values()][0] } : {}
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
fail(resource, code, message) {
|
|
2195
|
+
for (const barrier of resource.barriers.values()) barrier.invalidate();
|
|
2196
|
+
this.store.update(resource.input.key, resource.generation, {
|
|
2197
|
+
phase: "terminal",
|
|
2198
|
+
structureInvalidated: code === "structure_invalidated" || code === "structure_changed",
|
|
2199
|
+
quoteError: { code, message }
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
release(key) {
|
|
2203
|
+
const resource = this.resources.get(key);
|
|
2204
|
+
if (resource) this.releaseGeneration(key, resource.generation);
|
|
2205
|
+
}
|
|
2206
|
+
releaseGeneration(key, generation) {
|
|
2207
|
+
const resource = this.resources.get(key);
|
|
2208
|
+
if (!resource || resource.generation !== generation) return;
|
|
2209
|
+
resource.released = true;
|
|
2210
|
+
if (resource.bookRetryTimer !== void 0) {
|
|
2211
|
+
clearTimeout(resource.bookRetryTimer);
|
|
2212
|
+
resource.bookRetryTimer = void 0;
|
|
2213
|
+
}
|
|
2214
|
+
for (const subscription of resource.subscriptions) {
|
|
2215
|
+
subscription.unsubscribe();
|
|
2216
|
+
}
|
|
2217
|
+
for (const barrier of resource.barriers.values()) barrier.invalidate();
|
|
2218
|
+
this.resources.delete(key);
|
|
2219
|
+
this.store.remove(key, generation);
|
|
2220
|
+
if (!this.disposing) this.refreshWatchDemand();
|
|
2221
|
+
}
|
|
2222
|
+
refreshWatchDemand() {
|
|
2223
|
+
if (!this.watchScheduler) return;
|
|
2224
|
+
const request = this.combinedWatchRequest();
|
|
2225
|
+
if (!(request.quote_events?.length ?? 0) && !(request.quote_markets?.length ?? 0) && !request.orderbook_market) {
|
|
2226
|
+
this.watchScheduler.clear();
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
void this.watchScheduler.replace(request).catch(() => void 0);
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
function realtimeMarketKeys(input) {
|
|
2233
|
+
return input.structure.items.flatMap(
|
|
2234
|
+
(item) => item.markets.filter((market) => market.realtime_supported).map((market) => ({
|
|
2235
|
+
source: market.source,
|
|
2236
|
+
market_slug: market.market_slug
|
|
2237
|
+
}))
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
function structuralMarketsForKeys(input, keys) {
|
|
2241
|
+
return keys.map((key) => {
|
|
2242
|
+
const market = input.structure.items.flatMap((item) => item.markets).find(
|
|
2243
|
+
(candidate) => candidate.source === key.source && candidate.market_slug === key.market_slug
|
|
2244
|
+
);
|
|
2245
|
+
if (!market) throw new Error("quote catch-up key is absent from structure");
|
|
2246
|
+
return {
|
|
2247
|
+
source: market.source,
|
|
2248
|
+
market_slug: market.market_slug,
|
|
2249
|
+
outcomes: market.outcomes.map((outcome) => outcome.key)
|
|
2250
|
+
};
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
function chunk(values, size) {
|
|
2254
|
+
const chunks = [];
|
|
2255
|
+
for (let index = 0; index < values.length; index += size) {
|
|
2256
|
+
chunks.push(values.slice(index, index + size));
|
|
2257
|
+
}
|
|
2258
|
+
return chunks;
|
|
2259
|
+
}
|
|
2260
|
+
function httpStatus(error) {
|
|
2261
|
+
return typeof error === "object" && error !== null && "status" in error && typeof error.status === "number" ? error.status : void 0;
|
|
2262
|
+
}
|
|
2263
|
+
function isRetryableBookError(error) {
|
|
2264
|
+
const status = httpStatus(error);
|
|
2265
|
+
return status === void 0 || status === 408 || status === 409 || status === 429 || status >= 500;
|
|
2266
|
+
}
|
|
2267
|
+
function retryAfterMilliseconds2(error) {
|
|
2268
|
+
if (typeof error !== "object" || error === null || !("retryAfter" in error) || typeof error.retryAfter !== "string") {
|
|
2269
|
+
return void 0;
|
|
2270
|
+
}
|
|
2271
|
+
const seconds = Number(error.retryAfter);
|
|
2272
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : void 0;
|
|
2273
|
+
}
|
|
2274
|
+
function mergeItemChanges(current, changes) {
|
|
2275
|
+
return {
|
|
2276
|
+
...current,
|
|
2277
|
+
markets: current.markets.map((market) => ({
|
|
2278
|
+
...market,
|
|
2279
|
+
outcomes: market.outcomes.map((outcome) => {
|
|
2280
|
+
const change = changes.find(
|
|
2281
|
+
(candidate) => candidate.source === outcome.source && candidate.market_slug === outcome.market_slug && candidate.outcome === outcome.outcome
|
|
2282
|
+
);
|
|
2283
|
+
if (!change) return outcome;
|
|
2284
|
+
return {
|
|
2285
|
+
...outcome,
|
|
2286
|
+
...change.bba ? { bba: change.bba } : {},
|
|
2287
|
+
...change.last_trade ? { last_trade: change.last_trade } : {},
|
|
2288
|
+
...change.tick_size ? { tick_size: change.tick_size } : {}
|
|
2289
|
+
};
|
|
2290
|
+
})
|
|
2291
|
+
}))
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
function mergeQuoteCatchUp(current, catchUp) {
|
|
2295
|
+
if (!current) return catchUp;
|
|
2296
|
+
const identity = (source, slug) => `${source}\0${slug}`;
|
|
2297
|
+
const catchUpByMarket = new Map(
|
|
2298
|
+
catchUp.markets.map((market) => [
|
|
2299
|
+
identity(market.source, market.market_slug),
|
|
2300
|
+
market
|
|
2301
|
+
])
|
|
2302
|
+
);
|
|
2303
|
+
const merged = current.markets.map(
|
|
2304
|
+
(market) => catchUpByMarket.get(identity(market.source, market.market_slug)) ?? market
|
|
2305
|
+
);
|
|
2306
|
+
const existing = new Set(
|
|
2307
|
+
current.markets.map(
|
|
2308
|
+
(market) => identity(market.source, market.market_slug)
|
|
2309
|
+
)
|
|
2310
|
+
);
|
|
2311
|
+
for (const market of catchUp.markets) {
|
|
2312
|
+
if (!existing.has(identity(market.source, market.market_slug))) {
|
|
2313
|
+
merged.push(market);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
return { schema_version: 1, markets: merged };
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// src/market-data/marketDataStore.ts
|
|
2320
|
+
var legacyState = (key) => ({
|
|
2321
|
+
key,
|
|
2322
|
+
generation: 0,
|
|
2323
|
+
phase: "legacy"
|
|
2324
|
+
});
|
|
2325
|
+
var MarketDataStore = class {
|
|
2326
|
+
resources = /* @__PURE__ */ new Map();
|
|
2327
|
+
generations = /* @__PURE__ */ new Map();
|
|
2328
|
+
legacyStates = /* @__PURE__ */ new Map();
|
|
2329
|
+
listeners = /* @__PURE__ */ new Map();
|
|
2330
|
+
get(key) {
|
|
2331
|
+
const existing = this.resources.get(key);
|
|
2332
|
+
if (existing) return existing;
|
|
2333
|
+
let fallback = this.legacyStates.get(key);
|
|
2334
|
+
if (!fallback) {
|
|
2335
|
+
fallback = legacyState(key);
|
|
2336
|
+
this.legacyStates.set(key, fallback);
|
|
2337
|
+
}
|
|
2338
|
+
return fallback;
|
|
2339
|
+
}
|
|
2340
|
+
begin(key, structureETag) {
|
|
2341
|
+
const generation = (this.generations.get(key) ?? 0) + 1;
|
|
2342
|
+
this.generations.set(key, generation);
|
|
2343
|
+
const state = {
|
|
2344
|
+
key,
|
|
2345
|
+
generation,
|
|
2346
|
+
phase: "initializing",
|
|
2347
|
+
structureETag
|
|
2348
|
+
};
|
|
2349
|
+
this.resources.set(key, state);
|
|
2350
|
+
this.emit(key);
|
|
2351
|
+
return state;
|
|
2352
|
+
}
|
|
2353
|
+
update(key, generation, patch) {
|
|
2354
|
+
const current = this.resources.get(key);
|
|
2355
|
+
if (!current || current.generation !== generation) return false;
|
|
2356
|
+
this.resources.set(key, { ...current, ...patch });
|
|
2357
|
+
this.emit(key);
|
|
2358
|
+
return true;
|
|
2359
|
+
}
|
|
2360
|
+
remove(key, generation) {
|
|
2361
|
+
const current = this.resources.get(key);
|
|
2362
|
+
if (!current || current.generation !== generation) return false;
|
|
2363
|
+
this.resources.delete(key);
|
|
2364
|
+
this.emit(key);
|
|
2365
|
+
return true;
|
|
2366
|
+
}
|
|
2367
|
+
subscribe(key, listener) {
|
|
2368
|
+
let listeners = this.listeners.get(key);
|
|
2369
|
+
if (!listeners) {
|
|
2370
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2371
|
+
this.listeners.set(key, listeners);
|
|
2372
|
+
}
|
|
2373
|
+
listeners.add(listener);
|
|
2374
|
+
return () => {
|
|
2375
|
+
listeners?.delete(listener);
|
|
2376
|
+
if (listeners?.size === 0) this.listeners.delete(key);
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
emit(key) {
|
|
2380
|
+
for (const listener of this.listeners.get(key) ?? []) listener();
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
1005
2383
|
var PredictContext = createContext(null);
|
|
1006
2384
|
function PredictProvider({
|
|
1007
2385
|
client,
|
|
@@ -1012,6 +2390,32 @@ function PredictProvider({
|
|
|
1012
2390
|
const value = useMemo(() => ({ client, wsClient: ws }), [client, ws]);
|
|
1013
2391
|
return /* @__PURE__ */ jsx(PredictContext.Provider, { value, children });
|
|
1014
2392
|
}
|
|
2393
|
+
var MarketDataContext = createContext(
|
|
2394
|
+
null
|
|
2395
|
+
);
|
|
2396
|
+
function MarketDataProvider({
|
|
2397
|
+
capability,
|
|
2398
|
+
client,
|
|
2399
|
+
transportFactory,
|
|
2400
|
+
children
|
|
2401
|
+
}) {
|
|
2402
|
+
const store = useMemo(() => new MarketDataStore(), []);
|
|
2403
|
+
const runtime = useMemo(
|
|
2404
|
+
() => new MarketDataRuntime({
|
|
2405
|
+
enabled: capability.enabled,
|
|
2406
|
+
client,
|
|
2407
|
+
store,
|
|
2408
|
+
transportFactory
|
|
2409
|
+
}),
|
|
2410
|
+
[capability.enabled, client, store, transportFactory]
|
|
2411
|
+
);
|
|
2412
|
+
useEffect(() => () => runtime.dispose(), [runtime]);
|
|
2413
|
+
const value = useMemo(
|
|
2414
|
+
() => ({ capability, client, store, runtime }),
|
|
2415
|
+
[capability, client, store, runtime]
|
|
2416
|
+
);
|
|
2417
|
+
return /* @__PURE__ */ jsx(MarketDataContext.Provider, { value, children });
|
|
2418
|
+
}
|
|
1015
2419
|
|
|
1016
2420
|
// src/utils/polymarket-hmac.ts
|
|
1017
2421
|
function encode(str) {
|
|
@@ -1198,6 +2602,47 @@ function usePredictClient() {
|
|
|
1198
2602
|
}
|
|
1199
2603
|
return context.client;
|
|
1200
2604
|
}
|
|
2605
|
+
function useMarketDataCapability() {
|
|
2606
|
+
const context = useContext(MarketDataContext);
|
|
2607
|
+
if (!context) {
|
|
2608
|
+
throw new Error(
|
|
2609
|
+
"useMarketDataCapability must be used within a MarketDataProvider"
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
return context.capability;
|
|
2613
|
+
}
|
|
2614
|
+
function useMarketDataResource(input) {
|
|
2615
|
+
const context = useContext(MarketDataContext);
|
|
2616
|
+
if (!context) {
|
|
2617
|
+
throw new Error(
|
|
2618
|
+
"useMarketDataResource must be used within a MarketDataProvider"
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
const key = typeof input === "string" ? input : input.key;
|
|
2622
|
+
useEffect(() => {
|
|
2623
|
+
if (typeof input === "string" || !context.capability.enabled) return;
|
|
2624
|
+
return context.runtime.mount(input);
|
|
2625
|
+
}, [context.capability.enabled, context.runtime, input]);
|
|
2626
|
+
const subscribe = useCallback(
|
|
2627
|
+
(listener) => context.store.subscribe(key, listener),
|
|
2628
|
+
[context.store, key]
|
|
2629
|
+
);
|
|
2630
|
+
const getSnapshot = useCallback(
|
|
2631
|
+
() => context.store.get(key),
|
|
2632
|
+
[context.store, key]
|
|
2633
|
+
);
|
|
2634
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// src/hooks/predict/useMarketDataOrderbooks.ts
|
|
2638
|
+
function useMarketDataOrderbooks(resourceKey) {
|
|
2639
|
+
const resource = useMarketDataResource(resourceKey);
|
|
2640
|
+
return {
|
|
2641
|
+
snapshot: resource.orderbooks,
|
|
2642
|
+
live: resource.liveBook,
|
|
2643
|
+
error: resource.bookError
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
1201
2646
|
function eventsQueryKey(params) {
|
|
1202
2647
|
return ["predict", "events", params ?? {}];
|
|
1203
2648
|
}
|
|
@@ -1590,6 +3035,7 @@ function useRealtimeOrderbook(params, queryOptions = {}) {
|
|
|
1590
3035
|
const { wsClient } = usePredictWsClient();
|
|
1591
3036
|
const queryClient = useQueryClient();
|
|
1592
3037
|
const outcome = params.outcome ?? "yes";
|
|
3038
|
+
const enabled = queryOptions.enabled !== false && Boolean(params.slug);
|
|
1593
3039
|
const handleUpdate = useCallback(
|
|
1594
3040
|
(msg) => {
|
|
1595
3041
|
if (msg.data.market_slug !== params.slug) return;
|
|
@@ -1611,7 +3057,7 @@ function useRealtimeOrderbook(params, queryOptions = {}) {
|
|
|
1611
3057
|
const subParams = {
|
|
1612
3058
|
wsClient,
|
|
1613
3059
|
slug: params.slug,
|
|
1614
|
-
enabled
|
|
3060
|
+
enabled,
|
|
1615
3061
|
outcome,
|
|
1616
3062
|
onUpdate: handleUpdate
|
|
1617
3063
|
};
|
|
@@ -3674,6 +5120,6 @@ function walkOrderbook({
|
|
|
3674
5120
|
};
|
|
3675
5121
|
}
|
|
3676
5122
|
|
|
3677
|
-
export { CLOB_AUTH_DOMAIN, CLOB_AUTH_TYPES, CTF_EXCHANGE_ADDRESS, CTF_ORDER_TYPES, ChartRange, NEG_RISK_CTF_EXCHANGE_ADDRESS, ORDER_TYPE, POLYGON_CHAIN_ID, PolymarketContext, PolymarketProvider, PredictClient, PredictContext, PredictProvider, PredictWsClient, SIDE, USDC_ADDRESS, availableSharesQueryKey, balanceQueryKey, buildClobAuthMessage, buildClobPayload, buildCtfExchangeDomain, buildOrderMessage, buildPolymarketL2Headers, buildSignedOrder, buildSignedV2OrderPayload, candlesticksQueryKey, createPredictClient, createPredictWsClient, derivePolymarketApiKey, dflowKYCQueryKey, dflowQuoteQueryKey, esportsMatchesQueryKey, esportsPropsQueryKey, esportsTaxonomyQueryKey, eventQueryKey, eventStatsQueryKey, eventsQueryKey, feeRateQueryKey, fetchEsportsMatches, fetchEsportsProps, fetchEsportsTaxonomy, fetchEvent, fetchEvents, fetchEventsPage, fetchMarket, fetchMatchMarketsPage, fetchMatchesPage, fetchPredictSearch, fetchSportsMatchDetail, fetchSportsMatches, fetchSportsProps, fetchSportsRouting, fetchSportsTaxonomy, getPolymarketSharesPrecision, hmacSha256Base64, infiniteCommentsQueryKey, infiniteEventsQueryKey, infiniteOrdersQueryKey, infiniteTradesMultiQueryKey, infiniteTradesQueryKey, marketQueryKey, marketTradesQueryKey, matchMarketsQueryKey, matchQueryKey, matchesQueryKey, normalizePolymarketTickSize, orderQueryKey, orderbookQueryKey, ordersMultiQueryKey, ordersQueryKey, pickBestAsk, pickBestBid, polymarketDepositAddressesQueryKey, polymarketSetupQueryKey, polymarketSupportedAssetsQueryKey, polymarketWithdrawStatusQueryKey, positionValueMultiQueryKey, positionValueQueryKey, positionsMultiQueryKey, positionsQueryKey, predictSearchQueryKey, priceHistoryQueryKey, rebateConfigQueryKey, resolveEventsParams, resolveTagSlug, similarEventsQueryKey, sportsMatchDetailQueryKey, sportsMatchesQueryKey, sportsPropsQueryKey, sportsRoutingQueryKey, sportsTaxonomyQueryKey, tickSizeQueryKey, tradesQueryKey, updatePolymarketBalanceAllowance, useAvailableShares, useBalance, useCancelOrder, useCandlesticks, useCreatePolymarketOrder, useDFlowKYC, useDFlowQuote, useDFlowSubmit, useDeployPolymarketDepositWallet, useEsportsMatches, useEsportsProps, useEsportsTaxonomy, useEvent, useEventStats, useEvents, useFeeRate, useInfiniteComments, useInfiniteEvents, useInfiniteMatchMarkets, useInfiniteMatches, useInfiniteOrders, useInfinitePredictSearch, useInfiniteTrades, useInfiniteTradesMulti, useMarket, useMarketHistory, useMarketTrades, useMatch, useOrder, useOrderbook, useOrderbookSubscription, useOrders, useOrdersMulti, usePolymarket, usePolymarketDeposit, usePolymarketDepositAddresses, usePolymarketSetup, usePolymarketSupportedAssets, usePolymarketWithdraw, usePolymarketWithdrawPrepareMutation, usePolymarketWithdrawQuoteMutation, usePolymarketWithdrawRelayBuildMutation, usePolymarketWithdrawRelaySubmitMutation, usePolymarketWithdrawStatusQuery, usePositionValue, usePositionValueMulti, usePositions, usePositionsMulti, usePredictClient, usePredictSearch, usePredictWsClient, usePriceHistory, usePricesSubscription, useRealtimeOrderbook, useRealtimePrices, useRealtimeTrades, useRebateConfig, useRedeemPosition, useRunPolymarketSetup, useSearchEvents, useSimilarEvents, useSportsMatchDetail, useSportsMatches, useSportsOrderbook, useSportsProps, useSportsRouting, useSportsTaxonomy, useTickSize, useTradeResultConfirmation, useTrades, useTradesSubscription, useWithdrawBuildMutation, useWithdrawStatusQuery, useWithdrawSubmitMutation, walkOrderbook, withdrawStatusQueryKey };
|
|
5123
|
+
export { CLOB_AUTH_DOMAIN, CLOB_AUTH_TYPES, CTF_EXCHANGE_ADDRESS, CTF_ORDER_TYPES, ChartRange, MARKET_DATA_BACKEND_CONTRACT_COMMIT, MARKET_DATA_CONTRACT_MANIFEST_SHA256, MARKET_STRUCTURE_MEDIA_TYPE_V1, MarketDataHttpError, MarketDataProvider, MarketDataRuntime, MarketDataStore, MarketDataWatchScheduler, NEG_RISK_CTF_EXCHANGE_ADDRESS, ORDER_TYPE, POLYGON_CHAIN_ID, PolymarketContext, PolymarketProvider, PredictClient, PredictContext, PredictProvider, PredictWsClient, RecoveryBarrier, SIDE, USDC_ADDRESS, assertMarketDataBBAComponent, assertMarketDataValueComponent, availableSharesQueryKey, balanceQueryKey, buildClobAuthMessage, buildClobPayload, buildCtfExchangeDomain, buildOrderMessage, buildPolymarketL2Headers, buildSignedOrder, buildSignedV2OrderPayload, candlesticksQueryKey, chunkWatchRequest, comparePublicationCursor, createPredictClient, createPredictWsClient, decodeBookEnvelope, decodeItemEnvelope, derivePolymarketApiKey, dflowKYCQueryKey, dflowQuoteQueryKey, esportsMatchesQueryKey, esportsPropsQueryKey, esportsTaxonomyQueryKey, eventQueryKey, eventStatsQueryKey, eventsQueryKey, feeRateQueryKey, fetchEsportsMatches, fetchEsportsProps, fetchEsportsTaxonomy, fetchEvent, fetchEvents, fetchEventsPage, fetchMarket, fetchMatchMarketsPage, fetchMatchesPage, fetchPredictSearch, fetchSportsMatchDetail, fetchSportsMatches, fetchSportsProps, fetchSportsRouting, fetchSportsTaxonomy, getPolymarketSharesPrecision, hmacSha256Base64, infiniteCommentsQueryKey, infiniteEventsQueryKey, infiniteOrdersQueryKey, infiniteTradesMultiQueryKey, infiniteTradesQueryKey, marketQueryKey, marketTradesQueryKey, matchMarketsQueryKey, matchQueryKey, matchesQueryKey, normalizePolymarketTickSize, orderQueryKey, orderbookQueryKey, ordersMultiQueryKey, ordersQueryKey, parseInitialQuotes, pickBestAsk, pickBestBid, polymarketDepositAddressesQueryKey, polymarketSetupQueryKey, polymarketSupportedAssetsQueryKey, polymarketWithdrawStatusQueryKey, positionValueMultiQueryKey, positionValueQueryKey, positionsMultiQueryKey, positionsQueryKey, predictSearchQueryKey, priceHistoryQueryKey, rebateConfigQueryKey, resolveEventsParams, resolveTagSlug, similarEventsQueryKey, sportsMatchDetailQueryKey, sportsMatchesQueryKey, sportsPropsQueryKey, sportsRoutingQueryKey, sportsTaxonomyQueryKey, tickSizeQueryKey, tradesQueryKey, updatePolymarketBalanceAllowance, useAvailableShares, useBalance, useCancelOrder, useCandlesticks, useCreatePolymarketOrder, useDFlowKYC, useDFlowQuote, useDFlowSubmit, useDeployPolymarketDepositWallet, useEsportsMatches, useEsportsProps, useEsportsTaxonomy, useEvent, useEventStats, useEvents, useFeeRate, useInfiniteComments, useInfiniteEvents, useInfiniteMatchMarkets, useInfiniteMatches, useInfiniteOrders, useInfinitePredictSearch, useInfiniteTrades, useInfiniteTradesMulti, useMarket, useMarketDataCapability, useMarketDataOrderbooks, useMarketDataResource, useMarketHistory, useMarketTrades, useMatch, useOrder, useOrderbook, useOrderbookSubscription, useOrders, useOrdersMulti, usePolymarket, usePolymarketDeposit, usePolymarketDepositAddresses, usePolymarketSetup, usePolymarketSupportedAssets, usePolymarketWithdraw, usePolymarketWithdrawPrepareMutation, usePolymarketWithdrawQuoteMutation, usePolymarketWithdrawRelayBuildMutation, usePolymarketWithdrawRelaySubmitMutation, usePolymarketWithdrawStatusQuery, usePositionValue, usePositionValueMulti, usePositions, usePositionsMulti, usePredictClient, usePredictSearch, usePredictWsClient, usePriceHistory, usePricesSubscription, useRealtimeOrderbook, useRealtimePrices, useRealtimeTrades, useRebateConfig, useRedeemPosition, useRunPolymarketSetup, useSearchEvents, useSimilarEvents, useSportsMatchDetail, useSportsMatches, useSportsOrderbook, useSportsProps, useSportsRouting, useSportsTaxonomy, useTickSize, useTradeResultConfirmation, useTrades, useTradesSubscription, useWithdrawBuildMutation, useWithdrawStatusQuery, useWithdrawSubmitMutation, walkOrderbook, withdrawStatusQueryKey };
|
|
3678
5124
|
//# sourceMappingURL=index.mjs.map
|
|
3679
5125
|
//# sourceMappingURL=index.mjs.map
|