@agg-build/sdk 4.32.0 → 4.34.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/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as loadImmutableHistory, c as orderbookCrc32, i as snapshotToOrderbook, l as projectedOrderbookChecksum, n as applyOrderbookDelta, r as computeChecksum, s as readChartHistoryText, t as AggWebSocket } from "./agg-websocket-ClGhhROZ.mjs";
1
+ import { a as loadImmutableHistory, c as orderbookCrc32, i as snapshotToOrderbook, l as projectedOrderbookChecksum, n as applyOrderbookDelta, r as computeChecksum, s as readChartHistoryText, t as AggWebSocket } from "./agg-websocket-C2neMzGT.mjs";
2
2
  import { t as CandleBuilder } from "./candle-builder-BNewIe9K.mjs";
3
3
  //#region ../common/src/enums/venue.ts
4
4
  /** Use these constants instead of hardcoding "kalshi" or "polymarket". */
@@ -781,24 +781,6 @@ const isBalanceRefillWithinDailyCap = (minimumRaw, refillAmountRaw, dailyCapRaw,
781
781
  return BigInt(minimumRaw) <= maximum && BigInt(refillAmountRaw) <= maximum;
782
782
  };
783
783
  //#endregion
784
- //#region src/retry-after.ts
785
- /** Convert explicit retry hints to milliseconds without treating arbitrary strings as dates. */
786
- function parseRetryAfterSeconds(value) {
787
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
788
- const milliseconds = value * 1e3;
789
- return Number.isSafeInteger(milliseconds) ? milliseconds : void 0;
790
- }
791
- function parseRetryAfterHeader(value, now = Date.now()) {
792
- if (!value) return void 0;
793
- const text = value.trim();
794
- if (/^\d+$/.test(text)) return parseRetryAfterSeconds(Number(text));
795
- if (!/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(text)) return void 0;
796
- const timestamp = Date.parse(text);
797
- if (!Number.isFinite(timestamp) || new Date(timestamp).toUTCString() !== text) return void 0;
798
- const milliseconds = timestamp - now;
799
- return Number.isSafeInteger(milliseconds) && milliseconds >= 0 ? milliseconds : void 0;
800
- }
801
- //#endregion
802
784
  //#region src/errors.ts
803
785
  /**
804
786
  * Thrown when the server returns `challenge_required` during auth start.
@@ -845,6 +827,163 @@ function isAggApiError(value) {
845
827
  return value instanceof AggApiError;
846
828
  }
847
829
  //#endregion
830
+ //#region src/venue-event-read-scheduler.ts
831
+ const MAX_TIMER_DELAY_MS = 2147483647;
832
+ const abortError = () => Object.assign(/* @__PURE__ */ new Error("The request was aborted"), { name: "AbortError" });
833
+ /** Instance-local admission for event reads only. Never retries or retains successful data. */
834
+ var VenueEventReadScheduler = class {
835
+ constructor(concurrency = 4, queueLimit = 128) {
836
+ this.concurrency = concurrency;
837
+ this.queueLimit = queueLimit;
838
+ this.jobs = /* @__PURE__ */ new Map();
839
+ this.queue = [];
840
+ this.active = 0;
841
+ this.cooldownUntil = 0;
842
+ }
843
+ read(key, run, signal) {
844
+ if (signal?.aborted) return Promise.reject(abortError());
845
+ let job = this.jobs.get(key);
846
+ if (!job) {
847
+ this.queue = this.queue.filter((entry) => entry.subscribers > 0);
848
+ if (this.queue.length >= this.queueLimit) return Promise.reject(new AggApiError("Too many pending event reads", 429, {
849
+ code: "client_read_queue_full",
850
+ retryable: true,
851
+ retryAfterMs: Math.max(1e3, this.cooldownUntil - Date.now())
852
+ }));
853
+ let resolve;
854
+ let reject;
855
+ job = {
856
+ key,
857
+ run,
858
+ promise: new Promise((res, rej) => {
859
+ resolve = res;
860
+ reject = rej;
861
+ }),
862
+ resolve,
863
+ reject,
864
+ controller: new AbortController(),
865
+ subscribers: 0
866
+ };
867
+ this.jobs.set(key, job);
868
+ this.queue.push(job);
869
+ }
870
+ const entry = job;
871
+ entry.subscribers += 1;
872
+ const result = new Promise((resolve, reject) => {
873
+ let settled = false;
874
+ const finish = (callback) => {
875
+ if (settled) return;
876
+ settled = true;
877
+ signal?.removeEventListener("abort", onAbort);
878
+ entry.subscribers -= 1;
879
+ if (entry.subscribers === 0) {
880
+ if (this.jobs.get(key) === entry) this.jobs.delete(key);
881
+ entry.controller.abort();
882
+ this.pump();
883
+ }
884
+ callback();
885
+ };
886
+ const onAbort = () => finish(() => reject(abortError()));
887
+ signal?.addEventListener("abort", onAbort, { once: true });
888
+ entry.promise.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
889
+ });
890
+ this.pump();
891
+ return result;
892
+ }
893
+ reset() {
894
+ for (const job of this.jobs.values()) {
895
+ job.controller.abort();
896
+ job.reject(abortError());
897
+ }
898
+ this.jobs.clear();
899
+ this.queue = [];
900
+ this.cooldownUntil = 0;
901
+ if (this.timer !== void 0) clearTimeout(this.timer);
902
+ this.timer = void 0;
903
+ }
904
+ pump() {
905
+ if (this.timer !== void 0) clearTimeout(this.timer);
906
+ this.timer = void 0;
907
+ this.queue = this.queue.filter((job) => job.subscribers > 0);
908
+ if (this.queue.length === 0 || this.active >= this.concurrency) return;
909
+ const delay = this.cooldownUntil - Date.now();
910
+ if (delay > 0) {
911
+ this.timer = setTimeout(() => this.pump(), Math.min(delay, MAX_TIMER_DELAY_MS));
912
+ return;
913
+ }
914
+ while (this.active < this.concurrency && this.queue.length > 0) {
915
+ const job = this.queue.shift();
916
+ this.active += 1;
917
+ this.execute(job);
918
+ }
919
+ }
920
+ async execute(job) {
921
+ try {
922
+ job.resolve(await job.run(job.controller.signal));
923
+ } catch (error) {
924
+ if (!job.controller.signal.aborted && isAggApiError(error) && error.status === 429) {
925
+ const hint = error.retryAfterMs;
926
+ const delay = typeof hint === "number" && Number.isSafeInteger(hint) && hint >= 0 ? hint : 1e3;
927
+ this.cooldownUntil = Math.max(this.cooldownUntil, Date.now() + delay + 1 + Math.floor(Math.random() * 250));
928
+ }
929
+ job.reject(error);
930
+ } finally {
931
+ if (this.jobs.get(job.key) === job) this.jobs.delete(job.key);
932
+ this.active -= 1;
933
+ this.pump();
934
+ }
935
+ }
936
+ };
937
+ //#endregion
938
+ //#region src/execution-poll-wakeup.ts
939
+ /** A coalesced socket hint can interrupt one poll delay or precede it. */
940
+ var ExecutionPollWakeup = class {
941
+ constructor() {
942
+ this.pending = false;
943
+ }
944
+ notify() {
945
+ if (this.resume) this.resume();
946
+ else this.pending = true;
947
+ }
948
+ wait(ms) {
949
+ if (this.pending) {
950
+ this.pending = false;
951
+ return Promise.resolve();
952
+ }
953
+ return new Promise((resolve) => {
954
+ const finish = () => {
955
+ clearTimeout(timer);
956
+ this.resume = void 0;
957
+ resolve();
958
+ };
959
+ const timer = setTimeout(finish, ms);
960
+ this.resume = finish;
961
+ });
962
+ }
963
+ dispose() {
964
+ this.resume?.();
965
+ this.pending = false;
966
+ }
967
+ };
968
+ //#endregion
969
+ //#region src/retry-after.ts
970
+ /** Convert explicit retry hints to milliseconds without treating arbitrary strings as dates. */
971
+ function parseRetryAfterSeconds(value) {
972
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
973
+ const milliseconds = value * 1e3;
974
+ return Number.isSafeInteger(milliseconds) ? milliseconds : void 0;
975
+ }
976
+ function parseRetryAfterHeader(value, now = Date.now()) {
977
+ if (!value) return void 0;
978
+ const text = value.trim();
979
+ if (/^\d+$/.test(text)) return parseRetryAfterSeconds(Number(text));
980
+ if (!/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(text)) return void 0;
981
+ const timestamp = Date.parse(text);
982
+ if (!Number.isFinite(timestamp) || new Date(timestamp).toUTCString() !== text) return void 0;
983
+ const milliseconds = timestamp - now;
984
+ return Number.isSafeInteger(milliseconds) && milliseconds >= 0 ? milliseconds : void 0;
985
+ }
986
+ //#endregion
848
987
  //#region src/eip55.ts
849
988
  /**
850
989
  * EIP-55 address checksumming, self-contained.
@@ -1131,6 +1270,227 @@ function withTimeout(promise, ms) {
1131
1270
  });
1132
1271
  }
1133
1272
  //#endregion
1273
+ //#region src/catalog-feed.ts
1274
+ /** Recommended REST/WS overlap: re-read this much before the resume point. */
1275
+ const CATALOG_DEFAULT_OVERLAP_MS = 6e4;
1276
+ /** Live frames held during a catch-up. Past this the buffer is dropped and the catch-up re-reads. */
1277
+ const LIVE_BUFFER_MAX = 1e4;
1278
+ const RETRY_BASE_MS = 1e3;
1279
+ const RETRY_MAX_MS = 3e4;
1280
+ /** Live applies between dedupe-map prunes. */
1281
+ const PRUNE_EVERY = 1e3;
1282
+ /**
1283
+ * `GET /catalog/changes` answers 410 `resync_required` when the position is
1284
+ * older than the 24h retention window: re-list with the list endpoints, then
1285
+ * resume from the time that re-list started.
1286
+ */
1287
+ function isCatalogResyncRequired(err) {
1288
+ return isAggApiError(err) && (err.code === "resync_required" || err.status === 410);
1289
+ }
1290
+ /** See {@link AggClient.watchCatalog}. */
1291
+ function watchCatalog(deps, options) {
1292
+ if (Number.isNaN(toMs(options.since))) throw new Error("watchCatalog: `since` must be a valid date or ISO string");
1293
+ const overlapMs = options.overlapMs ?? 6e4;
1294
+ const filter = {
1295
+ ...options.venues?.length ? { venues: options.venues } : {},
1296
+ ...options.entities?.length ? { entities: options.entities } : {}
1297
+ };
1298
+ let sinceMs = toMs(options.since);
1299
+ let lastChangedAtMs;
1300
+ let lastChangedAt;
1301
+ /**
1302
+ * Dedupe state per `${entity}:${id}`. Pruned below `lastChangedAt - 2 *
1303
+ * overlap`: catch-ups re-read from `lastChangedAt - overlap` and REST
1304
+ * serves each entity's latest state, so nothing older can come back.
1305
+ */
1306
+ const held = /* @__PURE__ */ new Map();
1307
+ /** matchedOnly: ids the caller holds, so a leave or delete still reaches them. Not pruned. */
1308
+ const delivered = /* @__PURE__ */ new Set();
1309
+ let appliedSincePrune = 0;
1310
+ let buffering = true;
1311
+ let buffer = [];
1312
+ let bufferOverflowed = false;
1313
+ let running = false;
1314
+ let rerun = false;
1315
+ let resyncRequired = false;
1316
+ let stopped = false;
1317
+ let retryTimer = null;
1318
+ let wakeRetry = null;
1319
+ const abort = new AbortController();
1320
+ let resolveReady;
1321
+ const ready = new Promise((resolve) => {
1322
+ resolveReady = resolve;
1323
+ });
1324
+ const report = (err) => {
1325
+ if (stopped) return;
1326
+ try {
1327
+ options.onError?.(err);
1328
+ } catch {}
1329
+ };
1330
+ const prune = () => {
1331
+ appliedSincePrune = 0;
1332
+ if (lastChangedAtMs === void 0) return;
1333
+ const floor = lastChangedAtMs - 2 * overlapMs;
1334
+ for (const [key, h] of held) if (h.changedAtMs < floor) held.delete(key);
1335
+ };
1336
+ const apply = (change) => {
1337
+ const key = `${change.entity}:${change.id}`;
1338
+ const changedAtMs = toMs(change.changedAt);
1339
+ if (Number.isNaN(changedAtMs)) return;
1340
+ const prev = held.get(key);
1341
+ if (prev && (change.version < prev.version || change.version === prev.version && changedAtMs <= prev.changedAtMs)) return;
1342
+ held.set(key, {
1343
+ version: change.version,
1344
+ changedAtMs
1345
+ });
1346
+ if (lastChangedAtMs === void 0 || changedAtMs > lastChangedAtMs) {
1347
+ lastChangedAtMs = changedAtMs;
1348
+ lastChangedAt = change.changedAt;
1349
+ }
1350
+ if (++appliedSincePrune >= PRUNE_EVERY) prune();
1351
+ if (options.matchedOnly) {
1352
+ if ((change.event ?? change.market)?.matched === true && change.op !== "deleted") delivered.add(key);
1353
+ else if (delivered.has(key)) delivered.delete(key);
1354
+ else return;
1355
+ }
1356
+ const { type: _type, ...item } = change;
1357
+ try {
1358
+ options.onChange(item);
1359
+ } catch (err) {
1360
+ report(err);
1361
+ }
1362
+ };
1363
+ const onLive = (frame) => {
1364
+ if (stopped) return;
1365
+ if (!buffering) {
1366
+ apply(frame);
1367
+ return;
1368
+ }
1369
+ if (buffer.length >= LIVE_BUFFER_MAX) {
1370
+ buffer = [];
1371
+ bufferOverflowed = true;
1372
+ return;
1373
+ }
1374
+ buffer.push(frame);
1375
+ };
1376
+ const readAll = async () => {
1377
+ const from = (lastChangedAtMs ?? sinceMs) - overlapMs;
1378
+ let cursor;
1379
+ for (;;) {
1380
+ const page = await deps.getChanges({
1381
+ ...cursor ? { cursor } : { since: new Date(from).toISOString() },
1382
+ ...options.pageLimit ? { limit: options.pageLimit } : {},
1383
+ ...filter
1384
+ }, abort.signal);
1385
+ if (stopped) return;
1386
+ for (const change of page.data) apply(change);
1387
+ if (!page.hasMore) return;
1388
+ cursor = page.nextCursor;
1389
+ }
1390
+ };
1391
+ const sleep = (ms) => new Promise((resolve) => {
1392
+ wakeRetry = resolve;
1393
+ retryTimer = setTimeout(() => {
1394
+ retryTimer = null;
1395
+ wakeRetry = null;
1396
+ resolve();
1397
+ }, ms);
1398
+ });
1399
+ const catchUp = async () => {
1400
+ running = true;
1401
+ buffering = true;
1402
+ let attempt = 0;
1403
+ try {
1404
+ do {
1405
+ rerun = false;
1406
+ bufferOverflowed = false;
1407
+ try {
1408
+ await readAll();
1409
+ } catch (err) {
1410
+ if (stopped) return;
1411
+ if (isCatalogResyncRequired(err)) {
1412
+ resyncRequired = true;
1413
+ if (options.onResyncRequired) options.onResyncRequired();
1414
+ else report(err);
1415
+ return;
1416
+ }
1417
+ report(err);
1418
+ const step = Math.min(RETRY_BASE_MS * 2 ** attempt++, RETRY_MAX_MS);
1419
+ await sleep(Math.round(step * (.5 + Math.random() * .5)));
1420
+ if (stopped) return;
1421
+ rerun = true;
1422
+ continue;
1423
+ }
1424
+ if (stopped) return;
1425
+ attempt = 0;
1426
+ const drained = buffer;
1427
+ buffer = [];
1428
+ for (const frame of drained) apply(frame);
1429
+ if (bufferOverflowed) rerun = true;
1430
+ } while (rerun && !stopped && !resyncRequired);
1431
+ if (stopped || resyncRequired) return;
1432
+ prune();
1433
+ buffering = false;
1434
+ options.onSynced?.({ lastChangedAt });
1435
+ resolveReady();
1436
+ } finally {
1437
+ running = false;
1438
+ }
1439
+ };
1440
+ const requestCatchUp = () => {
1441
+ if (stopped || resyncRequired) return;
1442
+ if (running) {
1443
+ rerun = true;
1444
+ return;
1445
+ }
1446
+ catchUp().catch(report);
1447
+ };
1448
+ const owned = !options.socket;
1449
+ let socket = null;
1450
+ let unsubscribe = null;
1451
+ try {
1452
+ socket = options.socket ?? deps.createSocket();
1453
+ unsubscribe = socket.subscribeCatalog(filter, onLive, {
1454
+ onSubscribed: requestCatchUp,
1455
+ onInterrupted: requestCatchUp,
1456
+ onError: report
1457
+ });
1458
+ if (owned) socket.connect();
1459
+ } catch (err) {
1460
+ report(err);
1461
+ }
1462
+ return {
1463
+ stop() {
1464
+ if (stopped) return;
1465
+ stopped = true;
1466
+ abort.abort();
1467
+ if (retryTimer) clearTimeout(retryTimer);
1468
+ retryTimer = null;
1469
+ wakeRetry?.();
1470
+ wakeRetry = null;
1471
+ buffer = [];
1472
+ unsubscribe?.();
1473
+ if (owned) socket?.destroy();
1474
+ },
1475
+ get lastChangedAt() {
1476
+ return lastChangedAt;
1477
+ },
1478
+ resync(since) {
1479
+ if (stopped) return;
1480
+ if (Number.isNaN(toMs(since))) throw new Error("resync: `since` must be a valid date or ISO string");
1481
+ sinceMs = toMs(since);
1482
+ lastChangedAtMs = void 0;
1483
+ lastChangedAt = void 0;
1484
+ resyncRequired = false;
1485
+ requestCatchUp();
1486
+ },
1487
+ ready
1488
+ };
1489
+ }
1490
+ function toMs(value) {
1491
+ return value instanceof Date ? value.getTime() : Date.parse(value);
1492
+ }
1493
+ //#endregion
1134
1494
  //#region src/client.ts
1135
1495
  const COOKIE_REFRESH_DELIVERY = "cookie-refresh";
1136
1496
  const DEFAULT_MIDPOINT_IDS_PER_REQUEST = 75;
@@ -1221,9 +1581,12 @@ const AUTH_PATHS = /* @__PURE__ */ new Set([
1221
1581
  ]);
1222
1582
  var AggClient = class {
1223
1583
  constructor(options) {
1584
+ this.venueEventReads = new VenueEventReadScheduler();
1585
+ this.eventReadSession = 0;
1224
1586
  this.listeners = /* @__PURE__ */ new Set();
1225
1587
  this.webSocketAuthCleanups = /* @__PURE__ */ new Set();
1226
1588
  this.tradeSockets = /* @__PURE__ */ new Set();
1589
+ this.signatureWaitListeners = /* @__PURE__ */ new Set();
1227
1590
  this._authStatus = "unauthenticated";
1228
1591
  this.authChannel = null;
1229
1592
  this.pendingRefresh = null;
@@ -1320,6 +1683,7 @@ var AggClient = class {
1320
1683
  this.authChannel.onmessage = (event) => {
1321
1684
  const { type } = event.data ?? {};
1322
1685
  if (type === "logout") {
1686
+ this.invalidateEventReadSession();
1323
1687
  this.accessToken = void 0;
1324
1688
  this.accessTokenUserId = void 0;
1325
1689
  this.refreshTokenValue = void 0;
@@ -1335,6 +1699,7 @@ var AggClient = class {
1335
1699
  }
1336
1700
  /** Release resources (BroadcastChannel, etc.). Safe to call multiple times. */
1337
1701
  destroy() {
1702
+ this.venueEventReads.reset();
1338
1703
  this.authChannel?.close();
1339
1704
  this.authChannel = null;
1340
1705
  for (const cleanup of this.webSocketAuthCleanups) cleanup();
@@ -1347,15 +1712,17 @@ var AggClient = class {
1347
1712
  * @param init - Fetch init options, plus optional `query` object for URL search params
1348
1713
  */
1349
1714
  async request(path, init, decode) {
1715
+ const requestSession = this.eventReadSession;
1350
1716
  const response = await this.rawFetch(path, init);
1351
- if (response.status === 401 && this.accessToken && !AUTH_PATHS.has(path)) try {
1717
+ if (response.status === 401 && this.accessToken && !AUTH_PATHS.has(path) && this.eventReadSession === requestSession) try {
1352
1718
  await this.refreshWithDedup();
1719
+ if (this.eventReadSession !== requestSession) throw Object.assign(/* @__PURE__ */ new Error("Authentication changed during refresh"), { name: "AbortError" });
1353
1720
  const retryResponse = await this.rawFetch(path, init);
1354
1721
  if (!retryResponse.ok) return this.throwResponseError(retryResponse);
1355
1722
  if (retryResponse.status === 204) return void 0;
1356
1723
  return decode ? decode(retryResponse) : retryResponse.json();
1357
1724
  } catch {
1358
- this.clearAccessToken();
1725
+ if (!init?.signal?.aborted && this.eventReadSession === requestSession) this.clearAccessToken();
1359
1726
  return this.throwResponseError(response);
1360
1727
  }
1361
1728
  if (!response.ok) return this.throwResponseError(response);
@@ -1411,9 +1778,12 @@ var AggClient = class {
1411
1778
  * only one refresh call is made and the rest wait for it.
1412
1779
  */
1413
1780
  refreshWithDedup() {
1414
- if (!this.pendingRefresh) this.pendingRefresh = this.refreshAccessToken().finally(() => {
1415
- this.pendingRefresh = null;
1416
- });
1781
+ if (!this.pendingRefresh) {
1782
+ const pending = this.refreshAccessToken().finally(() => {
1783
+ if (this.pendingRefresh === pending) this.pendingRefresh = null;
1784
+ });
1785
+ this.pendingRefresh = pending;
1786
+ }
1417
1787
  return this.pendingRefresh;
1418
1788
  }
1419
1789
  buildAuthRequestInit(init, options) {
@@ -1444,7 +1814,10 @@ var AggClient = class {
1444
1814
  }
1445
1815
  /** Set the user access token (for future user-tier auth). */
1446
1816
  setAccessToken(token) {
1447
- if (this.accessToken !== token) this.accessTokenUserId = void 0;
1817
+ if (this.accessToken !== token) {
1818
+ this.invalidateEventReadSession();
1819
+ this.accessTokenUserId = void 0;
1820
+ }
1448
1821
  this.accessToken = token;
1449
1822
  this._authStatus = "authenticated";
1450
1823
  this.persistSession();
@@ -1452,8 +1825,12 @@ var AggClient = class {
1452
1825
  }
1453
1826
  /** Set the current auth session tokens and optionally seed the user profile. */
1454
1827
  setSession(session) {
1828
+ this.applySession(session);
1829
+ }
1830
+ applySession(session, tokenRenewal = false) {
1455
1831
  const wasAuthenticated = this.isAuthenticated;
1456
- this.accessTokenUserId = session.user?.id ?? (this.accessToken === session.accessToken ? this.accessTokenUserId : void 0);
1832
+ if (session.user !== void 0 && this.accessTokenUserId !== void 0 && session.user.id !== this.accessTokenUserId || !tokenRenewal && (this.accessToken !== session.accessToken || session.user && session.user.id !== this.accessTokenUserId)) this.invalidateEventReadSession();
1833
+ this.accessTokenUserId = session.user?.id ?? (tokenRenewal || this.accessToken === session.accessToken ? this.accessTokenUserId : void 0);
1457
1834
  this.accessToken = session.accessToken;
1458
1835
  if ("refreshToken" in session) this.refreshTokenValue = session.refreshToken;
1459
1836
  if (session.user !== void 0) this.currentUser = normalizeSessionUser(session.user, this.currentUser);
@@ -1464,6 +1841,7 @@ var AggClient = class {
1464
1841
  }
1465
1842
  /** Clear the user access token, refresh token, and user info. Broadcasts cross-tab logout. */
1466
1843
  clearAccessToken() {
1844
+ this.invalidateEventReadSession();
1467
1845
  this.accessToken = void 0;
1468
1846
  this.accessTokenUserId = void 0;
1469
1847
  this.refreshTokenValue = void 0;
@@ -1473,6 +1851,11 @@ var AggClient = class {
1473
1851
  this.notifyListeners();
1474
1852
  this.broadcastAuth("logout");
1475
1853
  }
1854
+ invalidateEventReadSession() {
1855
+ this.eventReadSession += 1;
1856
+ this.pendingRefresh = null;
1857
+ this.venueEventReads.reset();
1858
+ }
1476
1859
  broadcastAuth(type) {
1477
1860
  try {
1478
1861
  this.authChannel?.postMessage({ type });
@@ -1636,16 +2019,18 @@ var AggClient = class {
1636
2019
  }
1637
2020
  /** Refresh the access token using the stored refresh token. */
1638
2021
  async refreshAccessToken() {
2022
+ const refreshSession = this.eventReadSession;
1639
2023
  if (this.authDelivery !== COOKIE_REFRESH_DELIVERY && !this.refreshTokenValue) throw new Error("No refresh token available");
1640
2024
  const res = await this.request("/auth/token/refresh", this.buildAuthRequestInit({
1641
2025
  method: "POST",
1642
2026
  body: JSON.stringify(this.authDelivery === COOKIE_REFRESH_DELIVERY ? {} : { refreshToken: this.refreshTokenValue })
1643
2027
  }));
1644
- this.setSession({
2028
+ if (this.eventReadSession !== refreshSession) throw Object.assign(/* @__PURE__ */ new Error("Authentication changed during refresh"), { name: "AbortError" });
2029
+ this.applySession({
1645
2030
  accessToken: res.accessToken,
1646
2031
  refreshToken: res.refreshToken,
1647
2032
  user: res.user
1648
- });
2033
+ }, true);
1649
2034
  return res.accessToken;
1650
2035
  }
1651
2036
  /** Get the current user's profile. */
@@ -2017,6 +2402,7 @@ var AggClient = class {
2017
2402
  if (options?.minVolume != null && options.minVolume > 0) query.minVolume = String(options.minVolume);
2018
2403
  if (options?.endDateFrom) query.endDateFrom = options.endDateFrom;
2019
2404
  if (options?.grouped) query.grouped = "true";
2405
+ if (options?.live) query.live = "true";
2020
2406
  return this.request("/venue-events", {
2021
2407
  query: Object.keys(query).length > 0 ? query : void 0,
2022
2408
  signal: options?.signal
@@ -2033,14 +2419,24 @@ var AggClient = class {
2033
2419
  });
2034
2420
  }
2035
2421
  async getVenueEventById(id, options) {
2036
- return this.request(`/venue-events/${encodeURIComponent(id)}`, {
2037
- query: {
2038
- expand: "",
2039
- ...options?.cluster === false ? { cluster: "false" } : {},
2040
- ...options?.minVolume != null && options.minVolume > 0 ? { minVolume: String(options.minVolume) } : {}
2041
- },
2042
- signal: options?.signal
2043
- });
2422
+ const query = {
2423
+ expand: "",
2424
+ ...options?.cluster === false ? { cluster: "false" } : {},
2425
+ ...options?.minVolume != null && options.minVolume > 0 ? { minVolume: String(options.minVolume) } : {}
2426
+ };
2427
+ const session = this.eventReadSession;
2428
+ const key = JSON.stringify([
2429
+ id,
2430
+ query,
2431
+ session
2432
+ ]);
2433
+ return this.venueEventReads.read(key, (signal) => {
2434
+ if (this.eventReadSession !== session) throw Object.assign(/* @__PURE__ */ new Error("Authentication changed before event read"), { name: "AbortError" });
2435
+ return this.request(`/venue-events/${encodeURIComponent(id)}`, {
2436
+ query,
2437
+ signal
2438
+ });
2439
+ }, options?.signal);
2044
2440
  }
2045
2441
  /** Get normalized live score state for sports events. Requires appId or admin auth. */
2046
2442
  async getSportsLive(eventIds, options) {
@@ -2410,27 +2806,77 @@ var AggClient = class {
2410
2806
  });
2411
2807
  }
2412
2808
  /**
2809
+ * Catalog changes (events and markets created, changed or deleted) after
2810
+ * `since`, oldest first, one item per entity with its current state. Page
2811
+ * with `nextCursor` until `hasMore` is false. Positions older than 24h
2812
+ * throw an {@link AggApiError} with status 410 and code `resync_required`
2813
+ * (see `isCatalogResyncRequired`). Most callers want {@link watchCatalog}.
2814
+ */
2815
+ async getCatalogChanges(params, options) {
2816
+ const query = {};
2817
+ if (params.since !== void 0) query.since = params.since instanceof Date ? params.since.toISOString() : params.since;
2818
+ if (params.cursor) query.cursor = params.cursor;
2819
+ if (params.limit != null) query.limit = String(params.limit);
2820
+ if (params.venues?.length) query.venues = params.venues;
2821
+ if (params.entities?.length) query.entities = params.entities;
2822
+ return this.request("/catalog/changes", {
2823
+ query,
2824
+ signal: options?.signal
2825
+ });
2826
+ }
2827
+ /**
2828
+ * Keep a copy of the catalog in sync from `since`: subscribes to the live
2829
+ * `catalog` channel, catches up over `GET /catalog/changes` (from the last
2830
+ * applied `changedAt` minus `overlapMs`) on every (re)subscribe and stream
2831
+ * interruption, and delivers each entity's newer state once to `onChange`.
2832
+ *
2833
+ * Persist `handle.lastChangedAt` and pass it back as `since` to resume
2834
+ * after a restart. When that is older than 24h, `onResyncRequired` fires:
2835
+ * re-list with the list endpoints, then call `handle.resync(startOfReList)`.
2836
+ *
2837
+ * Without `socket`, opens a dedicated app-level connection (requires
2838
+ * `wsUrl`) that `stop()` closes.
2839
+ */
2840
+ watchCatalog(options) {
2841
+ return watchCatalog({
2842
+ getChanges: (params, signal) => this.getCatalogChanges(params, { signal }),
2843
+ createSocket: () => new AggWebSocket(this.webSocketOptions(options.webSocketImpl))
2844
+ }, options);
2845
+ }
2846
+ /**
2847
+ * URL and handshake headers for this client's app. Server-side clients with
2848
+ * an API key authenticate the handshake with it (the gateway then skips the
2849
+ * browser Origin check). Never in a browser: browsers cannot set handshake
2850
+ * headers, and keys must not ship there.
2851
+ */
2852
+ webSocketOptions(webSocketImpl) {
2853
+ if (!this.wsUrl) throw new Error("AggClient WebSockets require wsUrl to be set in client options");
2854
+ const url = new URL(this.wsUrl);
2855
+ if (this._appId) url.searchParams.set("appId", this._appId);
2856
+ const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
2857
+ const headers = this.apiKey && !isBrowser ? { "x-app-api-key": this.apiKey } : void 0;
2858
+ return {
2859
+ url: url.toString(),
2860
+ ...headers ? { headers } : {},
2861
+ ...webSocketImpl ? { webSocketImpl } : {}
2862
+ };
2863
+ }
2864
+ /**
2413
2865
  * Create an AggWebSocket instance configured with this client's appId and current auth session.
2414
2866
  * Session changes update this socket until it or the client is destroyed.
2415
2867
  * Requires `wsUrl` to be set in the client options.
2416
2868
  */
2417
2869
  createWebSocket(callbacks, options) {
2418
2870
  if (!this.wsUrl) throw new Error("AggClient.createWebSocket() requires wsUrl to be set in client options");
2419
- const url = new URL(this.wsUrl);
2420
- if (this._appId) url.searchParams.set("appId", this._appId);
2421
2871
  const createSocket = this.webSocketFactory ?? ((socketOptions) => new AggWebSocket(socketOptions));
2422
2872
  const watchdogHeaders = {};
2423
2873
  if (this._appId) watchdogHeaders["x-app-id"] = this._appId;
2424
2874
  if (this.apiKey) watchdogHeaders["x-app-api-key"] = this.apiKey;
2425
- const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
2426
- const headers = this.apiKey && !isBrowser ? { "x-app-api-key": this.apiKey } : void 0;
2427
2875
  const socket = createSocket({
2428
- url: url.toString(),
2876
+ ...this.webSocketOptions(options?.webSocketImpl),
2429
2877
  callbacks,
2430
2878
  initialToken: this.accessToken,
2431
2879
  enableLogs: options?.enableLogs ?? false,
2432
- ...headers ? { headers } : {},
2433
- ...options?.webSocketImpl ? { webSocketImpl: options.webSocketImpl } : {},
2434
2880
  bookWatchdog: options?.bookWatchdog === false ? void 0 : {
2435
2881
  baseUrl: this.baseUrl,
2436
2882
  headers: watchdogHeaders,
@@ -2491,7 +2937,9 @@ var AggClient = class {
2491
2937
  handlers.onAuthenticated?.(message);
2492
2938
  },
2493
2939
  onOrderEvent: (message) => {
2494
- if (canReceiveUserEvent(message.userId)) handlers.onOrderEvent?.(message);
2940
+ if (!canReceiveUserEvent(message.userId)) return;
2941
+ if (message.event === "step_waiting" && message.waitingFor?.topic === "agg.user.signature") for (const listener of this.signatureWaitListeners) listener(message.orderId);
2942
+ handlers.onOrderEvent?.(message);
2495
2943
  },
2496
2944
  onOrderEventReceipt: (message) => {
2497
2945
  if (canReceiveUserEvent()) handlers.onOrderEventReceipt?.(message);
@@ -2544,7 +2992,7 @@ var AggClient = class {
2544
2992
  userId = nextUserId;
2545
2993
  if (resetConnection) {
2546
2994
  const diagnostics = socket.getDiagnostics();
2547
- const reconnect = connectRequested || socket.connected || !manuallyDisconnected && (diagnostics.orderbookOutcomeIds.length > 0 || diagnostics.tradeOutcomeIds.length > 0 || diagnostics.arbMarketIds.length > 0 || diagnostics.arbFeedSubscribers > 0);
2995
+ const reconnect = connectRequested || socket.connected || !manuallyDisconnected && (diagnostics.orderbookOutcomeIds.length > 0 || diagnostics.tradeOutcomeIds.length > 0 || diagnostics.arbMarketIds.length > 0 || diagnostics.arbFeedSubscribers > 0 || (diagnostics.catalogSubscribers ?? 0) > 0);
2548
2996
  authenticatedUserId = void 0;
2549
2997
  awaitingDisconnect = true;
2550
2998
  this.tradeSocketAuthenticatedUserId = void 0;
@@ -2619,69 +3067,80 @@ var AggClient = class {
2619
3067
  let ordersFilled = false;
2620
3068
  const isPostFillFee = (req) => isPostFillFeeRequest(req, ordersFilled);
2621
3069
  let orderSubmitted = false;
2622
- for (;;) {
2623
- pending = pending.filter((p) => !declinedOptional.has(p.stepId) && (orderSubmitted || !isOptionalFeeRequest(p)));
2624
- if (Date.now() > deadline) throw new Error(`Fill for quote ${quoteId} did not finish within the timeout.`);
2625
- if (pending.length === 0) {
2626
- await new Promise((r) => setTimeout(r, nextPollMs));
2627
- const polled = await this.getExecutionStatus({ quoteId });
2628
- nextPollMs = Number.isFinite(polled.pollAfterMs) ? Math.min(Math.max(polled.pollAfterMs, MIN_POLL_INTERVAL_MS), MAX_POLL_INTERVAL_MS) : SELF_CUSTODY_POLL_INTERVAL_MS;
2629
- if (TERMINAL_EXECUTION_STATES.has(polled.overallState)) {
2630
- if (polled.overallState !== "filled" && polled.overallState !== "partially_filled") throw new Error(`Fill for quote ${quoteId} ended as ${polled.overallState}.`);
2631
- return {
2632
- ...first,
2633
- pendingSignatures: []
2634
- };
3070
+ const wakeup = new ExecutionPollWakeup();
3071
+ const orderIds = new Set(first.orderIds ?? []);
3072
+ const onSignatureWait = (orderId) => {
3073
+ if (orderIds.has(orderId)) wakeup.notify();
3074
+ };
3075
+ this.signatureWaitListeners.add(onSignatureWait);
3076
+ try {
3077
+ for (;;) {
3078
+ pending = pending.filter((p) => !declinedOptional.has(p.stepId) && (orderSubmitted || !isOptionalFeeRequest(p)));
3079
+ if (Date.now() > deadline) throw new Error(`Fill for quote ${quoteId} did not finish within the timeout.`);
3080
+ if (pending.length === 0) {
3081
+ await wakeup.wait(nextPollMs);
3082
+ const polled = await this.getExecutionStatus({ quoteId });
3083
+ nextPollMs = Number.isFinite(polled.pollAfterMs) ? Math.min(Math.max(polled.pollAfterMs, MIN_POLL_INTERVAL_MS), MAX_POLL_INTERVAL_MS) : SELF_CUSTODY_POLL_INTERVAL_MS;
3084
+ if (TERMINAL_EXECUTION_STATES.has(polled.overallState)) {
3085
+ if (polled.overallState !== "filled" && polled.overallState !== "partially_filled") throw new Error(`Fill for quote ${quoteId} ended as ${polled.overallState}.`);
3086
+ return {
3087
+ ...first,
3088
+ pendingSignatures: []
3089
+ };
3090
+ }
3091
+ ordersFilled = areOrdersFilled(polled.orders);
3092
+ pending = polled.pendingSignatures ?? [];
3093
+ continue;
2635
3094
  }
2636
- ordersFilled = areOrdersFilled(polled.orders);
2637
- pending = polled.pendingSignatures ?? [];
2638
- continue;
2639
- }
2640
- const key = pending.map((p) => p.stepId).sort().join(",");
2641
- if (key === lastKey) throw new Error(`Fill for quote ${quoteId} did not advance — still awaiting steps [${key}] after signing them.`);
2642
- lastKey = key;
2643
- const signatures = [];
2644
- const ordered = orderSignatureRound(pending, { orderSubmitted });
2645
- const userSigners = userSignerAddresses(pending, params.signingAddress);
2646
- for (const req of ordered) {
2647
- if (Date.now() > new Date(req.expiresAt).getTime()) {
2648
- if (req.optional) {
3095
+ const key = pending.map((p) => p.stepId).sort().join(",");
3096
+ if (key === lastKey) throw new Error(`Fill for quote ${quoteId} did not advance — still awaiting steps [${key}] after signing them.`);
3097
+ lastKey = key;
3098
+ const signatures = [];
3099
+ const ordered = orderSignatureRound(pending, { orderSubmitted });
3100
+ const userSigners = userSignerAddresses(pending, params.signingAddress);
3101
+ for (const req of ordered) {
3102
+ if (Date.now() > new Date(req.expiresAt).getTime()) {
3103
+ if (req.optional) {
3104
+ declinedOptional.add(req.stepId);
3105
+ continue;
3106
+ }
3107
+ if (isPostFillFee(req)) return {
3108
+ ...first,
3109
+ pendingSignatures: []
3110
+ };
3111
+ throw new Error(`Signature request ${req.stepId} for quote ${quoteId} expired at ${req.expiresAt}; the fill must be re-quoted.`);
3112
+ }
3113
+ let signed;
3114
+ try {
3115
+ signed = await withTimeout(signer(req), signatureTimeoutMs(req, userSigners));
3116
+ } catch (err) {
3117
+ if (isPostFillFee(req)) return {
3118
+ ...first,
3119
+ pendingSignatures: []
3120
+ };
3121
+ if (!req.optional) throw err;
2649
3122
  declinedOptional.add(req.stepId);
2650
3123
  continue;
2651
3124
  }
2652
- if (isPostFillFee(req)) return {
2653
- ...first,
2654
- pendingSignatures: []
2655
- };
2656
- throw new Error(`Signature request ${req.stepId} for quote ${quoteId} expired at ${req.expiresAt}; the fill must be re-quoted.`);
3125
+ signatures.push(typeof signed === "string" ? {
3126
+ stepId: req.stepId,
3127
+ signature: signed
3128
+ } : {
3129
+ stepId: req.stepId,
3130
+ txHash: signed.txHash
3131
+ });
2657
3132
  }
2658
- let signed;
2659
- try {
2660
- signed = await withTimeout(signer(req), signatureTimeoutMs(req, userSigners));
2661
- } catch (err) {
2662
- if (isPostFillFee(req)) return {
2663
- ...first,
2664
- pendingSignatures: []
2665
- };
2666
- if (!req.optional) throw err;
2667
- declinedOptional.add(req.stepId);
3133
+ if (signatures.length === 0) {
3134
+ pending = [];
2668
3135
  continue;
2669
3136
  }
2670
- signatures.push(typeof signed === "string" ? {
2671
- stepId: req.stepId,
2672
- signature: signed
2673
- } : {
2674
- stepId: req.stepId,
2675
- txHash: signed.txHash
2676
- });
2677
- }
2678
- if (signatures.length === 0) {
2679
- pending = [];
2680
- continue;
3137
+ const next = await this.submitFillSignatures(quoteId, signatures);
3138
+ if (signatures.some((s) => isOrderSignatureStep(s.stepId))) orderSubmitted = true;
3139
+ pending = next.pendingSignatures ?? [];
2681
3140
  }
2682
- const next = await this.submitFillSignatures(quoteId, signatures);
2683
- if (signatures.some((s) => isOrderSignatureStep(s.stepId))) orderSubmitted = true;
2684
- pending = next.pendingSignatures ?? [];
3141
+ } finally {
3142
+ this.signatureWaitListeners.delete(onSignatureWait);
3143
+ wakeup.dispose();
2685
3144
  }
2686
3145
  }
2687
3146
  /**
@@ -2765,6 +3224,14 @@ var AggClient = class {
2765
3224
  body: JSON.stringify(params)
2766
3225
  });
2767
3226
  }
3227
+ /** Submit signatures for a parked self-custody withdrawal. Returns whatever is still outstanding. */
3228
+ async submitWithdrawalSignatures(withdrawalId, signatures) {
3229
+ if (!withdrawalId) throw new Error("withdrawalId is required");
3230
+ return this.request(`/execution/withdrawals/${encodeURIComponent(withdrawalId)}/signatures`, {
3231
+ method: "POST",
3232
+ body: JSON.stringify({ signatures })
3233
+ });
3234
+ }
2768
3235
  /**
2769
3236
  * Preview a withdrawal — real fee + receive estimate without creating one.
2770
3237
  * Optional: `withdrawManaged` does NOT require a prior preview, so partners
@@ -3086,6 +3553,6 @@ function createAggClient(options) {
3086
3553
  return new AggClient(options);
3087
3554
  }
3088
3555
  //#endregion
3089
- export { ACTIVE_VENUES, AccountProvider, AccountType, AggApiError, AggClient, AggWebSocket, CONFIRMED_MATCH_STATUSES, CandleBuilder, Chain, DEFAULT_BALANCE_REFILL_DAILY_CAP_RAW, DEFAULT_BALANCE_REFILL_MAX_FEE_RAW, IMAGE_SIZES, INACTIVE_VENUES, ImageSize, LIMIT_PRICE_RAW_SCALE, MarketStatus, MatchStatus, MatchType, OPTIONAL_FEE_SIGNATURE_TIMEOUT_MS, OPTIONAL_SIGNATURE_TIMEOUT_MS, OrderStatus, OrderType, RECURRENCE_CADENCES, RETINA_IMAGE_SIZES, SELF_CUSTODY_FILL_TIMEOUT_MS, SELF_CUSTODY_POLL_INTERVAL_MS, TERMINAL_EXECUTION_STATES, TimeInForce, TradeSide, TurnstileChallengeError, VENUES, VENUE_CHAIN_IDS, Venue, adjustLimitPriceRawToTick, aggregateMidpoint, applyOrderbookDelta, areOrdersFilled, buildVenueUrl, computeBestSplitsByAmount, computeChecksum, createAggClient, enumGuard, formatMarketQuestion, formatOutcomeLabel, formatOutcomeTitle, getBalanceRefillMaximumRaw, getEffectiveDisabledVenues, getWalletAddressFromUserProfile, hasShape, isAggApiError, isBalanceRefillWithinDailyCap, isEmail, isEnum, isFiniteNonNeg, isNonEmptyString, isOptionalFeeRequest, isOrderSignatureStep, isPolymarketOrderSignatureStep, isPostFillFeeRequest, isSameWalletAddress, isVenueActive, isVenueDataVisible, isVenueInactive, loadImmutableHistory, mergeCandles, mergeClosedCandles, normalizeVenueMarketCluster, optimizedImageUrl, orderSignatureRound, orderbookCrc32, parse, parseEmail, parseEmailStrict, prepareHostedVenueFunding, prepareHostedVenueRoute, projectedOrderbookChecksum, retinaImageUrl, safeParse, signatureTimeoutMs, snapshotToOrderbook, sortVenues, toChecksumAddress, userSignerAddresses, withTimeout };
3556
+ export { ACTIVE_VENUES, AccountProvider, AccountType, AggApiError, AggClient, AggWebSocket, CATALOG_DEFAULT_OVERLAP_MS, CONFIRMED_MATCH_STATUSES, CandleBuilder, Chain, DEFAULT_BALANCE_REFILL_DAILY_CAP_RAW, DEFAULT_BALANCE_REFILL_MAX_FEE_RAW, IMAGE_SIZES, INACTIVE_VENUES, ImageSize, LIMIT_PRICE_RAW_SCALE, MarketStatus, MatchStatus, MatchType, OPTIONAL_FEE_SIGNATURE_TIMEOUT_MS, OPTIONAL_SIGNATURE_TIMEOUT_MS, OrderStatus, OrderType, RECURRENCE_CADENCES, RETINA_IMAGE_SIZES, SELF_CUSTODY_FILL_TIMEOUT_MS, SELF_CUSTODY_POLL_INTERVAL_MS, TERMINAL_EXECUTION_STATES, TimeInForce, TradeSide, TurnstileChallengeError, VENUES, VENUE_CHAIN_IDS, Venue, adjustLimitPriceRawToTick, aggregateMidpoint, applyOrderbookDelta, areOrdersFilled, buildVenueUrl, computeBestSplitsByAmount, computeChecksum, createAggClient, enumGuard, formatMarketQuestion, formatOutcomeLabel, formatOutcomeTitle, getBalanceRefillMaximumRaw, getEffectiveDisabledVenues, getWalletAddressFromUserProfile, hasShape, isAggApiError, isBalanceRefillWithinDailyCap, isCatalogResyncRequired, isEmail, isEnum, isFiniteNonNeg, isNonEmptyString, isOptionalFeeRequest, isOrderSignatureStep, isPolymarketOrderSignatureStep, isPostFillFeeRequest, isSameWalletAddress, isVenueActive, isVenueDataVisible, isVenueInactive, loadImmutableHistory, mergeCandles, mergeClosedCandles, normalizeVenueMarketCluster, optimizedImageUrl, orderSignatureRound, orderbookCrc32, parse, parseEmail, parseEmailStrict, prepareHostedVenueFunding, prepareHostedVenueRoute, projectedOrderbookChecksum, retinaImageUrl, safeParse, signatureTimeoutMs, snapshotToOrderbook, sortVenues, toChecksumAddress, userSignerAddresses, withTimeout };
3090
3557
 
3091
3558
  //# sourceMappingURL=index.mjs.map