@agg-build/sdk 4.32.0 → 4.33.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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_agg_websocket = require("./agg-websocket-CVZ7laJ4.js");
2
+ const require_agg_websocket = require("./agg-websocket-MYZiMRXn.js");
3
3
  const require_candle_builder = require("./candle-builder-BULYdxB4.js");
4
4
  //#region ../common/src/enums/venue.ts
5
5
  /** Use these constants instead of hardcoding "kalshi" or "polymarket". */
@@ -782,24 +782,6 @@ const isBalanceRefillWithinDailyCap = (minimumRaw, refillAmountRaw, dailyCapRaw,
782
782
  return BigInt(minimumRaw) <= maximum && BigInt(refillAmountRaw) <= maximum;
783
783
  };
784
784
  //#endregion
785
- //#region src/retry-after.ts
786
- /** Convert explicit retry hints to milliseconds without treating arbitrary strings as dates. */
787
- function parseRetryAfterSeconds(value) {
788
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
789
- const milliseconds = value * 1e3;
790
- return Number.isSafeInteger(milliseconds) ? milliseconds : void 0;
791
- }
792
- function parseRetryAfterHeader(value, now = Date.now()) {
793
- if (!value) return void 0;
794
- const text = value.trim();
795
- if (/^\d+$/.test(text)) return parseRetryAfterSeconds(Number(text));
796
- 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;
797
- const timestamp = Date.parse(text);
798
- if (!Number.isFinite(timestamp) || new Date(timestamp).toUTCString() !== text) return void 0;
799
- const milliseconds = timestamp - now;
800
- return Number.isSafeInteger(milliseconds) && milliseconds >= 0 ? milliseconds : void 0;
801
- }
802
- //#endregion
803
785
  //#region src/errors.ts
804
786
  /**
805
787
  * Thrown when the server returns `challenge_required` during auth start.
@@ -846,6 +828,163 @@ function isAggApiError(value) {
846
828
  return value instanceof AggApiError;
847
829
  }
848
830
  //#endregion
831
+ //#region src/venue-event-read-scheduler.ts
832
+ const MAX_TIMER_DELAY_MS = 2147483647;
833
+ const abortError = () => Object.assign(/* @__PURE__ */ new Error("The request was aborted"), { name: "AbortError" });
834
+ /** Instance-local admission for event reads only. Never retries or retains successful data. */
835
+ var VenueEventReadScheduler = class {
836
+ constructor(concurrency = 4, queueLimit = 128) {
837
+ this.concurrency = concurrency;
838
+ this.queueLimit = queueLimit;
839
+ this.jobs = /* @__PURE__ */ new Map();
840
+ this.queue = [];
841
+ this.active = 0;
842
+ this.cooldownUntil = 0;
843
+ }
844
+ read(key, run, signal) {
845
+ if (signal?.aborted) return Promise.reject(abortError());
846
+ let job = this.jobs.get(key);
847
+ if (!job) {
848
+ this.queue = this.queue.filter((entry) => entry.subscribers > 0);
849
+ if (this.queue.length >= this.queueLimit) return Promise.reject(new AggApiError("Too many pending event reads", 429, {
850
+ code: "client_read_queue_full",
851
+ retryable: true,
852
+ retryAfterMs: Math.max(1e3, this.cooldownUntil - Date.now())
853
+ }));
854
+ let resolve;
855
+ let reject;
856
+ job = {
857
+ key,
858
+ run,
859
+ promise: new Promise((res, rej) => {
860
+ resolve = res;
861
+ reject = rej;
862
+ }),
863
+ resolve,
864
+ reject,
865
+ controller: new AbortController(),
866
+ subscribers: 0
867
+ };
868
+ this.jobs.set(key, job);
869
+ this.queue.push(job);
870
+ }
871
+ const entry = job;
872
+ entry.subscribers += 1;
873
+ const result = new Promise((resolve, reject) => {
874
+ let settled = false;
875
+ const finish = (callback) => {
876
+ if (settled) return;
877
+ settled = true;
878
+ signal?.removeEventListener("abort", onAbort);
879
+ entry.subscribers -= 1;
880
+ if (entry.subscribers === 0) {
881
+ if (this.jobs.get(key) === entry) this.jobs.delete(key);
882
+ entry.controller.abort();
883
+ this.pump();
884
+ }
885
+ callback();
886
+ };
887
+ const onAbort = () => finish(() => reject(abortError()));
888
+ signal?.addEventListener("abort", onAbort, { once: true });
889
+ entry.promise.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
890
+ });
891
+ this.pump();
892
+ return result;
893
+ }
894
+ reset() {
895
+ for (const job of this.jobs.values()) {
896
+ job.controller.abort();
897
+ job.reject(abortError());
898
+ }
899
+ this.jobs.clear();
900
+ this.queue = [];
901
+ this.cooldownUntil = 0;
902
+ if (this.timer !== void 0) clearTimeout(this.timer);
903
+ this.timer = void 0;
904
+ }
905
+ pump() {
906
+ if (this.timer !== void 0) clearTimeout(this.timer);
907
+ this.timer = void 0;
908
+ this.queue = this.queue.filter((job) => job.subscribers > 0);
909
+ if (this.queue.length === 0 || this.active >= this.concurrency) return;
910
+ const delay = this.cooldownUntil - Date.now();
911
+ if (delay > 0) {
912
+ this.timer = setTimeout(() => this.pump(), Math.min(delay, MAX_TIMER_DELAY_MS));
913
+ return;
914
+ }
915
+ while (this.active < this.concurrency && this.queue.length > 0) {
916
+ const job = this.queue.shift();
917
+ this.active += 1;
918
+ this.execute(job);
919
+ }
920
+ }
921
+ async execute(job) {
922
+ try {
923
+ job.resolve(await job.run(job.controller.signal));
924
+ } catch (error) {
925
+ if (!job.controller.signal.aborted && isAggApiError(error) && error.status === 429) {
926
+ const hint = error.retryAfterMs;
927
+ const delay = typeof hint === "number" && Number.isSafeInteger(hint) && hint >= 0 ? hint : 1e3;
928
+ this.cooldownUntil = Math.max(this.cooldownUntil, Date.now() + delay + 1 + Math.floor(Math.random() * 250));
929
+ }
930
+ job.reject(error);
931
+ } finally {
932
+ if (this.jobs.get(job.key) === job) this.jobs.delete(job.key);
933
+ this.active -= 1;
934
+ this.pump();
935
+ }
936
+ }
937
+ };
938
+ //#endregion
939
+ //#region src/execution-poll-wakeup.ts
940
+ /** A coalesced socket hint can interrupt one poll delay or precede it. */
941
+ var ExecutionPollWakeup = class {
942
+ constructor() {
943
+ this.pending = false;
944
+ }
945
+ notify() {
946
+ if (this.resume) this.resume();
947
+ else this.pending = true;
948
+ }
949
+ wait(ms) {
950
+ if (this.pending) {
951
+ this.pending = false;
952
+ return Promise.resolve();
953
+ }
954
+ return new Promise((resolve) => {
955
+ const finish = () => {
956
+ clearTimeout(timer);
957
+ this.resume = void 0;
958
+ resolve();
959
+ };
960
+ const timer = setTimeout(finish, ms);
961
+ this.resume = finish;
962
+ });
963
+ }
964
+ dispose() {
965
+ this.resume?.();
966
+ this.pending = false;
967
+ }
968
+ };
969
+ //#endregion
970
+ //#region src/retry-after.ts
971
+ /** Convert explicit retry hints to milliseconds without treating arbitrary strings as dates. */
972
+ function parseRetryAfterSeconds(value) {
973
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
974
+ const milliseconds = value * 1e3;
975
+ return Number.isSafeInteger(milliseconds) ? milliseconds : void 0;
976
+ }
977
+ function parseRetryAfterHeader(value, now = Date.now()) {
978
+ if (!value) return void 0;
979
+ const text = value.trim();
980
+ if (/^\d+$/.test(text)) return parseRetryAfterSeconds(Number(text));
981
+ 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;
982
+ const timestamp = Date.parse(text);
983
+ if (!Number.isFinite(timestamp) || new Date(timestamp).toUTCString() !== text) return void 0;
984
+ const milliseconds = timestamp - now;
985
+ return Number.isSafeInteger(milliseconds) && milliseconds >= 0 ? milliseconds : void 0;
986
+ }
987
+ //#endregion
849
988
  //#region src/eip55.ts
850
989
  /**
851
990
  * EIP-55 address checksumming, self-contained.
@@ -1132,6 +1271,227 @@ function withTimeout(promise, ms) {
1132
1271
  });
1133
1272
  }
1134
1273
  //#endregion
1274
+ //#region src/catalog-feed.ts
1275
+ /** Recommended REST/WS overlap: re-read this much before the resume point. */
1276
+ const CATALOG_DEFAULT_OVERLAP_MS = 6e4;
1277
+ /** Live frames held during a catch-up. Past this the buffer is dropped and the catch-up re-reads. */
1278
+ const LIVE_BUFFER_MAX = 1e4;
1279
+ const RETRY_BASE_MS = 1e3;
1280
+ const RETRY_MAX_MS = 3e4;
1281
+ /** Live applies between dedupe-map prunes. */
1282
+ const PRUNE_EVERY = 1e3;
1283
+ /**
1284
+ * `GET /catalog/changes` answers 410 `resync_required` when the position is
1285
+ * older than the 24h retention window: re-list with the list endpoints, then
1286
+ * resume from the time that re-list started.
1287
+ */
1288
+ function isCatalogResyncRequired(err) {
1289
+ return isAggApiError(err) && (err.code === "resync_required" || err.status === 410);
1290
+ }
1291
+ /** See {@link AggClient.watchCatalog}. */
1292
+ function watchCatalog(deps, options) {
1293
+ if (Number.isNaN(toMs(options.since))) throw new Error("watchCatalog: `since` must be a valid date or ISO string");
1294
+ const overlapMs = options.overlapMs ?? 6e4;
1295
+ const filter = {
1296
+ ...options.venues?.length ? { venues: options.venues } : {},
1297
+ ...options.entities?.length ? { entities: options.entities } : {}
1298
+ };
1299
+ let sinceMs = toMs(options.since);
1300
+ let lastChangedAtMs;
1301
+ let lastChangedAt;
1302
+ /**
1303
+ * Dedupe state per `${entity}:${id}`. Pruned below `lastChangedAt - 2 *
1304
+ * overlap`: catch-ups re-read from `lastChangedAt - overlap` and REST
1305
+ * serves each entity's latest state, so nothing older can come back.
1306
+ */
1307
+ const held = /* @__PURE__ */ new Map();
1308
+ /** matchedOnly: ids the caller holds, so a leave or delete still reaches them. Not pruned. */
1309
+ const delivered = /* @__PURE__ */ new Set();
1310
+ let appliedSincePrune = 0;
1311
+ let buffering = true;
1312
+ let buffer = [];
1313
+ let bufferOverflowed = false;
1314
+ let running = false;
1315
+ let rerun = false;
1316
+ let resyncRequired = false;
1317
+ let stopped = false;
1318
+ let retryTimer = null;
1319
+ let wakeRetry = null;
1320
+ const abort = new AbortController();
1321
+ let resolveReady;
1322
+ const ready = new Promise((resolve) => {
1323
+ resolveReady = resolve;
1324
+ });
1325
+ const report = (err) => {
1326
+ if (stopped) return;
1327
+ try {
1328
+ options.onError?.(err);
1329
+ } catch {}
1330
+ };
1331
+ const prune = () => {
1332
+ appliedSincePrune = 0;
1333
+ if (lastChangedAtMs === void 0) return;
1334
+ const floor = lastChangedAtMs - 2 * overlapMs;
1335
+ for (const [key, h] of held) if (h.changedAtMs < floor) held.delete(key);
1336
+ };
1337
+ const apply = (change) => {
1338
+ const key = `${change.entity}:${change.id}`;
1339
+ const changedAtMs = toMs(change.changedAt);
1340
+ if (Number.isNaN(changedAtMs)) return;
1341
+ const prev = held.get(key);
1342
+ if (prev && (change.version < prev.version || change.version === prev.version && changedAtMs <= prev.changedAtMs)) return;
1343
+ held.set(key, {
1344
+ version: change.version,
1345
+ changedAtMs
1346
+ });
1347
+ if (lastChangedAtMs === void 0 || changedAtMs > lastChangedAtMs) {
1348
+ lastChangedAtMs = changedAtMs;
1349
+ lastChangedAt = change.changedAt;
1350
+ }
1351
+ if (++appliedSincePrune >= PRUNE_EVERY) prune();
1352
+ if (options.matchedOnly) {
1353
+ if ((change.event ?? change.market)?.matched === true && change.op !== "deleted") delivered.add(key);
1354
+ else if (delivered.has(key)) delivered.delete(key);
1355
+ else return;
1356
+ }
1357
+ const { type: _type, ...item } = change;
1358
+ try {
1359
+ options.onChange(item);
1360
+ } catch (err) {
1361
+ report(err);
1362
+ }
1363
+ };
1364
+ const onLive = (frame) => {
1365
+ if (stopped) return;
1366
+ if (!buffering) {
1367
+ apply(frame);
1368
+ return;
1369
+ }
1370
+ if (buffer.length >= LIVE_BUFFER_MAX) {
1371
+ buffer = [];
1372
+ bufferOverflowed = true;
1373
+ return;
1374
+ }
1375
+ buffer.push(frame);
1376
+ };
1377
+ const readAll = async () => {
1378
+ const from = (lastChangedAtMs ?? sinceMs) - overlapMs;
1379
+ let cursor;
1380
+ for (;;) {
1381
+ const page = await deps.getChanges({
1382
+ ...cursor ? { cursor } : { since: new Date(from).toISOString() },
1383
+ ...options.pageLimit ? { limit: options.pageLimit } : {},
1384
+ ...filter
1385
+ }, abort.signal);
1386
+ if (stopped) return;
1387
+ for (const change of page.data) apply(change);
1388
+ if (!page.hasMore) return;
1389
+ cursor = page.nextCursor;
1390
+ }
1391
+ };
1392
+ const sleep = (ms) => new Promise((resolve) => {
1393
+ wakeRetry = resolve;
1394
+ retryTimer = setTimeout(() => {
1395
+ retryTimer = null;
1396
+ wakeRetry = null;
1397
+ resolve();
1398
+ }, ms);
1399
+ });
1400
+ const catchUp = async () => {
1401
+ running = true;
1402
+ buffering = true;
1403
+ let attempt = 0;
1404
+ try {
1405
+ do {
1406
+ rerun = false;
1407
+ bufferOverflowed = false;
1408
+ try {
1409
+ await readAll();
1410
+ } catch (err) {
1411
+ if (stopped) return;
1412
+ if (isCatalogResyncRequired(err)) {
1413
+ resyncRequired = true;
1414
+ if (options.onResyncRequired) options.onResyncRequired();
1415
+ else report(err);
1416
+ return;
1417
+ }
1418
+ report(err);
1419
+ const step = Math.min(RETRY_BASE_MS * 2 ** attempt++, RETRY_MAX_MS);
1420
+ await sleep(Math.round(step * (.5 + Math.random() * .5)));
1421
+ if (stopped) return;
1422
+ rerun = true;
1423
+ continue;
1424
+ }
1425
+ if (stopped) return;
1426
+ attempt = 0;
1427
+ const drained = buffer;
1428
+ buffer = [];
1429
+ for (const frame of drained) apply(frame);
1430
+ if (bufferOverflowed) rerun = true;
1431
+ } while (rerun && !stopped && !resyncRequired);
1432
+ if (stopped || resyncRequired) return;
1433
+ prune();
1434
+ buffering = false;
1435
+ options.onSynced?.({ lastChangedAt });
1436
+ resolveReady();
1437
+ } finally {
1438
+ running = false;
1439
+ }
1440
+ };
1441
+ const requestCatchUp = () => {
1442
+ if (stopped || resyncRequired) return;
1443
+ if (running) {
1444
+ rerun = true;
1445
+ return;
1446
+ }
1447
+ catchUp().catch(report);
1448
+ };
1449
+ const owned = !options.socket;
1450
+ let socket = null;
1451
+ let unsubscribe = null;
1452
+ try {
1453
+ socket = options.socket ?? deps.createSocket();
1454
+ unsubscribe = socket.subscribeCatalog(filter, onLive, {
1455
+ onSubscribed: requestCatchUp,
1456
+ onInterrupted: requestCatchUp,
1457
+ onError: report
1458
+ });
1459
+ if (owned) socket.connect();
1460
+ } catch (err) {
1461
+ report(err);
1462
+ }
1463
+ return {
1464
+ stop() {
1465
+ if (stopped) return;
1466
+ stopped = true;
1467
+ abort.abort();
1468
+ if (retryTimer) clearTimeout(retryTimer);
1469
+ retryTimer = null;
1470
+ wakeRetry?.();
1471
+ wakeRetry = null;
1472
+ buffer = [];
1473
+ unsubscribe?.();
1474
+ if (owned) socket?.destroy();
1475
+ },
1476
+ get lastChangedAt() {
1477
+ return lastChangedAt;
1478
+ },
1479
+ resync(since) {
1480
+ if (stopped) return;
1481
+ if (Number.isNaN(toMs(since))) throw new Error("resync: `since` must be a valid date or ISO string");
1482
+ sinceMs = toMs(since);
1483
+ lastChangedAtMs = void 0;
1484
+ lastChangedAt = void 0;
1485
+ resyncRequired = false;
1486
+ requestCatchUp();
1487
+ },
1488
+ ready
1489
+ };
1490
+ }
1491
+ function toMs(value) {
1492
+ return value instanceof Date ? value.getTime() : Date.parse(value);
1493
+ }
1494
+ //#endregion
1135
1495
  //#region src/client.ts
1136
1496
  const COOKIE_REFRESH_DELIVERY = "cookie-refresh";
1137
1497
  const DEFAULT_MIDPOINT_IDS_PER_REQUEST = 75;
@@ -1222,9 +1582,12 @@ const AUTH_PATHS = /* @__PURE__ */ new Set([
1222
1582
  ]);
1223
1583
  var AggClient = class {
1224
1584
  constructor(options) {
1585
+ this.venueEventReads = new VenueEventReadScheduler();
1586
+ this.eventReadSession = 0;
1225
1587
  this.listeners = /* @__PURE__ */ new Set();
1226
1588
  this.webSocketAuthCleanups = /* @__PURE__ */ new Set();
1227
1589
  this.tradeSockets = /* @__PURE__ */ new Set();
1590
+ this.signatureWaitListeners = /* @__PURE__ */ new Set();
1228
1591
  this._authStatus = "unauthenticated";
1229
1592
  this.authChannel = null;
1230
1593
  this.pendingRefresh = null;
@@ -1321,6 +1684,7 @@ var AggClient = class {
1321
1684
  this.authChannel.onmessage = (event) => {
1322
1685
  const { type } = event.data ?? {};
1323
1686
  if (type === "logout") {
1687
+ this.invalidateEventReadSession();
1324
1688
  this.accessToken = void 0;
1325
1689
  this.accessTokenUserId = void 0;
1326
1690
  this.refreshTokenValue = void 0;
@@ -1336,6 +1700,7 @@ var AggClient = class {
1336
1700
  }
1337
1701
  /** Release resources (BroadcastChannel, etc.). Safe to call multiple times. */
1338
1702
  destroy() {
1703
+ this.venueEventReads.reset();
1339
1704
  this.authChannel?.close();
1340
1705
  this.authChannel = null;
1341
1706
  for (const cleanup of this.webSocketAuthCleanups) cleanup();
@@ -1348,15 +1713,17 @@ var AggClient = class {
1348
1713
  * @param init - Fetch init options, plus optional `query` object for URL search params
1349
1714
  */
1350
1715
  async request(path, init, decode) {
1716
+ const requestSession = this.eventReadSession;
1351
1717
  const response = await this.rawFetch(path, init);
1352
- if (response.status === 401 && this.accessToken && !AUTH_PATHS.has(path)) try {
1718
+ if (response.status === 401 && this.accessToken && !AUTH_PATHS.has(path) && this.eventReadSession === requestSession) try {
1353
1719
  await this.refreshWithDedup();
1720
+ if (this.eventReadSession !== requestSession) throw Object.assign(/* @__PURE__ */ new Error("Authentication changed during refresh"), { name: "AbortError" });
1354
1721
  const retryResponse = await this.rawFetch(path, init);
1355
1722
  if (!retryResponse.ok) return this.throwResponseError(retryResponse);
1356
1723
  if (retryResponse.status === 204) return void 0;
1357
1724
  return decode ? decode(retryResponse) : retryResponse.json();
1358
1725
  } catch {
1359
- this.clearAccessToken();
1726
+ if (!init?.signal?.aborted && this.eventReadSession === requestSession) this.clearAccessToken();
1360
1727
  return this.throwResponseError(response);
1361
1728
  }
1362
1729
  if (!response.ok) return this.throwResponseError(response);
@@ -1412,9 +1779,12 @@ var AggClient = class {
1412
1779
  * only one refresh call is made and the rest wait for it.
1413
1780
  */
1414
1781
  refreshWithDedup() {
1415
- if (!this.pendingRefresh) this.pendingRefresh = this.refreshAccessToken().finally(() => {
1416
- this.pendingRefresh = null;
1417
- });
1782
+ if (!this.pendingRefresh) {
1783
+ const pending = this.refreshAccessToken().finally(() => {
1784
+ if (this.pendingRefresh === pending) this.pendingRefresh = null;
1785
+ });
1786
+ this.pendingRefresh = pending;
1787
+ }
1418
1788
  return this.pendingRefresh;
1419
1789
  }
1420
1790
  buildAuthRequestInit(init, options) {
@@ -1445,7 +1815,10 @@ var AggClient = class {
1445
1815
  }
1446
1816
  /** Set the user access token (for future user-tier auth). */
1447
1817
  setAccessToken(token) {
1448
- if (this.accessToken !== token) this.accessTokenUserId = void 0;
1818
+ if (this.accessToken !== token) {
1819
+ this.invalidateEventReadSession();
1820
+ this.accessTokenUserId = void 0;
1821
+ }
1449
1822
  this.accessToken = token;
1450
1823
  this._authStatus = "authenticated";
1451
1824
  this.persistSession();
@@ -1453,8 +1826,12 @@ var AggClient = class {
1453
1826
  }
1454
1827
  /** Set the current auth session tokens and optionally seed the user profile. */
1455
1828
  setSession(session) {
1829
+ this.applySession(session);
1830
+ }
1831
+ applySession(session, tokenRenewal = false) {
1456
1832
  const wasAuthenticated = this.isAuthenticated;
1457
- this.accessTokenUserId = session.user?.id ?? (this.accessToken === session.accessToken ? this.accessTokenUserId : void 0);
1833
+ 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();
1834
+ this.accessTokenUserId = session.user?.id ?? (tokenRenewal || this.accessToken === session.accessToken ? this.accessTokenUserId : void 0);
1458
1835
  this.accessToken = session.accessToken;
1459
1836
  if ("refreshToken" in session) this.refreshTokenValue = session.refreshToken;
1460
1837
  if (session.user !== void 0) this.currentUser = normalizeSessionUser(session.user, this.currentUser);
@@ -1465,6 +1842,7 @@ var AggClient = class {
1465
1842
  }
1466
1843
  /** Clear the user access token, refresh token, and user info. Broadcasts cross-tab logout. */
1467
1844
  clearAccessToken() {
1845
+ this.invalidateEventReadSession();
1468
1846
  this.accessToken = void 0;
1469
1847
  this.accessTokenUserId = void 0;
1470
1848
  this.refreshTokenValue = void 0;
@@ -1474,6 +1852,11 @@ var AggClient = class {
1474
1852
  this.notifyListeners();
1475
1853
  this.broadcastAuth("logout");
1476
1854
  }
1855
+ invalidateEventReadSession() {
1856
+ this.eventReadSession += 1;
1857
+ this.pendingRefresh = null;
1858
+ this.venueEventReads.reset();
1859
+ }
1477
1860
  broadcastAuth(type) {
1478
1861
  try {
1479
1862
  this.authChannel?.postMessage({ type });
@@ -1637,16 +2020,18 @@ var AggClient = class {
1637
2020
  }
1638
2021
  /** Refresh the access token using the stored refresh token. */
1639
2022
  async refreshAccessToken() {
2023
+ const refreshSession = this.eventReadSession;
1640
2024
  if (this.authDelivery !== COOKIE_REFRESH_DELIVERY && !this.refreshTokenValue) throw new Error("No refresh token available");
1641
2025
  const res = await this.request("/auth/token/refresh", this.buildAuthRequestInit({
1642
2026
  method: "POST",
1643
2027
  body: JSON.stringify(this.authDelivery === COOKIE_REFRESH_DELIVERY ? {} : { refreshToken: this.refreshTokenValue })
1644
2028
  }));
1645
- this.setSession({
2029
+ if (this.eventReadSession !== refreshSession) throw Object.assign(/* @__PURE__ */ new Error("Authentication changed during refresh"), { name: "AbortError" });
2030
+ this.applySession({
1646
2031
  accessToken: res.accessToken,
1647
2032
  refreshToken: res.refreshToken,
1648
2033
  user: res.user
1649
- });
2034
+ }, true);
1650
2035
  return res.accessToken;
1651
2036
  }
1652
2037
  /** Get the current user's profile. */
@@ -2034,14 +2419,24 @@ var AggClient = class {
2034
2419
  });
2035
2420
  }
2036
2421
  async getVenueEventById(id, options) {
2037
- return this.request(`/venue-events/${encodeURIComponent(id)}`, {
2038
- query: {
2039
- expand: "",
2040
- ...options?.cluster === false ? { cluster: "false" } : {},
2041
- ...options?.minVolume != null && options.minVolume > 0 ? { minVolume: String(options.minVolume) } : {}
2042
- },
2043
- signal: options?.signal
2044
- });
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);
2045
2440
  }
2046
2441
  /** Get normalized live score state for sports events. Requires appId or admin auth. */
2047
2442
  async getSportsLive(eventIds, options) {
@@ -2411,27 +2806,77 @@ var AggClient = class {
2411
2806
  });
2412
2807
  }
2413
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 require_agg_websocket.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
+ /**
2414
2865
  * Create an AggWebSocket instance configured with this client's appId and current auth session.
2415
2866
  * Session changes update this socket until it or the client is destroyed.
2416
2867
  * Requires `wsUrl` to be set in the client options.
2417
2868
  */
2418
2869
  createWebSocket(callbacks, options) {
2419
2870
  if (!this.wsUrl) throw new Error("AggClient.createWebSocket() requires wsUrl to be set in client options");
2420
- const url = new URL(this.wsUrl);
2421
- if (this._appId) url.searchParams.set("appId", this._appId);
2422
2871
  const createSocket = this.webSocketFactory ?? ((socketOptions) => new require_agg_websocket.AggWebSocket(socketOptions));
2423
2872
  const watchdogHeaders = {};
2424
2873
  if (this._appId) watchdogHeaders["x-app-id"] = this._appId;
2425
2874
  if (this.apiKey) watchdogHeaders["x-app-api-key"] = this.apiKey;
2426
- const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
2427
- const headers = this.apiKey && !isBrowser ? { "x-app-api-key": this.apiKey } : void 0;
2428
2875
  const socket = createSocket({
2429
- url: url.toString(),
2876
+ ...this.webSocketOptions(options?.webSocketImpl),
2430
2877
  callbacks,
2431
2878
  initialToken: this.accessToken,
2432
2879
  enableLogs: options?.enableLogs ?? false,
2433
- ...headers ? { headers } : {},
2434
- ...options?.webSocketImpl ? { webSocketImpl: options.webSocketImpl } : {},
2435
2880
  bookWatchdog: options?.bookWatchdog === false ? void 0 : {
2436
2881
  baseUrl: this.baseUrl,
2437
2882
  headers: watchdogHeaders,
@@ -2492,7 +2937,9 @@ var AggClient = class {
2492
2937
  handlers.onAuthenticated?.(message);
2493
2938
  },
2494
2939
  onOrderEvent: (message) => {
2495
- 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);
2496
2943
  },
2497
2944
  onOrderEventReceipt: (message) => {
2498
2945
  if (canReceiveUserEvent()) handlers.onOrderEventReceipt?.(message);
@@ -2545,7 +2992,7 @@ var AggClient = class {
2545
2992
  userId = nextUserId;
2546
2993
  if (resetConnection) {
2547
2994
  const diagnostics = socket.getDiagnostics();
2548
- 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);
2549
2996
  authenticatedUserId = void 0;
2550
2997
  awaitingDisconnect = true;
2551
2998
  this.tradeSocketAuthenticatedUserId = void 0;
@@ -2620,69 +3067,80 @@ var AggClient = class {
2620
3067
  let ordersFilled = false;
2621
3068
  const isPostFillFee = (req) => isPostFillFeeRequest(req, ordersFilled);
2622
3069
  let orderSubmitted = false;
2623
- for (;;) {
2624
- pending = pending.filter((p) => !declinedOptional.has(p.stepId) && (orderSubmitted || !isOptionalFeeRequest(p)));
2625
- if (Date.now() > deadline) throw new Error(`Fill for quote ${quoteId} did not finish within the timeout.`);
2626
- if (pending.length === 0) {
2627
- await new Promise((r) => setTimeout(r, nextPollMs));
2628
- const polled = await this.getExecutionStatus({ quoteId });
2629
- nextPollMs = Number.isFinite(polled.pollAfterMs) ? Math.min(Math.max(polled.pollAfterMs, MIN_POLL_INTERVAL_MS), MAX_POLL_INTERVAL_MS) : SELF_CUSTODY_POLL_INTERVAL_MS;
2630
- if (TERMINAL_EXECUTION_STATES.has(polled.overallState)) {
2631
- if (polled.overallState !== "filled" && polled.overallState !== "partially_filled") throw new Error(`Fill for quote ${quoteId} ended as ${polled.overallState}.`);
2632
- return {
2633
- ...first,
2634
- pendingSignatures: []
2635
- };
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;
2636
3094
  }
2637
- ordersFilled = areOrdersFilled(polled.orders);
2638
- pending = polled.pendingSignatures ?? [];
2639
- continue;
2640
- }
2641
- const key = pending.map((p) => p.stepId).sort().join(",");
2642
- if (key === lastKey) throw new Error(`Fill for quote ${quoteId} did not advance — still awaiting steps [${key}] after signing them.`);
2643
- lastKey = key;
2644
- const signatures = [];
2645
- const ordered = orderSignatureRound(pending, { orderSubmitted });
2646
- const userSigners = userSignerAddresses(pending, params.signingAddress);
2647
- for (const req of ordered) {
2648
- if (Date.now() > new Date(req.expiresAt).getTime()) {
2649
- 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;
2650
3122
  declinedOptional.add(req.stepId);
2651
3123
  continue;
2652
3124
  }
2653
- if (isPostFillFee(req)) return {
2654
- ...first,
2655
- pendingSignatures: []
2656
- };
2657
- 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
+ });
2658
3132
  }
2659
- let signed;
2660
- try {
2661
- signed = await withTimeout(signer(req), signatureTimeoutMs(req, userSigners));
2662
- } catch (err) {
2663
- if (isPostFillFee(req)) return {
2664
- ...first,
2665
- pendingSignatures: []
2666
- };
2667
- if (!req.optional) throw err;
2668
- declinedOptional.add(req.stepId);
3133
+ if (signatures.length === 0) {
3134
+ pending = [];
2669
3135
  continue;
2670
3136
  }
2671
- signatures.push(typeof signed === "string" ? {
2672
- stepId: req.stepId,
2673
- signature: signed
2674
- } : {
2675
- stepId: req.stepId,
2676
- txHash: signed.txHash
2677
- });
2678
- }
2679
- if (signatures.length === 0) {
2680
- pending = [];
2681
- continue;
3137
+ const next = await this.submitFillSignatures(quoteId, signatures);
3138
+ if (signatures.some((s) => isOrderSignatureStep(s.stepId))) orderSubmitted = true;
3139
+ pending = next.pendingSignatures ?? [];
2682
3140
  }
2683
- const next = await this.submitFillSignatures(quoteId, signatures);
2684
- if (signatures.some((s) => isOrderSignatureStep(s.stepId))) orderSubmitted = true;
2685
- pending = next.pendingSignatures ?? [];
3141
+ } finally {
3142
+ this.signatureWaitListeners.delete(onSignatureWait);
3143
+ wakeup.dispose();
2686
3144
  }
2687
3145
  }
2688
3146
  /**
@@ -2766,6 +3224,14 @@ var AggClient = class {
2766
3224
  body: JSON.stringify(params)
2767
3225
  });
2768
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
+ }
2769
3235
  /**
2770
3236
  * Preview a withdrawal — real fee + receive estimate without creating one.
2771
3237
  * Optional: `withdrawManaged` does NOT require a prior preview, so partners
@@ -3093,6 +3559,7 @@ exports.AccountType = AccountType;
3093
3559
  exports.AggApiError = AggApiError;
3094
3560
  exports.AggClient = AggClient;
3095
3561
  exports.AggWebSocket = require_agg_websocket.AggWebSocket;
3562
+ exports.CATALOG_DEFAULT_OVERLAP_MS = CATALOG_DEFAULT_OVERLAP_MS;
3096
3563
  exports.CONFIRMED_MATCH_STATUSES = CONFIRMED_MATCH_STATUSES;
3097
3564
  exports.CandleBuilder = require_candle_builder.CandleBuilder;
3098
3565
  exports.Chain = Chain;
@@ -3138,6 +3605,7 @@ exports.getWalletAddressFromUserProfile = getWalletAddressFromUserProfile;
3138
3605
  exports.hasShape = hasShape;
3139
3606
  exports.isAggApiError = isAggApiError;
3140
3607
  exports.isBalanceRefillWithinDailyCap = isBalanceRefillWithinDailyCap;
3608
+ exports.isCatalogResyncRequired = isCatalogResyncRequired;
3141
3609
  exports.isEmail = isEmail;
3142
3610
  exports.isEnum = isEnum;
3143
3611
  exports.isFiniteNonNeg = isFiniteNonNeg;