@taphubhq/sdk-core 0.25.6 → 0.26.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.cjs CHANGED
@@ -35,10 +35,14 @@ __export(index_exports, {
35
35
  BidModule: () => BidModule,
36
36
  CANDLE_EVENT: () => CANDLE_EVENT,
37
37
  DEFAULT_CHART_HISTORY_LIMIT: () => DEFAULT_CHART_HISTORY_LIMIT,
38
+ DEFAULT_PROBE_TTL_SECONDS: () => DEFAULT_PROBE_TTL_SECONDS,
39
+ DEFAULT_REGION: () => DEFAULT_REGION,
40
+ KNOWN_REGIONS: () => KNOWN_REGIONS,
38
41
  LeaderboardModule: () => LeaderboardModule,
39
42
  LocaleModule: () => LocaleModule,
40
43
  NetworkQualityMonitor: () => NetworkQualityMonitor,
41
44
  PairModule: () => PairModule,
45
+ REGION_PROBE_CACHE_KEY: () => REGION_PROBE_CACHE_KEY,
42
46
  RealtimeModule: () => RealtimeModule,
43
47
  TaphubAuthError: () => TaphubAuthError,
44
48
  TaphubClient: () => TaphubClient,
@@ -59,6 +63,7 @@ __export(index_exports, {
59
63
  computeBaseline: () => computeBaseline,
60
64
  errorFunction: () => errorFunction,
61
65
  isCancelled: () => isCancelled,
66
+ isKnownRegion: () => isKnownRegion,
62
67
  isLoss: () => isLoss,
63
68
  isPending: () => isPending,
64
69
  isTerminal: () => isTerminal,
@@ -67,6 +72,8 @@ __export(index_exports, {
67
72
  normalPDF: () => normalPDF,
68
73
  normaliseLang: () => normaliseLang,
69
74
  pairIdFromBidResultTopic: () => pairIdFromBidResultTopic,
75
+ probeNearestRegion: () => probeNearestRegion,
76
+ resolveRegionBaseUrl: () => resolveRegionBaseUrl,
70
77
  roundCoefToSignificantDigits: () => roundCoefToSignificantDigits
71
78
  });
72
79
  module.exports = __toCommonJS(index_exports);
@@ -224,6 +231,36 @@ var AgencyPairModule = class {
224
231
  }
225
232
  };
226
233
 
234
+ // src/transport/graphql/wrongRegionRetry.ts
235
+ var WRONG_REGION_CODE = "Auth_WrongRegion";
236
+ function wrongRegionTarget(err) {
237
+ if (!(err instanceof TaphubError) || err.code !== WRONG_REGION_CODE) return null;
238
+ const details = err.details;
239
+ const meta = details?.errors?.[0]?.extensions?.meta;
240
+ const homeRegion = meta?.homeRegion;
241
+ return typeof homeRegion === "string" && homeRegion !== "" ? homeRegion : null;
242
+ }
243
+ function withWrongRegionRetry(transport, setRegion) {
244
+ async function retryOnce(call) {
245
+ try {
246
+ return await call();
247
+ } catch (err) {
248
+ const homeRegion = wrongRegionTarget(err);
249
+ if (homeRegion === null) throw err;
250
+ setRegion(homeRegion);
251
+ return await call();
252
+ }
253
+ }
254
+ return {
255
+ request(query, variables, opts) {
256
+ return retryOnce(() => transport.request(query, variables, opts));
257
+ },
258
+ publicRequest(query, variables, opts) {
259
+ return retryOnce(() => transport.publicRequest(query, variables, opts));
260
+ }
261
+ };
262
+ }
263
+
227
264
  // src/modules/auth/normalise.ts
228
265
  function normaliseGoogleResponse(body) {
229
266
  return {
@@ -295,6 +332,7 @@ function normaliseUserLoginResponse(body) {
295
332
  throw new TaphubServerError("Invalid response from server", { code: "InvalidResponse" });
296
333
  }
297
334
  const wallet = node.user.defaultWallet;
335
+ const homeRegion = node.homeRegion ?? void 0;
298
336
  return {
299
337
  accessToken: node.accessToken,
300
338
  user: {
@@ -309,9 +347,17 @@ function normaliseUserLoginResponse(body) {
309
347
  isEnabled: wallet.isEnable
310
348
  }
311
349
  },
312
- isDemo: false
350
+ isDemo: false,
351
+ homeRegion
313
352
  };
314
353
  }
354
+ function normaliseRefreshResponse(body) {
355
+ const node = body.refreshTapHubToken;
356
+ if (!node || typeof node.accessToken !== "string" || node.accessToken === "" || typeof node.refreshToken !== "string" || node.refreshToken === "") {
357
+ throw new TaphubServerError("Invalid response from server", { code: "InvalidResponse" });
358
+ }
359
+ return { taphubToken: node.accessToken, refreshToken: node.refreshToken };
360
+ }
315
361
  function validateLoginBody(body) {
316
362
  if (typeof body.access_token !== "string" || body.access_token === "") {
317
363
  throw new TaphubServerError("Invalid response from server", {
@@ -340,10 +386,21 @@ var CREATE_DEMO_USER_MUTATION = `
340
386
  }
341
387
  }
342
388
  `;
389
+ var REFRESH_TAPHUB_TOKEN_MUTATION = `
390
+ mutation refreshTapHubToken($refreshToken: String!, $clientMeta: JSON) {
391
+ refreshTapHubToken(refreshToken: $refreshToken, clientMeta: $clientMeta) {
392
+ accessToken
393
+ refreshToken
394
+ }
395
+ }
396
+ `;
343
397
  var USER_LOGIN_MUTATION = `
344
398
  mutation UserLogin($input: LoginInput!) {
345
399
  userLogin(input: $input) {
346
400
  accessToken
401
+ # multi-geo (infra-260715): home region sourced from the global region-directory
402
+ # (first join stores the supplied region; the stored region wins thereafter).
403
+ homeRegion
347
404
  user {
348
405
  id
349
406
  agencyUid
@@ -365,16 +422,32 @@ var AuthModule = class {
365
422
  #rest;
366
423
  #graphql;
367
424
  #graphqlUser;
425
+ #graphqlUserData;
368
426
  #setToken;
427
+ #isDemo;
369
428
  #agencyId;
429
+ #getRegion;
430
+ #setRegion;
370
431
  #onLoginSuccess;
371
432
  #onLogout;
433
+ /**
434
+ * Single-flight cache for {@link refreshTapHubToken}, keyed by the region the
435
+ * in-flight refresh was started under. Concurrent callers in the SAME region
436
+ * share the one network round-trip; a region change mid-flight (getRegion now
437
+ * differs) means the next caller starts a FRESH refresh targeting the new
438
+ * region rather than reusing a promise bound to the old cluster (task 2.3).
439
+ */
440
+ #refreshInflight = null;
372
441
  constructor(deps) {
373
442
  this.#rest = deps.rest;
374
443
  this.#graphql = deps.graphql;
375
444
  this.#graphqlUser = deps.graphqlUser;
445
+ this.#graphqlUserData = deps.graphqlUserData ?? deps.graphqlUser;
376
446
  this.#setToken = deps.setToken;
447
+ this.#isDemo = deps.isDemo ?? (() => false);
377
448
  this.#agencyId = deps.agencyId;
449
+ this.#getRegion = deps.getRegion;
450
+ this.#setRegion = deps.setRegion;
378
451
  this.#onLoginSuccess = deps.onLoginSuccess;
379
452
  this.#onLogout = deps.onLogout;
380
453
  }
@@ -429,14 +502,35 @@ var AuthModule = class {
429
502
  return result;
430
503
  }
431
504
  async loginWithSession(sessionToken, opts) {
432
- const variables = { input: { sessionToken, agencyId: this.#agencyId } };
505
+ const currentRegion = opts?.region ?? this.#getRegion?.() ?? void 0;
506
+ try {
507
+ return await this.#sendLogin(sessionToken, currentRegion, opts);
508
+ } catch (err) {
509
+ const homeRegion = wrongRegionTarget(err);
510
+ if (homeRegion === null || !this.#setRegion) throw err;
511
+ this.#setRegion(homeRegion);
512
+ return await this.#sendLogin(sessionToken, homeRegion, opts);
513
+ }
514
+ }
515
+ /**
516
+ * Single userLogin attempt against the (region-resolved) user-service endpoint.
517
+ * On success it stamps the token + home region and fires onLoginSuccess. Split
518
+ * out so {@link loginWithSession} can re-invoke it once after an
519
+ * Auth_WrongRegion region switch without duplicating the post-login wiring.
520
+ */
521
+ async #sendLogin(sessionToken, region, opts) {
522
+ const input = { sessionToken, agencyId: this.#agencyId };
523
+ if (region !== void 0 && region !== null) {
524
+ input.region = region;
525
+ }
526
+ const variables = { input };
433
527
  const body = await this.#graphqlUser.request(
434
528
  USER_LOGIN_MUTATION,
435
529
  variables,
436
530
  opts
437
531
  );
438
532
  const result = normaliseUserLoginResponse(body);
439
- this.#setToken(result.accessToken, { isDemo: false });
533
+ this.#setToken(result.accessToken, { isDemo: false, homeRegion: result.homeRegion });
440
534
  if (this.#onLoginSuccess) {
441
535
  try {
442
536
  await this.#onLoginSuccess();
@@ -445,6 +539,60 @@ var AuthModule = class {
445
539
  }
446
540
  return result;
447
541
  }
542
+ /**
543
+ * Exchange a refresh token for a rotated (taphubToken, refreshToken) pair via
544
+ * the user-service (region-routed through `graphqlUserData`, so a user pinned
545
+ * to a non-default region refreshes against THEIR cluster — the bug this change
546
+ * fixes). On success the SDK's own token is updated and the rotated pair is
547
+ * returned so the caller can persist it (rotation contract: present the NEW
548
+ * refresh token next time).
549
+ *
550
+ * - **Demo-skips-refresh:** demo users have no refresh path and reject with
551
+ * `Refresh_DemoNotSupported` (no network call). The caller reacts by clearing
552
+ * auth / re-creating the demo user.
553
+ * - **Single-flight:** concurrent callers share one in-flight request, keyed by
554
+ * the current region. A region change mid-flight re-targets the next caller.
555
+ * - **Wrong-region backstop:** `graphqlUserData` retries once on
556
+ * `Auth_WrongRegion`, so a stale region self-corrects.
557
+ *
558
+ * Invalid/expired/reused refresh tokens surface as the transport's typed error
559
+ * (e.g. `ErrRefreshTokenInvalid`); network failures surface as
560
+ * `TaphubNetworkError`. The caller decides how to react (clear vs retry-later).
561
+ */
562
+ refreshTapHubToken(refreshToken, opts) {
563
+ if (this.#isDemo()) {
564
+ return Promise.reject(
565
+ new TaphubValidationError("demo users have no refresh path", {
566
+ code: "Refresh_DemoNotSupported"
567
+ })
568
+ );
569
+ }
570
+ const region = this.#getRegion?.() ?? null;
571
+ if (this.#refreshInflight && this.#refreshInflight.region === region) {
572
+ return this.#refreshInflight.promise;
573
+ }
574
+ const promise = this.#doRefresh(refreshToken, opts).finally(() => {
575
+ if (this.#refreshInflight?.promise === promise) {
576
+ this.#refreshInflight = null;
577
+ }
578
+ });
579
+ this.#refreshInflight = { region, promise };
580
+ return promise;
581
+ }
582
+ async #doRefresh(refreshToken, opts) {
583
+ const variables = { refreshToken };
584
+ if (opts?.clientMeta !== void 0) {
585
+ variables.clientMeta = opts.clientMeta;
586
+ }
587
+ const body = await this.#graphqlUserData.request(
588
+ REFRESH_TAPHUB_TOKEN_MUTATION,
589
+ variables,
590
+ opts?.signal ? { signal: opts.signal } : void 0
591
+ );
592
+ const result = normaliseRefreshResponse(body);
593
+ this.#setToken(result.taphubToken, { isDemo: false });
594
+ return result;
595
+ }
448
596
  async logout() {
449
597
  this.#setToken(null);
450
598
  if (this.#onLogout) {
@@ -1109,18 +1257,30 @@ function normaliseCandles(list) {
1109
1257
  coefMults: c.coefMults
1110
1258
  }));
1111
1259
  }
1260
+ function isAgencyComposite(id) {
1261
+ return id.includes(":");
1262
+ }
1263
+ function stripAgencyPrefix(id) {
1264
+ return id.slice(id.indexOf(":") + 1);
1265
+ }
1112
1266
  function normalisePairInfo(node) {
1113
1267
  if (typeof node.id !== "string" || node.id === "" || typeof node.pair !== "string" || node.pair === "") {
1114
1268
  throw new TaphubServerError("Invalid response from server", { code: "InvalidResponse" });
1115
1269
  }
1270
+ const composite = isAgencyComposite(node.id);
1116
1271
  return {
1117
- id: node.id,
1272
+ id: composite ? stripAgencyPrefix(node.id) : node.id,
1118
1273
  pair: node.pair,
1119
- gameplayId: node.gameplayId,
1120
- gameplayName: node.gameplayName,
1274
+ // available-pairs-retired-fields: the server no longer sends these; default
1275
+ // rather than emit `undefined` through a non-optional entity property.
1276
+ gameplayId: node.gameplayId ?? "",
1277
+ gameplayName: node.gameplayName ?? "",
1121
1278
  source: node.source,
1122
1279
  // REVIEW[bid-260602]: maps node.agencyPairId (was: node.gameId)
1123
- agencyPairId: node.agencyPairId ?? null
1280
+ // Fall back to the wire `id` only when it IS the composite — otherwise stay
1281
+ // null, as the retired field did, so nobody builds an MQTT topic out of a
1282
+ // catalog id.
1283
+ agencyPairId: node.agencyPairId ?? (composite ? node.id : null)
1124
1284
  };
1125
1285
  }
1126
1286
 
@@ -1146,10 +1306,7 @@ var BUILDER_AVAILABLE_GAME_PAIRS_QUERY = `query BuilderAvailableGamePairs($gamep
1146
1306
  builderAvailableGamePairs(gameplayId: $gameplayId) {
1147
1307
  id
1148
1308
  pair
1149
- gameplayId
1150
- gameplayName
1151
1309
  source
1152
- agencyPairId
1153
1310
  }
1154
1311
  }`;
1155
1312
 
@@ -1194,8 +1351,11 @@ var PairModule = class {
1194
1351
  /**
1195
1352
  * Returns available game pairs, optionally filtered by gameplay.
1196
1353
  *
1197
- * When called with a valid JWT (authenticated builder), each entry includes
1198
- * `gameId` use it directly as the MQTT topic `game/{gameId}/candle`.
1354
+ * Each entry's `id` is the bare catalog pair id, safe to pass straight back
1355
+ * as a `pairId` argument. When called with a valid JWT (authenticated
1356
+ * builder), `agencyPairId` additionally carries the agency composite — use
1357
+ * THAT one as the MQTT topic `game/{gameId}/candle`. It is `null` for
1358
+ * anonymous callers.
1199
1359
  *
1200
1360
  * @example
1201
1361
  * const pairs = await client.pair.availableGamePairs({ gameplayId: 'taptrading' });
@@ -1245,6 +1405,9 @@ function walletBalanceTopic(userId) {
1245
1405
  function agencyPairStatsTopic(aid, pairId) {
1246
1406
  return `public/agency/${aid}/pair/${pairId}/stats`;
1247
1407
  }
1408
+ function userMigrationTopic(userId) {
1409
+ return `user/${userId}/migration`;
1410
+ }
1248
1411
  function userBidsWildcardTopic(userId) {
1249
1412
  return `${TOPIC_PREFIX}/+/user/${userId}/bid_result`;
1250
1413
  }
@@ -1271,12 +1434,14 @@ function userScopedTopicsFor(gameId, userId) {
1271
1434
  return USER_SCOPED_SUFFIXES.map((s) => topicFor(gameId, s, userId));
1272
1435
  }
1273
1436
  function createMqttTransport(endpoint, opts = {}) {
1437
+ const resolveEndpoint = () => typeof endpoint === "function" ? endpoint() : endpoint;
1274
1438
  let client = null;
1275
1439
  const subscriptions = /* @__PURE__ */ new Map();
1276
1440
  const candleSubscriptions = /* @__PURE__ */ new Map();
1277
1441
  const statsSubscriptions = /* @__PURE__ */ new Map();
1278
1442
  const walletSubscriptions = /* @__PURE__ */ new Map();
1279
1443
  const userBidsSubscriptions = /* @__PURE__ */ new Map();
1444
+ const migrationSubscriptions = /* @__PURE__ */ new Map();
1280
1445
  const { onLifecycle, auth } = opts;
1281
1446
  let connectStartedAt = 0;
1282
1447
  function fireLifecycle(event) {
@@ -1289,7 +1454,7 @@ function createMqttTransport(endpoint, opts = {}) {
1289
1454
  function ensureConnected() {
1290
1455
  if (client) return client;
1291
1456
  connectStartedAt = Date.now();
1292
- client = import_mqtt.default.connect(endpoint, {
1457
+ client = import_mqtt.default.connect(resolveEndpoint(), {
1293
1458
  clean: true,
1294
1459
  reconnectPeriod: 2e3,
1295
1460
  connectTimeout: 1e4,
@@ -1330,6 +1495,9 @@ function createMqttTransport(endpoint, opts = {}) {
1330
1495
  for (const entry of userBidsSubscriptions.values()) {
1331
1496
  c.subscribe(entry.pattern, { qos: 1 });
1332
1497
  }
1498
+ for (const entry of migrationSubscriptions.values()) {
1499
+ c.subscribe(entry.topic, { qos: 1 });
1500
+ }
1333
1501
  fireLifecycle({ kind: "connect", rttMs: Date.now() - connectStartedAt });
1334
1502
  });
1335
1503
  client.on("reconnect", () => {
@@ -1378,6 +1546,20 @@ function createMqttTransport(endpoint, opts = {}) {
1378
1546
  walletSub.onMessage(receivedTopic, payload2);
1379
1547
  return;
1380
1548
  }
1549
+ const migrationSub = [...migrationSubscriptions.values()].find(
1550
+ (s) => s.topic === receivedTopic
1551
+ );
1552
+ if (migrationSub) {
1553
+ let payload2;
1554
+ try {
1555
+ payload2 = JSON.parse(message.toString());
1556
+ } catch {
1557
+ migrationSub.onError(new Error(`Invalid JSON on topic ${receivedTopic}`));
1558
+ return;
1559
+ }
1560
+ migrationSub.onMessage(receivedTopic, payload2);
1561
+ return;
1562
+ }
1381
1563
  let userBidsPayload;
1382
1564
  for (const sub of userBidsSubscriptions.values()) {
1383
1565
  if (!topicMatchesWildcard(sub.pattern, receivedTopic)) continue;
@@ -1491,6 +1673,19 @@ function createMqttTransport(endpoint, opts = {}) {
1491
1673
  if (client) client.unsubscribe(entry.pattern);
1492
1674
  userBidsSubscriptions.delete(userId);
1493
1675
  },
1676
+ subscribeMigration(userId, onMessage, onError) {
1677
+ if (migrationSubscriptions.has(userId)) return;
1678
+ const topic = userMigrationTopic(userId);
1679
+ const mqttClient = ensureConnected();
1680
+ migrationSubscriptions.set(userId, { topic, onMessage, onError });
1681
+ mqttClient.subscribe(topic, { qos: 1 });
1682
+ },
1683
+ unsubscribeMigration(userId) {
1684
+ const entry = migrationSubscriptions.get(userId);
1685
+ if (!entry) return;
1686
+ if (client) client.unsubscribe(entry.topic);
1687
+ migrationSubscriptions.delete(userId);
1688
+ },
1494
1689
  unsubscribeAll(gameId, userId) {
1495
1690
  const matches = entriesForGame(gameId);
1496
1691
  if (matches.length === 0) return;
@@ -1515,6 +1710,12 @@ function createMqttTransport(endpoint, opts = {}) {
1515
1710
  }
1516
1711
  subscriptions.delete(subKey(target.gameId, target.userId));
1517
1712
  },
1713
+ reconnect() {
1714
+ if (!client) return;
1715
+ client.end(true);
1716
+ client = null;
1717
+ ensureConnected();
1718
+ },
1518
1719
  close() {
1519
1720
  if (client) {
1520
1721
  for (const sub of subscriptions.values()) {
@@ -1534,12 +1735,16 @@ function createMqttTransport(endpoint, opts = {}) {
1534
1735
  for (const entry of userBidsSubscriptions.values()) {
1535
1736
  client.unsubscribe(entry.pattern);
1536
1737
  }
1738
+ for (const entry of migrationSubscriptions.values()) {
1739
+ client.unsubscribe(entry.topic);
1740
+ }
1537
1741
  }
1538
1742
  subscriptions.clear();
1539
1743
  candleSubscriptions.clear();
1540
1744
  statsSubscriptions.clear();
1541
1745
  walletSubscriptions.clear();
1542
1746
  userBidsSubscriptions.clear();
1747
+ migrationSubscriptions.clear();
1543
1748
  if (client) {
1544
1749
  client.end(true);
1545
1750
  client = null;
@@ -1549,7 +1754,7 @@ function createMqttTransport(endpoint, opts = {}) {
1549
1754
  }
1550
1755
 
1551
1756
  // src/modules/realtime/index.ts
1552
- var import_eventemitter34 = __toESM(require("eventemitter3"));
1757
+ var import_eventemitter35 = __toESM(require("eventemitter3"));
1553
1758
 
1554
1759
  // src/modules/realtime/GameChannel.ts
1555
1760
  var import_eventemitter3 = __toESM(require("eventemitter3"));
@@ -1561,9 +1766,9 @@ var GameChannel = class extends import_eventemitter3.default {
1561
1766
  }
1562
1767
  };
1563
1768
 
1564
- // src/modules/realtime/UserBidsChannel.ts
1769
+ // src/modules/realtime/MigrationChannel.ts
1565
1770
  var import_eventemitter32 = __toESM(require("eventemitter3"));
1566
- var UserBidsChannel = class extends import_eventemitter32.default {
1771
+ var MigrationChannel = class extends import_eventemitter32.default {
1567
1772
  userId;
1568
1773
  constructor(userId) {
1569
1774
  super();
@@ -1571,9 +1776,19 @@ var UserBidsChannel = class extends import_eventemitter32.default {
1571
1776
  }
1572
1777
  };
1573
1778
 
1574
- // src/modules/realtime/WalletChannel.ts
1779
+ // src/modules/realtime/UserBidsChannel.ts
1575
1780
  var import_eventemitter33 = __toESM(require("eventemitter3"));
1576
- var WalletChannel = class extends import_eventemitter33.default {
1781
+ var UserBidsChannel = class extends import_eventemitter33.default {
1782
+ userId;
1783
+ constructor(userId) {
1784
+ super();
1785
+ this.userId = userId;
1786
+ }
1787
+ };
1788
+
1789
+ // src/modules/realtime/WalletChannel.ts
1790
+ var import_eventemitter34 = __toESM(require("eventemitter3"));
1791
+ var WalletChannel = class extends import_eventemitter34.default {
1577
1792
  userId;
1578
1793
  constructor(userId) {
1579
1794
  super();
@@ -1664,6 +1879,19 @@ function mapWireWalletBalance(raw) {
1664
1879
  reason: p.reason ?? ""
1665
1880
  };
1666
1881
  }
1882
+ function mapWireMigrationCompleted(raw) {
1883
+ if (raw === null || typeof raw !== "object") return null;
1884
+ const p = raw;
1885
+ if (p.type !== "migration_completed") return null;
1886
+ if (typeof p.toRegion !== "string" || p.toRegion === "") return null;
1887
+ return {
1888
+ type: "migration_completed",
1889
+ migrationId: p.migrationId,
1890
+ fromRegion: p.fromRegion,
1891
+ toRegion: p.toRegion,
1892
+ completedAt: p.completedAt
1893
+ };
1894
+ }
1667
1895
  function mapWireConfig(raw) {
1668
1896
  const p = raw;
1669
1897
  return {
@@ -1704,13 +1932,15 @@ function mapWireToEvent(topic, payload) {
1704
1932
  function normaliseUserId2(userId) {
1705
1933
  return userId && userId !== "" ? userId : null;
1706
1934
  }
1707
- var RealtimeModule = class extends import_eventemitter34.default {
1935
+ var RealtimeModule = class extends import_eventemitter35.default {
1708
1936
  #transport;
1709
1937
  #entries = /* @__PURE__ */ new Map();
1710
1938
  #walletEntries = /* @__PURE__ */ new Map();
1711
1939
  // keyed by userId
1712
1940
  #userBidsEntries = /* @__PURE__ */ new Map();
1713
1941
  // keyed by userId
1942
+ #migrationEntries = /* @__PURE__ */ new Map();
1943
+ // keyed by userId
1714
1944
  #agencyId;
1715
1945
  constructor(mqttEndpointOrOptions) {
1716
1946
  super();
@@ -1899,6 +2129,57 @@ var RealtimeModule = class extends import_eventemitter34.default {
1899
2129
  entry.channel.removeAllListeners();
1900
2130
  this.#transport.unsubscribeUserBids(cleanUserId);
1901
2131
  }
2132
+ /**
2133
+ * Subscribe to the user-scoped migration completion stream for `userId`,
2134
+ * topic `user/{userId}/migration` (ux-260730). Returns a `MigrationChannel`
2135
+ * that emits `migrationCompleted` when the user's home-region migration
2136
+ * finishes. Malformed or schema-mismatched payloads are dropped silently.
2137
+ *
2138
+ * Reference-counted by `userId`, mirroring `subscribeWallet`. Intended
2139
+ * lifecycle (design D7): hold the channel only while the client is in the
2140
+ * migrating state; unsubscribe on completion/logout. The publisher retains
2141
+ * the completion message, so subscribing after the fact still delivers it.
2142
+ */
2143
+ subscribeMigration(userId) {
2144
+ const cleanUserId = normaliseUserId2(userId);
2145
+ if (!cleanUserId) {
2146
+ throw new TaphubError("userId is required to subscribe to migration events", {
2147
+ code: "UserIdRequired"
2148
+ });
2149
+ }
2150
+ const existing = this.#migrationEntries.get(cleanUserId);
2151
+ if (existing) {
2152
+ existing.refcount += 1;
2153
+ return existing.channel;
2154
+ }
2155
+ const channel = new MigrationChannel(cleanUserId);
2156
+ const onMessage = (_topic, payload) => {
2157
+ const mapped = mapWireMigrationCompleted(payload);
2158
+ if (!mapped) return;
2159
+ channel.emit("migrationCompleted", mapped);
2160
+ };
2161
+ const onError = (err) => {
2162
+ channel.emit("error", err);
2163
+ };
2164
+ this.#migrationEntries.set(cleanUserId, { userId: cleanUserId, channel, refcount: 1 });
2165
+ this.#transport.subscribeMigration(cleanUserId, onMessage, onError);
2166
+ return channel;
2167
+ }
2168
+ /**
2169
+ * Decrement the migration subscription refcount for `userId`. Tears down the
2170
+ * MQTT topic and removes the channel only at zero. No-op if absent.
2171
+ */
2172
+ unsubscribeMigration(userId) {
2173
+ const cleanUserId = normaliseUserId2(userId);
2174
+ if (!cleanUserId) return;
2175
+ const entry = this.#migrationEntries.get(cleanUserId);
2176
+ if (!entry) return;
2177
+ entry.refcount -= 1;
2178
+ if (entry.refcount > 0) return;
2179
+ this.#migrationEntries.delete(cleanUserId);
2180
+ entry.channel.removeAllListeners();
2181
+ this.#transport.unsubscribeMigration(cleanUserId);
2182
+ }
1902
2183
  /**
1903
2184
  * Subscribe to public market candle data for a pair, keyed by pairId
1904
2185
  * (the #2 game_pairs.id, e.g. "grid-ETH-USD"). Independent of game/agency —
@@ -1941,6 +2222,15 @@ var RealtimeModule = class extends import_eventemitter34.default {
1941
2222
  if (!this.#agencyId) return;
1942
2223
  this.#transport.unsubscribeAgencyPairStats(this.#agencyId, pairId);
1943
2224
  }
2225
+ /**
2226
+ * Reconnect the MQTT transport, re-resolving its (possibly region-scoped)
2227
+ * endpoint. All active subscriptions are preserved and re-declared on the new
2228
+ * connection (multi-geo home-region switch). No-op when not currently
2229
+ * connected. Delegates to the transport's `reconnect`.
2230
+ */
2231
+ reconnect() {
2232
+ this.#transport.reconnect();
2233
+ }
1944
2234
  disconnect() {
1945
2235
  for (const entry of this.#entries.values()) {
1946
2236
  entry.channel.removeAllListeners();
@@ -1954,6 +2244,10 @@ var RealtimeModule = class extends import_eventemitter34.default {
1954
2244
  entry.channel.removeAllListeners();
1955
2245
  }
1956
2246
  this.#userBidsEntries.clear();
2247
+ for (const entry of this.#migrationEntries.values()) {
2248
+ entry.channel.removeAllListeners();
2249
+ }
2250
+ this.#migrationEntries.clear();
1957
2251
  this.#transport.close();
1958
2252
  }
1959
2253
  };
@@ -2046,6 +2340,9 @@ var LIST_ENABLED_CURRENCIES_QUERY = `query ListEnabledCurrencies($input: ListEna
2046
2340
  listEnabledCurrencies(input: $input) { code unit unitSymbol }
2047
2341
  }`;
2048
2342
  var MY_WALLET_BY_CURRENCY_QUERY = "query MyWalletByCurrency($currency: String!) { myWalletByCurrency(currency: $currency) { id amount currency isEnable } }";
2343
+ var REQUEST_REGION_MIGRATION_MUTATION = `mutation RequestRegionMigration($input: RequestRegionMigrationInput!) {
2344
+ requestRegionMigration(input: $input) { migrationId }
2345
+ }`;
2049
2346
  var USER_PNL_QUERY = `query UserPnL($period: String) {
2050
2347
  userPnL(period: $period) {
2051
2348
  gain total_wagered total_payout total_bids total_wins pnlRank volRank
@@ -2095,6 +2392,29 @@ var UserModule = class {
2095
2392
  );
2096
2393
  return normaliseMyWalletByCurrencyResponse(body);
2097
2394
  }
2395
+ /**
2396
+ * Start migrating the authenticated user's home region (multi-geo Phase 2).
2397
+ * User-service validates the JWT, checks the feature flag, and forwards to the
2398
+ * region directory (cooldown / single-flight / region-set validation happen
2399
+ * server-side). Typed rejections keep their `extensions.code`
2400
+ * (Migration_Unavailable, Migration_CooldownActive, …) on the error's `code`
2401
+ * so hosts can localise them.
2402
+ */
2403
+ async requestRegionMigration(toRegion, opts) {
2404
+ const body = await this.#graphqlUser.request(
2405
+ REQUEST_REGION_MIGRATION_MUTATION,
2406
+ { input: { toRegion } },
2407
+ opts
2408
+ );
2409
+ const migrationId = body?.requestRegionMigration?.migrationId;
2410
+ if (typeof migrationId !== "string" || migrationId === "") {
2411
+ throw new TaphubServerError("Invalid response from server", {
2412
+ code: "INVALID_RESPONSE",
2413
+ details: body
2414
+ });
2415
+ }
2416
+ return { migrationId };
2417
+ }
2098
2418
  get currencies() {
2099
2419
  return this.#currencies;
2100
2420
  }
@@ -2545,6 +2865,21 @@ function createRestProbe(monitor) {
2545
2865
  };
2546
2866
  }
2547
2867
 
2868
+ // src/region.ts
2869
+ var KNOWN_REGIONS = ["sg", "eu", "jp"];
2870
+ var DEFAULT_REGION = "sg";
2871
+ function isKnownRegion(region) {
2872
+ return region != null && KNOWN_REGIONS.includes(region);
2873
+ }
2874
+ function resolveRegionBaseUrl(region, domains, fallbackBaseUrl) {
2875
+ if (region == null || region === "") {
2876
+ return fallbackBaseUrl;
2877
+ }
2878
+ const map = domains ?? {};
2879
+ const effectiveRegion = isKnownRegion(region) ? region : DEFAULT_REGION;
2880
+ return map[effectiveRegion] ?? map[DEFAULT_REGION] ?? fallbackBaseUrl;
2881
+ }
2882
+
2548
2883
  // src/storage/index.ts
2549
2884
  var MemoryAdapter = class {
2550
2885
  store = /* @__PURE__ */ new Map();
@@ -2601,6 +2936,11 @@ var TaphubStorage = {
2601
2936
  }
2602
2937
  };
2603
2938
 
2939
+ // src/transport/shared/baseUrl.ts
2940
+ function resolveBaseUrl(input) {
2941
+ return typeof input === "function" ? input() : input;
2942
+ }
2943
+
2604
2944
  // src/transport/shared/errors.ts
2605
2945
  function mapNetworkErrorToTaphubError(err) {
2606
2946
  if (err instanceof DOMException && err.name === "AbortError") {
@@ -2756,7 +3096,7 @@ function createGraphQLTransport(deps) {
2756
3096
  }
2757
3097
  async function execute(token, query, variables, opts) {
2758
3098
  const fetchImpl = resolveFetch();
2759
- const base = baseUrl.replace(/\/+$/, "");
3099
+ const base = resolveBaseUrl(baseUrl).replace(/\/+$/, "");
2760
3100
  const op = extractOp(query);
2761
3101
  const url = `${base}?${op}`;
2762
3102
  const headers = buildHeaders({ token, hasBody: true, agencyId });
@@ -2900,7 +3240,7 @@ function createRestTransport(deps) {
2900
3240
  }
2901
3241
  async function request(method, path, body, opts) {
2902
3242
  const fetchImpl = resolveFetch();
2903
- const url = buildUrl(baseUrl, path);
3243
+ const url = buildUrl(resolveBaseUrl(baseUrl), path);
2904
3244
  const token = getToken();
2905
3245
  const hasBody = body !== void 0;
2906
3246
  const headers = buildHeaders({ token, hasBody });
@@ -2970,6 +3310,9 @@ function tokenStorageKey(agencyId) {
2970
3310
  function isDemoStorageKey(agencyId) {
2971
3311
  return `taphub:${agencyId}:isDemo`;
2972
3312
  }
3313
+ function regionStorageKey(agencyId) {
3314
+ return `taphub:${agencyId}:region`;
3315
+ }
2973
3316
  var TaphubClient = class {
2974
3317
  agencyId;
2975
3318
  endpoint;
@@ -2996,11 +3339,29 @@ var TaphubClient = class {
2996
3339
  bus;
2997
3340
  #token;
2998
3341
  #isDemo;
3342
+ /** User's home region (multi-geo, design D4). null → default base URL. */
3343
+ #region;
2999
3344
  #tokenKey;
3000
3345
  #isDemoKey;
3346
+ #regionKey;
3347
+ /** Region → base URL map (config, never hardcoded). undefined → always use `endpoint`. */
3348
+ #regionDomains;
3349
+ /** Fixed MQTT broker endpoint (fallback for region resolution). undefined → no realtime. */
3350
+ #mqttEndpoint;
3351
+ /** Region → MQTT endpoint map (config, never hardcoded). undefined → always use `#mqttEndpoint`. */
3352
+ #regionMqttEndpoints;
3001
3353
  #rest;
3002
3354
  #graphql;
3003
3355
  #graphqlUser;
3356
+ /**
3357
+ * Auth_WrongRegion-retrying variants of the two GraphQL transports, used by the
3358
+ * user-scoped DATA modules (user / bid) and token refresh. A data call that
3359
+ * lands on the wrong cluster switches the region and retries once (multi-geo
3360
+ * backstop). Login keeps the raw transports — it runs its own wrong-region
3361
+ * retry, so wrapping there would double-retry.
3362
+ */
3363
+ #graphqlData;
3364
+ #graphqlUserData;
3004
3365
  constructor(config) {
3005
3366
  if (!config.agencyId) {
3006
3367
  throw new TaphubValidationError("agencyId is required", {
@@ -3025,39 +3386,62 @@ var TaphubClient = class {
3025
3386
  const restProbe = createRestProbe(this.network);
3026
3387
  this.#tokenKey = tokenStorageKey(this.agencyId);
3027
3388
  this.#isDemoKey = isDemoStorageKey(this.agencyId);
3389
+ this.#regionKey = regionStorageKey(this.agencyId);
3390
+ this.#regionDomains = config.regionDomains;
3391
+ this.#mqttEndpoint = config.mqttEndpoint;
3392
+ this.#regionMqttEndpoints = config.regionMqttEndpoints;
3028
3393
  this.#token = this.storage.get(this.#tokenKey);
3029
3394
  this.#isDemo = this.storage.get(this.#isDemoKey) === "1";
3395
+ this.#region = this.storage.get(this.#regionKey);
3030
3396
  this.#rest = createRestTransport({
3031
- baseUrl: this.endpoint,
3397
+ // Thunk: re-resolved per request so a post-login region change re-targets the
3398
+ // origin without rebuilding the transport. Resolves to `endpoint` when no
3399
+ // region domains are configured (pre-multi-geo behaviour unchanged).
3400
+ baseUrl: () => this.#originForRegion(),
3032
3401
  getToken: () => this.getToken(),
3033
3402
  fetch: config.fetch,
3034
3403
  onRequest: restProbe
3035
3404
  });
3036
- const baseApi = this.endpoint.replace(/\/+$/, "");
3037
3405
  this.#graphql = createGraphQLTransport({
3038
- baseUrl: `${baseApi}/grid-api/grid-gql`,
3406
+ baseUrl: () => `${this.#originForRegion()}/grid-api/grid-gql`,
3039
3407
  getToken: () => this.getToken(),
3040
3408
  agencyId: this.agencyId,
3041
3409
  fetch: config.fetch,
3042
3410
  onRequest: graphqlProbe
3043
3411
  });
3044
3412
  this.#graphqlUser = createGraphQLTransport({
3045
- baseUrl: `${baseApi}/taphub-user-service/th-user-gql`,
3413
+ baseUrl: () => `${this.#originForRegion()}/taphub-user-service/th-user-gql`,
3046
3414
  getToken: () => this.getToken(),
3047
3415
  agencyId: this.agencyId,
3048
3416
  fetch: config.fetch,
3049
3417
  onRequest: graphqlProbe
3050
3418
  });
3419
+ this.#graphqlData = withWrongRegionRetry(this.#graphql, (region) => this.#setRegion(region));
3420
+ this.#graphqlUserData = withWrongRegionRetry(
3421
+ this.#graphqlUser,
3422
+ (region) => this.#setRegion(region)
3423
+ );
3051
3424
  this.user = new UserModule({
3052
- graphql: this.#graphql,
3053
- graphqlUser: this.#graphqlUser
3425
+ graphql: this.#graphqlData,
3426
+ graphqlUser: this.#graphqlUserData
3054
3427
  });
3055
3428
  this.auth = new AuthModule({
3056
3429
  rest: this.#rest,
3057
3430
  graphql: this.#graphql,
3058
3431
  graphqlUser: this.#graphqlUser,
3432
+ // Region-routed + wrong-region-retrying transport for token refresh only.
3433
+ graphqlUserData: this.#graphqlUserData,
3059
3434
  setToken: (t, opts) => this.setToken(t, opts),
3435
+ isDemo: () => this.isDemo(),
3060
3436
  agencyId: this.agencyId,
3437
+ // Multi-geo: login sends the client's current (persisted) region; the server
3438
+ // pins it on first join and returns the stored home region thereafter.
3439
+ getRegion: () => this.getRegion(),
3440
+ // Multi-geo: a login that lands on the wrong cluster is rejected with
3441
+ // Auth_WrongRegion + the user's homeRegion; the auth module silently
3442
+ // switches the client's region here and retries once. Kept separate from
3443
+ // setToken so a region redirect never disturbs the auth/token state.
3444
+ setRegion: (region) => this.#setRegion(region),
3061
3445
  onLoginSuccess: () => this.user.refreshCurrencies().then(() => void 0).catch(() => void 0),
3062
3446
  onLogout: () => {
3063
3447
  this.user.clearCurrencies();
@@ -3066,14 +3450,17 @@ var TaphubClient = class {
3066
3450
  const clockSync = new ClockSync();
3067
3451
  this.pair = new PairModule({ graphql: this.#graphql, clockSync });
3068
3452
  this.bid = new BidModule({
3069
- graphql: this.#graphql,
3453
+ graphql: this.#graphqlData,
3070
3454
  getClockOffset: () => clockSync.getOffset()
3071
3455
  });
3072
3456
  this.leaderboard = new LeaderboardModule({ graphql: this.#graphql });
3073
3457
  this.agencyPairs = new AgencyPairModule({ graphql: this.#graphql });
3074
3458
  this.locale = new LocaleModule({ graphql: this.#graphql });
3075
3459
  this.realtime = config.mqttEndpoint ? new RealtimeModule({
3076
- mqttEndpoint: config.mqttEndpoint,
3460
+ // Thunk: re-resolved at connect time so a post-login region change
3461
+ // re-targets the broker without rebuilding the module (multi-geo D4).
3462
+ // Resolves to `mqttEndpoint` when no region MQTT map is configured.
3463
+ mqttEndpoint: () => this.#resolveMqttEndpoint(),
3077
3464
  agencyId: this.agencyId,
3078
3465
  mqttAuth: config.mqttAuth,
3079
3466
  onMqttLifecycle: createMqttProbe(this.network)
@@ -3133,18 +3520,87 @@ var TaphubClient = class {
3133
3520
  getToken() {
3134
3521
  return this.#token;
3135
3522
  }
3523
+ /**
3524
+ * Current API origin, resolved from the home region (multi-geo, design D4).
3525
+ * Trailing slashes trimmed so the service-path suffix composes cleanly. Falls
3526
+ * back to `endpoint` when no region domains are configured or the region is
3527
+ * unknown/absent.
3528
+ */
3529
+ #originForRegion() {
3530
+ return resolveRegionBaseUrl(this.#region, this.#regionDomains, this.endpoint).replace(
3531
+ /\/+$/,
3532
+ ""
3533
+ );
3534
+ }
3535
+ /**
3536
+ * Current MQTT broker endpoint, resolved from the home region (multi-geo,
3537
+ * design D4). Falls back to the configured `mqttEndpoint` when no region MQTT
3538
+ * map is configured or the region is unknown/absent. Only meaningful when
3539
+ * `mqttEndpoint` was configured (otherwise `realtime` is undefined).
3540
+ */
3541
+ #resolveMqttEndpoint() {
3542
+ return resolveRegionBaseUrl(this.#region, this.#regionMqttEndpoints, this.#mqttEndpoint ?? "");
3543
+ }
3544
+ /** The user's home region, once resolved from a login response. */
3545
+ getRegion() {
3546
+ return this.#region;
3547
+ }
3548
+ /**
3549
+ * Set the home region and persist it (survives reloads like the token/isDemo). Shared
3550
+ * by the login path (setToken with a homeRegion) and the wrong-region redirect handler
3551
+ * (task 1.13). A pure region update — it never touches the token or isDemo, so a
3552
+ * mid-session redirect cannot disturb the auth state.
3553
+ */
3554
+ #setRegion(region) {
3555
+ if (this.#region === region) return;
3556
+ const prevMqttEndpoint = this.realtime ? this.#resolveMqttEndpoint() : null;
3557
+ this.#region = region;
3558
+ this.storage.set(this.#regionKey, region);
3559
+ if (this.realtime && prevMqttEndpoint !== null) {
3560
+ const nextMqttEndpoint = this.#resolveMqttEndpoint();
3561
+ if (nextMqttEndpoint !== prevMqttEndpoint) {
3562
+ this.realtime.reconnect();
3563
+ }
3564
+ }
3565
+ }
3566
+ /**
3567
+ * Complete a home-region migration by cutting the client over to `toRegion`
3568
+ * (ux-260730-migration-complete-notify). Intended to be called after a
3569
+ * `migrationCompleted` event (realtime `subscribeMigration`) or an equivalent
3570
+ * poll result confirmed the migration finished — by then the server-side home
3571
+ * region already points at `toRegion`.
3572
+ *
3573
+ * Purpose-named public wrapper over the private `#setRegion` primitive (the
3574
+ * generic region setter stays private so third-party builders cannot
3575
+ * arbitrarily re-target regions). Persists the region and re-targets the
3576
+ * GQL/REST transports; MQTT reconnects only when the resolved broker endpoint
3577
+ * actually changes. Region validation is lenient, matching `region.ts`: an
3578
+ * unknown region is stored as-is and resolves to the default region's
3579
+ * endpoints (enum-fallback, never throws). Idempotent for the already-current
3580
+ * region (no reconnect), and a safe endpoint-level no-op when no region
3581
+ * domain/MQTT maps are configured.
3582
+ */
3583
+ completeRegionMigration(toRegion) {
3584
+ if (toRegion == null || toRegion === "") return;
3585
+ this.#setRegion(toRegion);
3586
+ }
3136
3587
  setToken(token, opts) {
3137
3588
  this.#token = token;
3138
3589
  if (token === null) {
3139
3590
  this.#isDemo = false;
3591
+ this.#region = null;
3140
3592
  this.storage.remove(this.#tokenKey);
3141
3593
  this.storage.remove(this.#isDemoKey);
3594
+ this.storage.remove(this.#regionKey);
3142
3595
  return;
3143
3596
  }
3144
3597
  const isDemo = opts?.isDemo ?? false;
3145
3598
  this.#isDemo = isDemo;
3146
3599
  this.storage.set(this.#tokenKey, token);
3147
3600
  this.storage.set(this.#isDemoKey, isDemo ? "1" : "0");
3601
+ if (opts?.homeRegion !== void 0) {
3602
+ this.#setRegion(opts.homeRegion);
3603
+ }
3148
3604
  }
3149
3605
  isDemo() {
3150
3606
  return this.#isDemo;
@@ -3165,6 +3621,81 @@ var TaphubClient = class {
3165
3621
  }
3166
3622
  };
3167
3623
 
3624
+ // src/regionProbe.ts
3625
+ var REGION_PROBE_CACHE_KEY = "taphub:region-probe";
3626
+ var DEFAULT_PROBE_TTL_SECONDS = 3600;
3627
+ var DEFAULT_PROBE_TIMEOUT_MS = 2e3;
3628
+ var DEFAULT_PROBE_PATH = "/grid-api/grid-gql";
3629
+ var PROBE_QUERY = "{serverTime}";
3630
+ function readFreshCache(storage, key, now, ttlSeconds) {
3631
+ try {
3632
+ const raw = storage.get(key);
3633
+ if (!raw) return null;
3634
+ const parsed = JSON.parse(raw);
3635
+ if (!parsed || typeof parsed.region !== "string" || typeof parsed.ts !== "number") {
3636
+ return null;
3637
+ }
3638
+ if (now() - parsed.ts >= ttlSeconds * 1e3) return null;
3639
+ return parsed.region;
3640
+ } catch {
3641
+ return null;
3642
+ }
3643
+ }
3644
+ function writeCache(storage, key, region, now) {
3645
+ try {
3646
+ storage.set(key, JSON.stringify({ region, ts: now() }));
3647
+ } catch {
3648
+ }
3649
+ }
3650
+ async function probeNearestRegion(domains, opts = {}) {
3651
+ const {
3652
+ fetch: fetchImpl = globalThis.fetch,
3653
+ storage = autoDetectStorage(),
3654
+ ttlSeconds = DEFAULT_PROBE_TTL_SECONDS,
3655
+ timeoutMs = DEFAULT_PROBE_TIMEOUT_MS,
3656
+ probePath = DEFAULT_PROBE_PATH,
3657
+ now = Date.now,
3658
+ cacheKey = REGION_PROBE_CACHE_KEY
3659
+ } = opts;
3660
+ const entries = Object.entries(domains ?? {}).filter(
3661
+ (entry) => typeof entry[1] === "string" && entry[1] !== ""
3662
+ );
3663
+ if (entries.length < 2) return null;
3664
+ const cached = readFreshCache(storage, cacheKey, now, ttlSeconds);
3665
+ if (cached !== null && entries.some(([region]) => region === cached)) {
3666
+ return cached;
3667
+ }
3668
+ if (typeof fetchImpl !== "function") return null;
3669
+ const controller = new AbortController();
3670
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3671
+ try {
3672
+ const winner = await Promise.any(
3673
+ entries.map(async ([region, baseUrl]) => {
3674
+ const url = baseUrl.replace(/\/+$/, "") + probePath;
3675
+ const response = await fetchImpl(url, {
3676
+ method: "POST",
3677
+ headers: { "content-type": "application/json" },
3678
+ body: JSON.stringify({ query: PROBE_QUERY }),
3679
+ signal: controller.signal
3680
+ });
3681
+ const body = await response.json();
3682
+ const serverTime = body?.data?.serverTime;
3683
+ if (typeof serverTime !== "number") {
3684
+ throw new Error(`region probe: "${region}" returned no serverTime`);
3685
+ }
3686
+ return region;
3687
+ })
3688
+ );
3689
+ writeCache(storage, cacheKey, winner, now);
3690
+ return winner;
3691
+ } catch {
3692
+ return null;
3693
+ } finally {
3694
+ clearTimeout(timer);
3695
+ controller.abort();
3696
+ }
3697
+ }
3698
+
3168
3699
  // src/modules/realtime/types.ts
3169
3700
  var CANDLE_EVENT = {
3170
3701
  NEW: "new",
@@ -3371,10 +3902,14 @@ function computeBaseline(candleClose, candleTimeSec, cellSizeValue, cellSizeTime
3371
3902
  BidModule,
3372
3903
  CANDLE_EVENT,
3373
3904
  DEFAULT_CHART_HISTORY_LIMIT,
3905
+ DEFAULT_PROBE_TTL_SECONDS,
3906
+ DEFAULT_REGION,
3907
+ KNOWN_REGIONS,
3374
3908
  LeaderboardModule,
3375
3909
  LocaleModule,
3376
3910
  NetworkQualityMonitor,
3377
3911
  PairModule,
3912
+ REGION_PROBE_CACHE_KEY,
3378
3913
  RealtimeModule,
3379
3914
  TaphubAuthError,
3380
3915
  TaphubClient,
@@ -3395,6 +3930,7 @@ function computeBaseline(candleClose, candleTimeSec, cellSizeValue, cellSizeTime
3395
3930
  computeBaseline,
3396
3931
  errorFunction,
3397
3932
  isCancelled,
3933
+ isKnownRegion,
3398
3934
  isLoss,
3399
3935
  isPending,
3400
3936
  isTerminal,
@@ -3403,5 +3939,7 @@ function computeBaseline(candleClose, candleTimeSec, cellSizeValue, cellSizeTime
3403
3939
  normalPDF,
3404
3940
  normaliseLang,
3405
3941
  pairIdFromBidResultTopic,
3942
+ probeNearestRegion,
3943
+ resolveRegionBaseUrl,
3406
3944
  roundCoefToSignificantDigits
3407
3945
  });