@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.d.mts CHANGED
@@ -1,5 +1,49 @@
1
1
  import EventEmitter from 'eventemitter3';
2
2
 
3
+ /**
4
+ * Home-region → API base-URL resolution (multi-geo, design D4: domain-per-region).
5
+ *
6
+ * The region → domain map is caller-supplied config (see
7
+ * {@link TaphubClientConfig.regionDomains}) — hosts are NEVER hardcoded here. An
8
+ * unknown or missing region value degrades to the default region rather than
9
+ * crashing the client (defensive client coding): a server may one day return a
10
+ * region a shipped build does not yet recognise, and that must not break the app.
11
+ */
12
+ /**
13
+ * Region set for Phase 1 (design D4: `sg`, `eu`, `jp`). Kept as a `const` tuple so
14
+ * {@link KnownRegion} is derived from it and the two never drift apart.
15
+ */
16
+ declare const KNOWN_REGIONS: readonly ["sg", "eu", "jp"];
17
+ type KnownRegion = (typeof KNOWN_REGIONS)[number];
18
+ /** Default home region (design/tasks: default `sg`) — also the enum-fallback target. */
19
+ declare const DEFAULT_REGION: KnownRegion;
20
+ /**
21
+ * Region → API base URL. Config-driven; supplied by the host app from env, never
22
+ * hardcoded. Keyed by region string so an unknown key is simply absent (and
23
+ * resolution falls back), rather than a type error.
24
+ */
25
+ type RegionDomainMap = Partial<Record<string, string>>;
26
+ /** Narrowing guard: is `region` one of the build's known regions? */
27
+ declare function isKnownRegion(region: string | null | undefined): region is KnownRegion;
28
+ /**
29
+ * Resolve the API base URL for a home region.
30
+ *
31
+ * - **Absent region** (`null`/`undefined`/`''`) → `fallbackBaseUrl` unchanged. A
32
+ * login response that carries no region keeps the client on its construction-time
33
+ * endpoint (additive / backward-compatible).
34
+ * - **Known region** present in `domains` → that region's domain.
35
+ * - **Unknown non-empty region** value → the default region's domain (enum-fallback,
36
+ * defensive client coding — a server-sent region a shipped build does not
37
+ * recognise must not crash it).
38
+ * - Known region absent from the map → the default region's domain when configured,
39
+ * otherwise `fallbackBaseUrl`.
40
+ *
41
+ * Never throws. When `domains` is undefined/empty (the pre-multi-geo default) it
42
+ * always returns `fallbackBaseUrl`, so a client with no region config behaves
43
+ * exactly as before.
44
+ */
45
+ declare function resolveRegionBaseUrl(region: string | null | undefined, domains: RegionDomainMap | undefined, fallbackBaseUrl: string): string;
46
+
3
47
  interface TaphubStorageAdapter {
4
48
  get(key: string): string | null;
5
49
  set(key: string, value: string): void;
@@ -28,10 +72,27 @@ interface TaphubClientConfig {
28
72
  agencyId: string;
29
73
  endpoint: string;
30
74
  mqttEndpoint?: string;
75
+ /**
76
+ * Multi-geo (design D4): region → MQTT broker endpoint. Same shape/semantics as
77
+ * {@link regionDomains} but for the realtime (MQTT) connection. When present, the
78
+ * realtime module targets the endpoint resolved from the user's home region; a
79
+ * login that changes the region reconnects to the new region's broker. When
80
+ * omitted, the single `mqttEndpoint` is used for every region — i.e. the
81
+ * pre-multi-geo behaviour is unchanged.
82
+ */
83
+ regionMqttEndpoints?: RegionDomainMap;
31
84
  /** Optional MQTT broker credentials. Ignored when `mqttEndpoint` is absent. */
32
85
  mqttAuth?: MqttAuthConfig;
33
86
  storage?: TaphubStorageAdapter;
34
87
  fetch?: typeof globalThis.fetch;
88
+ /**
89
+ * Multi-geo (design D4): region → API base URL. Supplied by the host app from
90
+ * env (never hardcoded hosts). When present, once login resolves the user's home
91
+ * region, subsequent GraphQL/REST calls target that region's domain; an unknown
92
+ * region falls back to the default region. When omitted, the client always uses
93
+ * `endpoint` — i.e. the pre-multi-geo behaviour is unchanged.
94
+ */
95
+ regionDomains?: RegionDomainMap;
35
96
  }
36
97
 
37
98
  type SignalSource = 'graphql' | 'rest' | 'mqtt' | 'browser' | 'connection';
@@ -155,16 +216,64 @@ interface LoginResult {
155
216
  accessToken: string;
156
217
  user: User;
157
218
  isDemo: boolean;
219
+ /**
220
+ * The user's home region (multi-geo, design D4/R6), when the login response
221
+ * carries it. Optional and additive: a BE that does not yet return a region
222
+ * leaves this `undefined`, and the client keeps using its default base URL.
223
+ */
224
+ homeRegion?: string;
225
+ }
226
+ /**
227
+ * Result of a successful {@link AuthModule.refreshTapHubToken}: the rotated token
228
+ * pair. The rotation contract requires presenting the NEW refresh token on the
229
+ * next refresh, so callers must persist both. Demo users never reach this path —
230
+ * `refreshTapHubToken` rejects with `Refresh_DemoNotSupported` for them.
231
+ */
232
+ interface RefreshTokenResult {
233
+ taphubToken: string;
234
+ refreshToken: string;
158
235
  }
159
236
 
160
237
  interface AuthModuleDeps {
161
238
  rest: RestTransport;
162
239
  graphql: GraphQLTransport;
163
240
  graphqlUser: GraphQLTransport;
241
+ /**
242
+ * Region-routed, Auth_WrongRegion-retrying variant of `graphqlUser`, used by
243
+ * {@link AuthModule.refreshTapHubToken}. Login keeps using the raw `graphqlUser`
244
+ * because it runs its OWN wrong-region retry (`loginWithSession`); wiring the
245
+ * wrapped transport there would double-retry. Optional: falls back to
246
+ * `graphqlUser` (unwrapped) when not provided.
247
+ */
248
+ graphqlUserData?: GraphQLTransport;
164
249
  setToken: (token: string | null, opts?: {
165
250
  isDemo?: boolean;
251
+ homeRegion?: string;
166
252
  }) => void;
253
+ /**
254
+ * Whether the current session is a demo user. Demo users have no refresh path
255
+ * (see design D2/D4/D7), so {@link AuthModule.refreshTapHubToken} short-circuits
256
+ * for them instead of hitting the network. Optional: defaults to a non-demo
257
+ * session (`() => false`).
258
+ */
259
+ isDemo?: () => boolean;
167
260
  agencyId: string;
261
+ /**
262
+ * Multi-geo: the client's CURRENT region (e.g. persisted from a previous login),
263
+ * sent with the login request. First join → the server registers it as the home
264
+ * region; existing user → the stored home region wins. Optional (single-region
265
+ * clients never set a region and the server falls back to its default).
266
+ */
267
+ getRegion?: () => string | null;
268
+ /**
269
+ * Multi-geo: switch the client's home region (and persist it). Called by the
270
+ * silent Auth_WrongRegion redirect — the server rejected a login on the wrong
271
+ * cluster and told us the user's home region, so we set it here and retry the
272
+ * SAME login once (the user-service base-URL thunk then re-resolves to the home
273
+ * region's domain). Kept distinct from `setToken` so a redirect never touches
274
+ * the token/isDemo state. Optional: single-region clients never wire it.
275
+ */
276
+ setRegion?: (region: string) => void;
168
277
  onLoginSuccess?: () => Promise<void>;
169
278
  onLogout?: () => void | Promise<void>;
170
279
  }
@@ -180,7 +289,32 @@ declare class AuthModule {
180
289
  }): Promise<LoginResult>;
181
290
  loginWithSession(sessionToken: string, opts?: {
182
291
  signal?: AbortSignal;
292
+ region?: string;
183
293
  }): Promise<LoginResult>;
294
+ /**
295
+ * Exchange a refresh token for a rotated (taphubToken, refreshToken) pair via
296
+ * the user-service (region-routed through `graphqlUserData`, so a user pinned
297
+ * to a non-default region refreshes against THEIR cluster — the bug this change
298
+ * fixes). On success the SDK's own token is updated and the rotated pair is
299
+ * returned so the caller can persist it (rotation contract: present the NEW
300
+ * refresh token next time).
301
+ *
302
+ * - **Demo-skips-refresh:** demo users have no refresh path and reject with
303
+ * `Refresh_DemoNotSupported` (no network call). The caller reacts by clearing
304
+ * auth / re-creating the demo user.
305
+ * - **Single-flight:** concurrent callers share one in-flight request, keyed by
306
+ * the current region. A region change mid-flight re-targets the next caller.
307
+ * - **Wrong-region backstop:** `graphqlUserData` retries once on
308
+ * `Auth_WrongRegion`, so a stale region self-corrects.
309
+ *
310
+ * Invalid/expired/reused refresh tokens surface as the transport's typed error
311
+ * (e.g. `ErrRefreshTokenInvalid`); network failures surface as
312
+ * `TaphubNetworkError`. The caller decides how to react (clear vs retry-later).
313
+ */
314
+ refreshTapHubToken(refreshToken: string, opts?: {
315
+ clientMeta?: string;
316
+ signal?: AbortSignal;
317
+ }): Promise<RefreshTokenResult>;
184
318
  logout(): Promise<void>;
185
319
  }
186
320
 
@@ -691,21 +825,39 @@ interface Constraints {
691
825
  maxCoef: number;
692
826
  }
693
827
  interface PairInfo {
694
- /** game_pairs.id — catalog identifier for this pair within a gameplay. */
828
+ /**
829
+ * game_pairs.id — the catalog identifier for this pair, always the bare
830
+ * slash-free form (e.g. `"grid-ETH-USD"`), never the agency composite.
831
+ *
832
+ * The server returns `"{agencyId}:{pairId}"` here for callers with an agency
833
+ * JWT; the SDK strips that prefix so this value is identical across auth
834
+ * modes and can be passed straight back as a `pairId` argument. The composite
835
+ * is preserved in `agencyPairId`.
836
+ */
695
837
  id: string;
696
838
  /** Trading pair symbol, e.g. "BTC/USD". */
697
839
  pair: string;
698
- /** ID of the gameplay this pair belongs to. Use as `gameplaySlug` in game.get(). */
840
+ /**
841
+ * ID of the gameplay this pair belongs to.
842
+ * @deprecated grid-api retired this field from `type Pair`; it is always `''`.
843
+ */
699
844
  gameplayId: string;
700
- /** Human-readable gameplay name. */
845
+ /**
846
+ * Human-readable gameplay name.
847
+ * @deprecated grid-api retired this field from `type Pair`; it is always `''`.
848
+ */
701
849
  gameplayName: string;
702
850
  /** Price feed source, e.g. "binance". */
703
851
  source: string;
704
852
  /**
705
- * agency_game_pairs.id — the runtime game ID for this agency.
706
- * Present only when the caller is authenticated with a valid JWT.
707
- * Use directly as `gameId` in MQTT topic `game/{gameId}/candle`.
708
- * Null when called without authentication.
853
+ * agency_game_pairs.id — the runtime game ID for this agency, shaped
854
+ * `"{agencyId}:{pairId}"`. Use directly as `gameId` in MQTT topic
855
+ * `game/{gameId}/candle`.
856
+ *
857
+ * grid-api retired the dedicated wire field and now returns this value as
858
+ * `Pair.id` instead, but only for callers with an agency JWT. Anonymous
859
+ * callers receive the bare catalog id in `id`, and this property is `null` —
860
+ * do not substitute `id` for it, that id has no MQTT topic.
709
861
  */
710
862
  agencyPairId: string | null;
711
863
  }
@@ -740,8 +892,11 @@ declare class PairModule {
740
892
  /**
741
893
  * Returns available game pairs, optionally filtered by gameplay.
742
894
  *
743
- * When called with a valid JWT (authenticated builder), each entry includes
744
- * `gameId` use it directly as the MQTT topic `game/{gameId}/candle`.
895
+ * Each entry's `id` is the bare catalog pair id, safe to pass straight back
896
+ * as a `pairId` argument. When called with a valid JWT (authenticated
897
+ * builder), `agencyPairId` additionally carries the agency composite — use
898
+ * THAT one as the MQTT topic `game/{gameId}/candle`. It is `null` for
899
+ * anonymous callers.
745
900
  *
746
901
  * @example
747
902
  * const pairs = await client.pair.availableGamePairs({ gameplayId: 'taptrading' });
@@ -834,7 +989,20 @@ interface MqttWireWalletBalance {
834
989
  currency: string;
835
990
  reason: string;
836
991
  }
837
- type MqttWirePayload = MqttWireCandle | MqttWireBidResult | MqttWireBalanceUpdate | MqttWireConfig | MqttWireAgencyPairStats | MqttWireWalletBalance;
992
+ /**
993
+ * Wire payload on the user-scoped migration topic `user/{userId}/migration`,
994
+ * published by `taphub-user-service` when a home-region migration completes
995
+ * (ux-260730-migration-complete-notify). camelCase on the wire; `completedAt`
996
+ * is unix epoch SECONDS.
997
+ */
998
+ interface MqttWireMigrationCompleted {
999
+ type: 'migration_completed';
1000
+ migrationId: string;
1001
+ fromRegion: string;
1002
+ toRegion: string;
1003
+ completedAt: number;
1004
+ }
1005
+ type MqttWirePayload = MqttWireCandle | MqttWireBidResult | MqttWireBalanceUpdate | MqttWireConfig | MqttWireAgencyPairStats | MqttWireWalletBalance | MqttWireMigrationCompleted;
838
1006
  type MqttMessageHandler = (gameId: string, topic: string, payload: MqttWirePayload) => void;
839
1007
  type MqttErrorHandler = (err: Error) => void;
840
1008
  type MqttLifecycleEvent = {
@@ -857,6 +1025,7 @@ type MqttCandleHandler = (topic: string, payload: MqttWirePayload) => void;
857
1025
  type MqttAgencyPairStatsHandler = (topic: string, payload: MqttWirePayload) => void;
858
1026
  type MqttWalletMessageHandler = (topic: string, payload: MqttWirePayload) => void;
859
1027
  type MqttUserBidsMessageHandler = (topic: string, payload: MqttWirePayload) => void;
1028
+ type MqttMigrationMessageHandler = (topic: string, payload: MqttWirePayload) => void;
860
1029
  interface MqttTransport {
861
1030
  /**
862
1031
  * Subscribe to a game's MQTT topics (config, ideal_config, bid_result, balance)
@@ -910,6 +1079,25 @@ interface MqttTransport {
910
1079
  subscribeUserBids(userId: string, onMessage: MqttUserBidsMessageHandler, onError: MqttErrorHandler): void;
911
1080
  /** Tear down the cross-pair user-bids subscription for a `userId`. No-op if absent. */
912
1081
  unsubscribeUserBids(userId: string): void;
1082
+ /**
1083
+ * Subscribe to the user-scoped migration completion stream, topic
1084
+ * `user/{userId}/migration` (ux-260730). Independent of any game subscription —
1085
+ * NOT torn down by `unsubscribeAll`. Idempotent per `userId`. Subscribed at
1086
+ * QoS 1 to match the publisher (redelivery on reconnect beats silent loss);
1087
+ * the publisher also retains the message so a late subscriber still sees it.
1088
+ */
1089
+ subscribeMigration(userId: string, onMessage: MqttMigrationMessageHandler, onError: MqttErrorHandler): void;
1090
+ /** Tear down the migration subscription for a `userId`. No-op if absent. */
1091
+ unsubscribeMigration(userId: string): void;
1092
+ /**
1093
+ * Reconnect the underlying MQTT socket, re-resolving the endpoint (multi-geo:
1094
+ * the endpoint may be a thunk that now points at a different region's broker).
1095
+ * All tracked subscriptions are preserved and re-declared on the fresh
1096
+ * connection's `connect` event. No-op when not currently connected — the next
1097
+ * `ensureConnected` (triggered by any subscribe) already resolves the fresh
1098
+ * endpoint.
1099
+ */
1100
+ reconnect(): void;
913
1101
  close(): void;
914
1102
  }
915
1103
 
@@ -1045,6 +1233,22 @@ interface MqttIdealConfigEvent {
1045
1233
  currentPrice: number;
1046
1234
  reason: string;
1047
1235
  }
1236
+ /**
1237
+ * Home-region migration completion event from the user-scoped MQTT topic
1238
+ * `user/{userId}/migration` (ux-260730-migration-complete-notify). Published by
1239
+ * taphub-user-service (QoS 1, retained with TTL) when the migration saga reaches
1240
+ * COMPLETED — by then the user's home region already points at `toRegion`.
1241
+ * Consumers match `toRegion` against their pending migration target and then
1242
+ * complete the cutover via `TaphubClient.completeRegionMigration(toRegion)`.
1243
+ */
1244
+ interface MigrationCompletedEvent {
1245
+ type: 'migration_completed';
1246
+ migrationId: string;
1247
+ fromRegion: string;
1248
+ toRegion: string;
1249
+ /** Unix epoch **seconds**. */
1250
+ completedAt: number;
1251
+ }
1048
1252
  /**
1049
1253
  * Live per-pair stats tick from the public stream
1050
1254
  * `public/agency/{aid}/pair/{pairId}/stats`. The backend suppresses
@@ -1075,6 +1279,22 @@ declare class GameChannel extends EventEmitter<GameChannelEvents> {
1075
1279
  constructor(gameId: string);
1076
1280
  }
1077
1281
 
1282
+ interface MigrationChannelEvents {
1283
+ migrationCompleted: [MigrationCompletedEvent];
1284
+ error: [Error];
1285
+ }
1286
+ /**
1287
+ * User-scoped migration channel (ux-260730). Emits `migrationCompleted` when the
1288
+ * user's home-region migration finishes, from `user/{userId}/migration`. Mirror
1289
+ * of {@link WalletChannel}: keyed by `userId`, independent of any game
1290
+ * subscription. Intended to be held only while the client is in the migrating
1291
+ * state (subscribe on trigger/resume, unsubscribe on completion/logout).
1292
+ */
1293
+ declare class MigrationChannel extends EventEmitter<MigrationChannelEvents> {
1294
+ readonly userId: string;
1295
+ constructor(userId: string);
1296
+ }
1297
+
1078
1298
  interface UserBidsChannelEvents {
1079
1299
  bidWon: [MqttBidWonEvent];
1080
1300
  bidLost: [MqttBidLostEvent];
@@ -1108,7 +1328,12 @@ declare class WalletChannel extends EventEmitter<WalletChannelEvents> {
1108
1328
  }
1109
1329
 
1110
1330
  interface RealtimeModuleOptions {
1111
- mqttEndpoint: string;
1331
+ /**
1332
+ * MQTT broker endpoint. May be a thunk (multi-geo, design D4): resolved at
1333
+ * connect time so {@link RealtimeModule.reconnect} can re-target the broker to
1334
+ * the user's home-region endpoint after a login region switch.
1335
+ */
1336
+ mqttEndpoint: string | (() => string);
1112
1337
  /**
1113
1338
  * The client's own agency id. Threaded so `subscribeAgencyPairStats` can
1114
1339
  * scope the stats topic to this agency without accepting a target agency from
@@ -1179,6 +1404,23 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
1179
1404
  * down the wildcard topic and removes the channel only at zero. No-op if absent.
1180
1405
  */
1181
1406
  unsubscribeUserBids(userId: string): void;
1407
+ /**
1408
+ * Subscribe to the user-scoped migration completion stream for `userId`,
1409
+ * topic `user/{userId}/migration` (ux-260730). Returns a `MigrationChannel`
1410
+ * that emits `migrationCompleted` when the user's home-region migration
1411
+ * finishes. Malformed or schema-mismatched payloads are dropped silently.
1412
+ *
1413
+ * Reference-counted by `userId`, mirroring `subscribeWallet`. Intended
1414
+ * lifecycle (design D7): hold the channel only while the client is in the
1415
+ * migrating state; unsubscribe on completion/logout. The publisher retains
1416
+ * the completion message, so subscribing after the fact still delivers it.
1417
+ */
1418
+ subscribeMigration(userId: string): MigrationChannel;
1419
+ /**
1420
+ * Decrement the migration subscription refcount for `userId`. Tears down the
1421
+ * MQTT topic and removes the channel only at zero. No-op if absent.
1422
+ */
1423
+ unsubscribeMigration(userId: string): void;
1182
1424
  /**
1183
1425
  * Subscribe to public market candle data for a pair, keyed by pairId
1184
1426
  * (the #2 game_pairs.id, e.g. "grid-ETH-USD"). Independent of game/agency —
@@ -1195,6 +1437,13 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
1195
1437
  subscribeAgencyPairStats(pairId: string, handler: (event: MqttAgencyPairStatsEvent) => void): void;
1196
1438
  /** Unsubscribe from the per-pair stats stream for the client's own agency. */
1197
1439
  unsubscribeAgencyPairStats(pairId: string): void;
1440
+ /**
1441
+ * Reconnect the MQTT transport, re-resolving its (possibly region-scoped)
1442
+ * endpoint. All active subscriptions are preserved and re-declared on the new
1443
+ * connection (multi-geo home-region switch). No-op when not currently
1444
+ * connected. Delegates to the transport's `reconnect`.
1445
+ */
1446
+ reconnect(): void;
1198
1447
  disconnect(): void;
1199
1448
  }
1200
1449
 
@@ -1256,6 +1505,17 @@ declare class UserModule {
1256
1505
  }): Promise<UserPnL>;
1257
1506
  wallets(opts?: RequestOpts): Promise<Wallet[]>;
1258
1507
  walletByCurrency(currency: string, opts?: RequestOpts): Promise<Wallet>;
1508
+ /**
1509
+ * Start migrating the authenticated user's home region (multi-geo Phase 2).
1510
+ * User-service validates the JWT, checks the feature flag, and forwards to the
1511
+ * region directory (cooldown / single-flight / region-set validation happen
1512
+ * server-side). Typed rejections keep their `extensions.code`
1513
+ * (Migration_Unavailable, Migration_CooldownActive, …) on the error's `code`
1514
+ * so hosts can localise them.
1515
+ */
1516
+ requestRegionMigration(toRegion: string, opts?: RequestOpts): Promise<{
1517
+ migrationId: string;
1518
+ }>;
1259
1519
  get currencies(): Currency[] | null;
1260
1520
  refreshCurrencies(): Promise<Currency[]>;
1261
1521
  clearCurrencies(): void;
@@ -1338,8 +1598,29 @@ declare class TaphubClient {
1338
1598
  private bus;
1339
1599
  constructor(config: TaphubClientConfig);
1340
1600
  getToken(): string | null;
1601
+ /** The user's home region, once resolved from a login response. */
1602
+ getRegion(): string | null;
1603
+ /**
1604
+ * Complete a home-region migration by cutting the client over to `toRegion`
1605
+ * (ux-260730-migration-complete-notify). Intended to be called after a
1606
+ * `migrationCompleted` event (realtime `subscribeMigration`) or an equivalent
1607
+ * poll result confirmed the migration finished — by then the server-side home
1608
+ * region already points at `toRegion`.
1609
+ *
1610
+ * Purpose-named public wrapper over the private `#setRegion` primitive (the
1611
+ * generic region setter stays private so third-party builders cannot
1612
+ * arbitrarily re-target regions). Persists the region and re-targets the
1613
+ * GQL/REST transports; MQTT reconnects only when the resolved broker endpoint
1614
+ * actually changes. Region validation is lenient, matching `region.ts`: an
1615
+ * unknown region is stored as-is and resolves to the default region's
1616
+ * endpoints (enum-fallback, never throws). Idempotent for the already-current
1617
+ * region (no reconnect), and a safe endpoint-level no-op when no region
1618
+ * domain/MQTT maps are configured.
1619
+ */
1620
+ completeRegionMigration(toRegion: KnownRegion | (string & {})): void;
1341
1621
  setToken(token: string | null, opts?: {
1342
1622
  isDemo?: boolean;
1623
+ homeRegion?: string;
1343
1624
  }): void;
1344
1625
  isDemo(): boolean;
1345
1626
  /** @internal Used by module integrations to access the REST transport. Not part of the public API. */
@@ -1350,6 +1631,73 @@ declare class TaphubClient {
1350
1631
  off<K extends keyof TaphubEventMap & string>(event: K, handler: (payload: TaphubEventMap[K]) => void): void;
1351
1632
  }
1352
1633
 
1634
+ /**
1635
+ * Nearest-region latency probe (multi-geo, Solution 3 — first-join geo routing).
1636
+ *
1637
+ * The browser is the only vantage point that can measure real per-region latency
1638
+ * (it holds a network path to every region domain). This module runs one cheap,
1639
+ * parallel round of requests and proposes the **first region to answer with a
1640
+ * valid body** — an approximate lowest-latency signal, which is sufficient because
1641
+ * exact nearness is a non-goal (a fast-responding region is what we want, not the
1642
+ * provably-nearest one).
1643
+ *
1644
+ * What it measures: the `{serverTime}` query on each region's grid-api GraphQL
1645
+ * endpoint — the cheapest call that proves the request reached that region's
1646
+ * backend rather than an edge in front of it. The winner must return a PARSEABLE
1647
+ * body carrying a numeric `serverTime`, not merely respond first: an edge
1648
+ * rejection (404 page, WAF challenge, gateway error) comes back in milliseconds
1649
+ * from the nearest PoP and would otherwise beat every genuine round trip and win
1650
+ * the race.
1651
+ *
1652
+ * The result is only ever a *hint*: the caller sends it to the builder backend
1653
+ * (`taptrading-api`) inside `clientMeta.preferredRegion`, where it is validated
1654
+ * against the deployed-region set before it can influence the user's home-region
1655
+ * pin. This module never decides anything on its own and never throws — geo must
1656
+ * not be able to fail a login.
1657
+ */
1658
+
1659
+ /** Stable storage key for the throttle cache. Not agency-scoped — the nearest
1660
+ * region is a property of the user's network geography, the same for every agency. */
1661
+ declare const REGION_PROBE_CACHE_KEY = "taphub:region-probe";
1662
+ /** Default throttle window — re-probe at most once per hour (config overrides it). */
1663
+ declare const DEFAULT_PROBE_TTL_SECONDS = 3600;
1664
+ interface ProbeNearestRegionOptions {
1665
+ /** Fetch implementation (injectable for tests / non-browser hosts). */
1666
+ fetch?: typeof globalThis.fetch;
1667
+ /** Storage for the throttle cache (defaults to the SDK's auto-detected storage). */
1668
+ storage?: TaphubStorageAdapter;
1669
+ /** Throttle window in seconds — a cached result younger than this skips the probe. */
1670
+ ttlSeconds?: number;
1671
+ /** Overall cap for one probe round; nothing responding in time → `null`. */
1672
+ timeoutMs?: number;
1673
+ /** Probe path appended to each region base URL. */
1674
+ probePath?: string;
1675
+ /** Clock, injectable for deterministic cache-expiry tests. */
1676
+ now?: () => number;
1677
+ /** Cache key override (tests / multi-tenant hosts). */
1678
+ cacheKey?: string;
1679
+ }
1680
+ /**
1681
+ * Probe the configured regions and return the fastest-responding one, or `null`.
1682
+ *
1683
+ * Behaviour:
1684
+ * - Fewer than two configured regions → `null` (nothing to choose; the caller
1685
+ * sends no hint and the server uses its default region).
1686
+ * - A fresh cached result (younger than `ttlSeconds`) is reused without any
1687
+ * network probe — this throttles repeated app boots / login-modal opens by an
1688
+ * un-pinned user to at most one probe round per TTL window.
1689
+ * - Otherwise one parallel round of `POST <domain><probePath>` runs, each request
1690
+ * carrying the `{serverTime}` query; the first region to return a parseable body
1691
+ * with a numeric `serverTime` wins and is cached. A region that answers fast but
1692
+ * unparseably (an edge 404/error page) loses the race rather than winning it.
1693
+ * The response body has to be readable, so this is a normal CORS request — the
1694
+ * region endpoints must allow the app origin (they already do: the app posts to
1695
+ * this same endpoint).
1696
+ * - Any failure — all endpoints erroring, a timeout, a thrown fetch, a storage
1697
+ * error — resolves to `null`. This function never rejects.
1698
+ */
1699
+ declare function probeNearestRegion(domains: RegionDomainMap | undefined, opts?: ProbeNearestRegionOptions): Promise<string | null>;
1700
+
1353
1701
  declare class TaphubError extends Error {
1354
1702
  readonly code: string;
1355
1703
  readonly details?: unknown;
@@ -1500,4 +1848,4 @@ declare function computeBaseline(candleClose: number, candleTimeSec: number, cel
1500
1848
  baselineTime: number;
1501
1849
  };
1502
1850
 
1503
- export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, CANDLE_EVENT, type CancelBidResult, type Candle, type CandleEventType, type CoefficientInput, type Constraints, type Currency, DEFAULT_CHART_HISTORY_LIMIT, type ErrorMessageCatalog, type ErrorMessageMeta, type ErrorMessagesResponse, GameChannel, type GameConfig, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttAuthConfig, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type MyRank, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type Pair, type PairInfo, PairModule, type PlaceBidInput, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type Sample, type SampleReason, type SignalSource, type SortDir, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserBidsChannel, type UserBidsChannelEvents, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, type WalletBalanceReason, WalletChannel, adaptiveSimpson, autoDetectStorage, calculateCoefficientWrapper, calculateProbHit, calculateProbWin, calculateProbWin_v2, computeBaseline, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, normaliseLang, pairIdFromBidResultTopic, roundCoefToSignificantDigits };
1851
+ export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, CANDLE_EVENT, type CancelBidResult, type Candle, type CandleEventType, type CoefficientInput, type Constraints, type Currency, DEFAULT_CHART_HISTORY_LIMIT, DEFAULT_PROBE_TTL_SECONDS, DEFAULT_REGION, type ErrorMessageCatalog, type ErrorMessageMeta, type ErrorMessagesResponse, GameChannel, type GameConfig, type GridConfig, KNOWN_REGIONS, type KnownRegion, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, MigrationChannel, type MigrationChannelEvents, type MigrationCompletedEvent, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttAuthConfig, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type MyRank, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type Pair, type PairInfo, PairModule, type PlaceBidInput, type ProbeNearestRegionOptions, REGION_PROBE_CACHE_KEY, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type RefreshTokenResult, type RegionDomainMap, type Sample, type SampleReason, type SignalSource, type SortDir, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserBidsChannel, type UserBidsChannelEvents, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, type WalletBalanceReason, WalletChannel, adaptiveSimpson, autoDetectStorage, calculateCoefficientWrapper, calculateProbHit, calculateProbWin, calculateProbWin_v2, computeBaseline, errorFunction, isCancelled, isKnownRegion, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, normaliseLang, pairIdFromBidResultTopic, probeNearestRegion, resolveRegionBaseUrl, roundCoefToSignificantDigits };