@taphubhq/sdk-core 0.25.3 → 0.25.5

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
@@ -33,6 +33,8 @@ __export(index_exports, {
33
33
  AgencyPairModule: () => AgencyPairModule,
34
34
  AuthModule: () => AuthModule,
35
35
  BidModule: () => BidModule,
36
+ CANDLE_EVENT: () => CANDLE_EVENT,
37
+ DEFAULT_CHART_HISTORY_LIMIT: () => DEFAULT_CHART_HISTORY_LIMIT,
36
38
  LeaderboardModule: () => LeaderboardModule,
37
39
  LocaleModule: () => LocaleModule,
38
40
  NetworkQualityMonitor: () => NetworkQualityMonitor,
@@ -60,6 +62,7 @@ __export(index_exports, {
60
62
  isWin: () => isWin,
61
63
  normalCDF: () => normalCDF,
62
64
  normalPDF: () => normalPDF,
65
+ normaliseLang: () => normaliseLang,
63
66
  pairIdFromBidResultTopic: () => pairIdFromBidResultTopic
64
67
  });
65
68
  module.exports = __toCommonJS(index_exports);
@@ -781,6 +784,39 @@ var LeaderboardModule = class {
781
784
  }
782
785
  };
783
786
 
787
+ // src/modules/locale/errorMessages.ts
788
+ function normaliseLang(tag) {
789
+ const clean = (tag || "").trim().toLowerCase();
790
+ if (!clean) return "en";
791
+ if (clean === "zh" || clean.startsWith("zh-")) {
792
+ if (/(^|-)(hant|tw|hk|mo)(-|$)/.test(clean)) return "zh-TW";
793
+ return "zh-CN";
794
+ }
795
+ return clean.split("-")[0] || "en";
796
+ }
797
+ function pickStringEntries(value) {
798
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
799
+ const out = {};
800
+ for (const [key, entry] of Object.entries(value)) {
801
+ if (typeof entry === "string") out[key] = entry;
802
+ }
803
+ return out;
804
+ }
805
+ function interpolate(template, meta) {
806
+ const filled = template.replace(/\{(\w+)\}/g, (_match, key) => {
807
+ const value = meta?.[key];
808
+ if (value === void 0 || value === null) return "";
809
+ return String(value);
810
+ });
811
+ return filled.replace(/\s{2,}/g, " ").trim();
812
+ }
813
+ function resolveErrorMessage(code, catalog, fallback, meta) {
814
+ const row = catalog?.[code];
815
+ if (typeof row !== "string" || row.trim() === "") return fallback;
816
+ const filled = interpolate(row, meta);
817
+ return filled === "" ? fallback : filled;
818
+ }
819
+
784
820
  // src/modules/locale/normalise.ts
785
821
  function normaliseLocaleResponse(node) {
786
822
  let translations = null;
@@ -805,6 +841,14 @@ function normaliseLocaleResponse(node) {
805
841
  translations
806
842
  };
807
843
  }
844
+ function normaliseErrorMessagesResponse(node) {
845
+ return {
846
+ lang: node.lang,
847
+ version: node.version,
848
+ notModified: node.notModified,
849
+ messages: node.messages === null ? null : pickStringEntries(node.messages)
850
+ };
851
+ }
808
852
  function normaliseRefreshLocalesPayload(node) {
809
853
  let versions;
810
854
  try {
@@ -838,10 +882,33 @@ var REFRESH_LOCALES_MUTATION = `mutation RefreshLocales {
838
882
  versions
839
883
  }
840
884
  }`;
885
+ var ERROR_MESSAGES_QUERY = `query ErrorMessages($input: LocalesInput!) {
886
+ errorMessages(input: $input) {
887
+ lang
888
+ version
889
+ notModified
890
+ messages
891
+ }
892
+ }`;
841
893
 
842
894
  // src/modules/locale/index.ts
843
895
  var LocaleModule = class {
844
896
  #graphql;
897
+ /**
898
+ * The loaded error-message catalog. Instance state, not module state, so two
899
+ * clients in one process (tests, multi-tenant hosts) never share a catalog.
900
+ *
901
+ * Null until a load succeeds. It is NEVER reset to null afterwards — a failed
902
+ * refresh keeps the last-known-good copy, because losing all error copy is
903
+ * strictly worse than showing slightly stale copy.
904
+ */
905
+ #errorMessages = null;
906
+ /**
907
+ * In-flight catalog loads, keyed by normalised language. Collapses concurrent
908
+ * callers onto one request; entries are removed as soon as a load settles, so
909
+ * this never becomes a cache of results (or of failures).
910
+ */
911
+ #errorMessagesInFlight = /* @__PURE__ */ new Map();
845
912
  constructor(deps) {
846
913
  this.#graphql = deps.graphql;
847
914
  }
@@ -864,6 +931,94 @@ var LocaleModule = class {
864
931
  const body = await this.#graphql.request(LOCALES_QUERY, { input }, opts);
865
932
  return normaliseLocaleResponse(body.locales);
866
933
  }
934
+ /**
935
+ * Fetch the error-message catalog for a single language.
936
+ *
937
+ * The throwing sibling of {@link loadErrorMessages}: errors surface as
938
+ * TaphubError subclasses with backend `extensions.code` values
939
+ * (`LangNotSupported`, `FeatureDisabled`, …), because a caller doing an
940
+ * explicit fetch wants to see why it failed. Hosts that just want copy on
941
+ * screen should call `loadErrorMessages` instead.
942
+ *
943
+ * `lang` is passed through as given — normalisation is `loadErrorMessages`'
944
+ * job, so an explicit caller keeps full control of the tag.
945
+ */
946
+ async getErrorMessages(lang, knownVersion, opts) {
947
+ const input = { lang };
948
+ if (knownVersion !== void 0 && knownVersion !== "") {
949
+ input.knownVersion = knownVersion;
950
+ }
951
+ const body = await this.#graphql.request(
952
+ ERROR_MESSAGES_QUERY,
953
+ { input },
954
+ opts
955
+ );
956
+ return normaliseErrorMessagesResponse(body.errorMessages);
957
+ }
958
+ /**
959
+ * Load the error-message catalog for `lang` into this client.
960
+ *
961
+ * Background hydration with nobody waiting on it, so it **never throws and
962
+ * never rejects**. Every failure path — network error, backend down,
963
+ * `FeatureDisabled` when the source sheet is not configured, an unsupported
964
+ * tag, a malformed body — leaves the previously loaded catalog in place.
965
+ * Failing to translate an error must not itself become an error.
966
+ *
967
+ * The tag is normalised first (region dropped, Chinese scripts kept apart),
968
+ * so a host can hand over `navigator.language` untouched.
969
+ *
970
+ * Concurrent calls for the same language share one request. The catalog lives
971
+ * on the client while the thing that triggers a load (a mounted provider)
972
+ * lives a level below, so two providers sharing one client would otherwise
973
+ * fetch the same bytes into the same place twice.
974
+ *
975
+ * This is dedup, not caching — it lasts only while a request is in flight. A
976
+ * host calling again after one settles gets a fresh fetch.
977
+ *
978
+ * Deliberately takes NO AbortSignal. A follower piggybacking on someone
979
+ * else's request would otherwise have its outcome decided by whether that
980
+ * other caller happened to unmount first. Letting a background hydration run
981
+ * to completion costs one small request; stranding a caller costs all the
982
+ * error copy on screen.
983
+ *
984
+ * No persistence and no polling: the host decides when to call this. The
985
+ * React provider calls it on mount and on language change.
986
+ */
987
+ async loadErrorMessages(lang) {
988
+ const key = normaliseLang(lang);
989
+ const inFlight = this.#errorMessagesInFlight.get(key);
990
+ if (inFlight) return inFlight;
991
+ const run = (async () => {
992
+ try {
993
+ const res = await this.getErrorMessages(key);
994
+ if (res.notModified || res.messages === null) return;
995
+ if (Object.keys(res.messages).length === 0) return;
996
+ this.#errorMessages = res.messages;
997
+ } catch {
998
+ } finally {
999
+ this.#errorMessagesInFlight.delete(key);
1000
+ }
1001
+ })();
1002
+ this.#errorMessagesInFlight.set(key, run);
1003
+ return run;
1004
+ }
1005
+ /**
1006
+ * The sentence to show for a backend error `code`.
1007
+ *
1008
+ * **Synchronous by design** — error copy is read when a toast is built, inside
1009
+ * a `catch` or an error handler, where React hooks are illegal. That is why
1010
+ * this is a plain method and not a hook.
1011
+ *
1012
+ * Returns `fallback` whenever the catalog cannot produce a usable sentence: no
1013
+ * catalog loaded yet, no row for the code, or a blank row. Never returns an
1014
+ * empty string, and never returns the raw code.
1015
+ *
1016
+ * `meta` fills `{name}` placeholders in a catalog row; any placeholder left
1017
+ * unfilled is stripped, so raw braces never reach the user.
1018
+ */
1019
+ errorMessage(code, fallback, meta) {
1020
+ return resolveErrorMessage(code, this.#errorMessages, fallback, meta);
1021
+ }
867
1022
  /**
868
1023
  * Trigger an admin refresh — backend re-pulls the source Sheet, invalidates
869
1024
  * its cache, and writes fresh entries for every supported language.
@@ -994,6 +1149,9 @@ var BUILDER_AVAILABLE_GAME_PAIRS_QUERY = `query BuilderAvailableGamePairs($gamep
994
1149
  }
995
1150
  }`;
996
1151
 
1152
+ // src/modules/pair/types.ts
1153
+ var DEFAULT_CHART_HISTORY_LIMIT = 600;
1154
+
997
1155
  // src/modules/pair/index.ts
998
1156
  var PairModule = class {
999
1157
  #graphql;
@@ -1051,11 +1209,8 @@ var PairModule = class {
1051
1209
  return body.builderAvailableGamePairs.map(normalisePairInfo);
1052
1210
  }
1053
1211
  // REVIEW[bid-260602-v2]: chartHistory param/variable renamed pairId (was: agencyPairId/gameId)
1054
- async chartHistory(pairId, limit, opts) {
1055
- const variables = { pairId };
1056
- if (limit !== void 0) {
1057
- variables.limit = limit;
1058
- }
1212
+ async chartHistory(pairId, limit = DEFAULT_CHART_HISTORY_LIMIT, opts) {
1213
+ const variables = { pairId, limit };
1059
1214
  const body = await this.#graphql.request(
1060
1215
  CHART_HISTORY_QUERY,
1061
1216
  variables,
@@ -3006,6 +3161,12 @@ var TaphubClient = class {
3006
3161
  }
3007
3162
  };
3008
3163
 
3164
+ // src/modules/realtime/types.ts
3165
+ var CANDLE_EVENT = {
3166
+ NEW: "new",
3167
+ UPDATE: "update"
3168
+ };
3169
+
3009
3170
  // src/utils/coefficient.ts
3010
3171
  function errorFunction(x) {
3011
3172
  const a1 = 0.254829592;
@@ -3095,6 +3256,8 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
3095
3256
  AgencyPairModule,
3096
3257
  AuthModule,
3097
3258
  BidModule,
3259
+ CANDLE_EVENT,
3260
+ DEFAULT_CHART_HISTORY_LIMIT,
3098
3261
  LeaderboardModule,
3099
3262
  LocaleModule,
3100
3263
  NetworkQualityMonitor,
@@ -3122,5 +3285,6 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
3122
3285
  isWin,
3123
3286
  normalCDF,
3124
3287
  normalPDF,
3288
+ normaliseLang,
3125
3289
  pairIdFromBidResultTopic
3126
3290
  });
package/dist/index.d.mts CHANGED
@@ -454,6 +454,57 @@ interface LocaleRefreshResult {
454
454
  refreshed: string[];
455
455
  versions: Record<string, string>;
456
456
  }
457
+ /**
458
+ * Flat error-message catalog for a single language: backend error code → sentence.
459
+ *
460
+ * Keys are the `extensions.code` values the backend returns on a rejection
461
+ * (`Bid_RateLimited`, `Wallet_WithdrawNetworkUnavailable`, …). Values may contain
462
+ * `{name}` placeholders filled from the error's meta at lookup time.
463
+ */
464
+ type ErrorMessageCatalog = Record<string, string>;
465
+ /**
466
+ * Values available to fill a catalog row's `{name}` placeholders — normally the
467
+ * `meta` carried by the backend error. Values are stringified as-is; a caller who
468
+ * needs specific formatting (decimal places, currency) should format before passing.
469
+ */
470
+ type ErrorMessageMeta = Record<string, unknown>;
471
+ /**
472
+ * Public response shape from LocaleModule.getErrorMessages().
473
+ *
474
+ * Mirrors {@link LocaleResponse}, with one wire-level difference: `messages`
475
+ * arrives as a real JSON object rather than a JSON-string, so there is no parse
476
+ * step. Non-string values are dropped during normalisation.
477
+ *
478
+ * `messages` is null when `notModified` is true (caller keeps prior state).
479
+ */
480
+ interface ErrorMessagesResponse {
481
+ lang: string;
482
+ version: string;
483
+ notModified: boolean;
484
+ messages: ErrorMessageCatalog | null;
485
+ }
486
+
487
+ /**
488
+ * Pure helpers behind the error-message catalog.
489
+ *
490
+ * Kept free of I/O and of module state so the precedence rule, the language
491
+ * mapping and the template filling are all testable without a transport.
492
+ */
493
+
494
+ /**
495
+ * Reduce a host language tag to the catalog's column name.
496
+ *
497
+ * The sheet's columns are en, vi, es, hi, zh-TW, zh-CN, ja, pt, fr, de, it, tr,
498
+ * ko, ar. Only Chinese needs more than the primary subtag: zh-TW and zh-CN are
499
+ * different SCRIPTS, so collapsing both to "zh" would show Simplified text to a
500
+ * Traditional reader. Everything else drops its region.
501
+ *
502
+ * The supported-language list is deliberately NOT duplicated here — the server
503
+ * owns it, and an unsupported tag simply fails the fetch and leaves prior copy
504
+ * in place, which is the same outcome a stale local list would produce with
505
+ * more code.
506
+ */
507
+ declare function normaliseLang(tag: string): string;
457
508
 
458
509
  interface LocaleModuleDeps {
459
510
  graphql: GraphQLTransport;
@@ -484,6 +535,66 @@ declare class LocaleModule {
484
535
  get(lang: string, knownVersion?: string, opts?: {
485
536
  signal?: AbortSignal;
486
537
  }): Promise<LocaleResponse>;
538
+ /**
539
+ * Fetch the error-message catalog for a single language.
540
+ *
541
+ * The throwing sibling of {@link loadErrorMessages}: errors surface as
542
+ * TaphubError subclasses with backend `extensions.code` values
543
+ * (`LangNotSupported`, `FeatureDisabled`, …), because a caller doing an
544
+ * explicit fetch wants to see why it failed. Hosts that just want copy on
545
+ * screen should call `loadErrorMessages` instead.
546
+ *
547
+ * `lang` is passed through as given — normalisation is `loadErrorMessages`'
548
+ * job, so an explicit caller keeps full control of the tag.
549
+ */
550
+ getErrorMessages(lang: string, knownVersion?: string, opts?: {
551
+ signal?: AbortSignal;
552
+ }): Promise<ErrorMessagesResponse>;
553
+ /**
554
+ * Load the error-message catalog for `lang` into this client.
555
+ *
556
+ * Background hydration with nobody waiting on it, so it **never throws and
557
+ * never rejects**. Every failure path — network error, backend down,
558
+ * `FeatureDisabled` when the source sheet is not configured, an unsupported
559
+ * tag, a malformed body — leaves the previously loaded catalog in place.
560
+ * Failing to translate an error must not itself become an error.
561
+ *
562
+ * The tag is normalised first (region dropped, Chinese scripts kept apart),
563
+ * so a host can hand over `navigator.language` untouched.
564
+ *
565
+ * Concurrent calls for the same language share one request. The catalog lives
566
+ * on the client while the thing that triggers a load (a mounted provider)
567
+ * lives a level below, so two providers sharing one client would otherwise
568
+ * fetch the same bytes into the same place twice.
569
+ *
570
+ * This is dedup, not caching — it lasts only while a request is in flight. A
571
+ * host calling again after one settles gets a fresh fetch.
572
+ *
573
+ * Deliberately takes NO AbortSignal. A follower piggybacking on someone
574
+ * else's request would otherwise have its outcome decided by whether that
575
+ * other caller happened to unmount first. Letting a background hydration run
576
+ * to completion costs one small request; stranding a caller costs all the
577
+ * error copy on screen.
578
+ *
579
+ * No persistence and no polling: the host decides when to call this. The
580
+ * React provider calls it on mount and on language change.
581
+ */
582
+ loadErrorMessages(lang: string): Promise<void>;
583
+ /**
584
+ * The sentence to show for a backend error `code`.
585
+ *
586
+ * **Synchronous by design** — error copy is read when a toast is built, inside
587
+ * a `catch` or an error handler, where React hooks are illegal. That is why
588
+ * this is a plain method and not a hook.
589
+ *
590
+ * Returns `fallback` whenever the catalog cannot produce a usable sentence: no
591
+ * catalog loaded yet, no row for the code, or a blank row. Never returns an
592
+ * empty string, and never returns the raw code.
593
+ *
594
+ * `meta` fills `{name}` placeholders in a catalog row; any placeholder left
595
+ * unfilled is stripped, so raw braces never reach the user.
596
+ */
597
+ errorMessage(code: string, fallback: string, meta?: ErrorMessageMeta): string;
487
598
  /**
488
599
  * Trigger an admin refresh — backend re-pulls the source Sheet, invalidates
489
600
  * its cache, and writes fresh entries for every supported language.
@@ -598,6 +709,13 @@ interface PairInfo {
598
709
  */
599
710
  agencyPairId: string | null;
600
711
  }
712
+ /**
713
+ * Default number of candles fetched on initial chart load when the caller does
714
+ * not pass an explicit limit. The server clamps any request to its cache size
715
+ * (grid-api services.ChartHistoryMaxLimit = 1500), so this is purely how much
716
+ * history the chart shows by default — kept lower than the cap for faster init.
717
+ */
718
+ declare const DEFAULT_CHART_HISTORY_LIMIT = 600;
601
719
  interface Candle {
602
720
  /** Unix epoch SECONDS (not ms). Multiply by 1000 for JS Date. */
603
721
  time: number;
@@ -795,6 +913,16 @@ interface MqttTransport {
795
913
  close(): void;
796
914
  }
797
915
 
916
+ /**
917
+ * Candle tick kind. `new` = a fresh bar opened; `update` = the current bar was
918
+ * revised. Exported as named constants so consumers compare against these
919
+ * instead of bare string literals (a typo on a literal fails silently).
920
+ */
921
+ declare const CANDLE_EVENT: {
922
+ readonly NEW: "new";
923
+ readonly UPDATE: "update";
924
+ };
925
+ type CandleEventType = (typeof CANDLE_EVENT)[keyof typeof CANDLE_EVENT];
798
926
  /**
799
927
  * Candle tick from MQTT.
800
928
  * WARNING: `time` is Unix epoch MILLISECONDS.
@@ -802,7 +930,7 @@ interface MqttTransport {
802
930
  * Do NOT multiply MQTT candle time by 1000.
803
931
  */
804
932
  interface MqttCandleEvent {
805
- type: 'new' | 'update';
933
+ type: CandleEventType;
806
934
  /** Unix epoch **milliseconds** — differs from GQL chartHistory (seconds) */
807
935
  time: number;
808
936
  o: number;
@@ -1304,4 +1432,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
1304
1432
  declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1305
1433
  declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1306
1434
 
1307
- export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, 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, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, pairIdFromBidResultTopic };
1435
+ 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 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, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, normaliseLang, pairIdFromBidResultTopic };
package/dist/index.d.ts CHANGED
@@ -454,6 +454,57 @@ interface LocaleRefreshResult {
454
454
  refreshed: string[];
455
455
  versions: Record<string, string>;
456
456
  }
457
+ /**
458
+ * Flat error-message catalog for a single language: backend error code → sentence.
459
+ *
460
+ * Keys are the `extensions.code` values the backend returns on a rejection
461
+ * (`Bid_RateLimited`, `Wallet_WithdrawNetworkUnavailable`, …). Values may contain
462
+ * `{name}` placeholders filled from the error's meta at lookup time.
463
+ */
464
+ type ErrorMessageCatalog = Record<string, string>;
465
+ /**
466
+ * Values available to fill a catalog row's `{name}` placeholders — normally the
467
+ * `meta` carried by the backend error. Values are stringified as-is; a caller who
468
+ * needs specific formatting (decimal places, currency) should format before passing.
469
+ */
470
+ type ErrorMessageMeta = Record<string, unknown>;
471
+ /**
472
+ * Public response shape from LocaleModule.getErrorMessages().
473
+ *
474
+ * Mirrors {@link LocaleResponse}, with one wire-level difference: `messages`
475
+ * arrives as a real JSON object rather than a JSON-string, so there is no parse
476
+ * step. Non-string values are dropped during normalisation.
477
+ *
478
+ * `messages` is null when `notModified` is true (caller keeps prior state).
479
+ */
480
+ interface ErrorMessagesResponse {
481
+ lang: string;
482
+ version: string;
483
+ notModified: boolean;
484
+ messages: ErrorMessageCatalog | null;
485
+ }
486
+
487
+ /**
488
+ * Pure helpers behind the error-message catalog.
489
+ *
490
+ * Kept free of I/O and of module state so the precedence rule, the language
491
+ * mapping and the template filling are all testable without a transport.
492
+ */
493
+
494
+ /**
495
+ * Reduce a host language tag to the catalog's column name.
496
+ *
497
+ * The sheet's columns are en, vi, es, hi, zh-TW, zh-CN, ja, pt, fr, de, it, tr,
498
+ * ko, ar. Only Chinese needs more than the primary subtag: zh-TW and zh-CN are
499
+ * different SCRIPTS, so collapsing both to "zh" would show Simplified text to a
500
+ * Traditional reader. Everything else drops its region.
501
+ *
502
+ * The supported-language list is deliberately NOT duplicated here — the server
503
+ * owns it, and an unsupported tag simply fails the fetch and leaves prior copy
504
+ * in place, which is the same outcome a stale local list would produce with
505
+ * more code.
506
+ */
507
+ declare function normaliseLang(tag: string): string;
457
508
 
458
509
  interface LocaleModuleDeps {
459
510
  graphql: GraphQLTransport;
@@ -484,6 +535,66 @@ declare class LocaleModule {
484
535
  get(lang: string, knownVersion?: string, opts?: {
485
536
  signal?: AbortSignal;
486
537
  }): Promise<LocaleResponse>;
538
+ /**
539
+ * Fetch the error-message catalog for a single language.
540
+ *
541
+ * The throwing sibling of {@link loadErrorMessages}: errors surface as
542
+ * TaphubError subclasses with backend `extensions.code` values
543
+ * (`LangNotSupported`, `FeatureDisabled`, …), because a caller doing an
544
+ * explicit fetch wants to see why it failed. Hosts that just want copy on
545
+ * screen should call `loadErrorMessages` instead.
546
+ *
547
+ * `lang` is passed through as given — normalisation is `loadErrorMessages`'
548
+ * job, so an explicit caller keeps full control of the tag.
549
+ */
550
+ getErrorMessages(lang: string, knownVersion?: string, opts?: {
551
+ signal?: AbortSignal;
552
+ }): Promise<ErrorMessagesResponse>;
553
+ /**
554
+ * Load the error-message catalog for `lang` into this client.
555
+ *
556
+ * Background hydration with nobody waiting on it, so it **never throws and
557
+ * never rejects**. Every failure path — network error, backend down,
558
+ * `FeatureDisabled` when the source sheet is not configured, an unsupported
559
+ * tag, a malformed body — leaves the previously loaded catalog in place.
560
+ * Failing to translate an error must not itself become an error.
561
+ *
562
+ * The tag is normalised first (region dropped, Chinese scripts kept apart),
563
+ * so a host can hand over `navigator.language` untouched.
564
+ *
565
+ * Concurrent calls for the same language share one request. The catalog lives
566
+ * on the client while the thing that triggers a load (a mounted provider)
567
+ * lives a level below, so two providers sharing one client would otherwise
568
+ * fetch the same bytes into the same place twice.
569
+ *
570
+ * This is dedup, not caching — it lasts only while a request is in flight. A
571
+ * host calling again after one settles gets a fresh fetch.
572
+ *
573
+ * Deliberately takes NO AbortSignal. A follower piggybacking on someone
574
+ * else's request would otherwise have its outcome decided by whether that
575
+ * other caller happened to unmount first. Letting a background hydration run
576
+ * to completion costs one small request; stranding a caller costs all the
577
+ * error copy on screen.
578
+ *
579
+ * No persistence and no polling: the host decides when to call this. The
580
+ * React provider calls it on mount and on language change.
581
+ */
582
+ loadErrorMessages(lang: string): Promise<void>;
583
+ /**
584
+ * The sentence to show for a backend error `code`.
585
+ *
586
+ * **Synchronous by design** — error copy is read when a toast is built, inside
587
+ * a `catch` or an error handler, where React hooks are illegal. That is why
588
+ * this is a plain method and not a hook.
589
+ *
590
+ * Returns `fallback` whenever the catalog cannot produce a usable sentence: no
591
+ * catalog loaded yet, no row for the code, or a blank row. Never returns an
592
+ * empty string, and never returns the raw code.
593
+ *
594
+ * `meta` fills `{name}` placeholders in a catalog row; any placeholder left
595
+ * unfilled is stripped, so raw braces never reach the user.
596
+ */
597
+ errorMessage(code: string, fallback: string, meta?: ErrorMessageMeta): string;
487
598
  /**
488
599
  * Trigger an admin refresh — backend re-pulls the source Sheet, invalidates
489
600
  * its cache, and writes fresh entries for every supported language.
@@ -598,6 +709,13 @@ interface PairInfo {
598
709
  */
599
710
  agencyPairId: string | null;
600
711
  }
712
+ /**
713
+ * Default number of candles fetched on initial chart load when the caller does
714
+ * not pass an explicit limit. The server clamps any request to its cache size
715
+ * (grid-api services.ChartHistoryMaxLimit = 1500), so this is purely how much
716
+ * history the chart shows by default — kept lower than the cap for faster init.
717
+ */
718
+ declare const DEFAULT_CHART_HISTORY_LIMIT = 600;
601
719
  interface Candle {
602
720
  /** Unix epoch SECONDS (not ms). Multiply by 1000 for JS Date. */
603
721
  time: number;
@@ -795,6 +913,16 @@ interface MqttTransport {
795
913
  close(): void;
796
914
  }
797
915
 
916
+ /**
917
+ * Candle tick kind. `new` = a fresh bar opened; `update` = the current bar was
918
+ * revised. Exported as named constants so consumers compare against these
919
+ * instead of bare string literals (a typo on a literal fails silently).
920
+ */
921
+ declare const CANDLE_EVENT: {
922
+ readonly NEW: "new";
923
+ readonly UPDATE: "update";
924
+ };
925
+ type CandleEventType = (typeof CANDLE_EVENT)[keyof typeof CANDLE_EVENT];
798
926
  /**
799
927
  * Candle tick from MQTT.
800
928
  * WARNING: `time` is Unix epoch MILLISECONDS.
@@ -802,7 +930,7 @@ interface MqttTransport {
802
930
  * Do NOT multiply MQTT candle time by 1000.
803
931
  */
804
932
  interface MqttCandleEvent {
805
- type: 'new' | 'update';
933
+ type: CandleEventType;
806
934
  /** Unix epoch **milliseconds** — differs from GQL chartHistory (seconds) */
807
935
  time: number;
808
936
  o: number;
@@ -1304,4 +1432,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
1304
1432
  declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1305
1433
  declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1306
1434
 
1307
- export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, 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, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, pairIdFromBidResultTopic };
1435
+ 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 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, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, normaliseLang, pairIdFromBidResultTopic };
package/dist/index.js CHANGED
@@ -715,6 +715,39 @@ var LeaderboardModule = class {
715
715
  }
716
716
  };
717
717
 
718
+ // src/modules/locale/errorMessages.ts
719
+ function normaliseLang(tag) {
720
+ const clean = (tag || "").trim().toLowerCase();
721
+ if (!clean) return "en";
722
+ if (clean === "zh" || clean.startsWith("zh-")) {
723
+ if (/(^|-)(hant|tw|hk|mo)(-|$)/.test(clean)) return "zh-TW";
724
+ return "zh-CN";
725
+ }
726
+ return clean.split("-")[0] || "en";
727
+ }
728
+ function pickStringEntries(value) {
729
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
730
+ const out = {};
731
+ for (const [key, entry] of Object.entries(value)) {
732
+ if (typeof entry === "string") out[key] = entry;
733
+ }
734
+ return out;
735
+ }
736
+ function interpolate(template, meta) {
737
+ const filled = template.replace(/\{(\w+)\}/g, (_match, key) => {
738
+ const value = meta?.[key];
739
+ if (value === void 0 || value === null) return "";
740
+ return String(value);
741
+ });
742
+ return filled.replace(/\s{2,}/g, " ").trim();
743
+ }
744
+ function resolveErrorMessage(code, catalog, fallback, meta) {
745
+ const row = catalog?.[code];
746
+ if (typeof row !== "string" || row.trim() === "") return fallback;
747
+ const filled = interpolate(row, meta);
748
+ return filled === "" ? fallback : filled;
749
+ }
750
+
718
751
  // src/modules/locale/normalise.ts
719
752
  function normaliseLocaleResponse(node) {
720
753
  let translations = null;
@@ -739,6 +772,14 @@ function normaliseLocaleResponse(node) {
739
772
  translations
740
773
  };
741
774
  }
775
+ function normaliseErrorMessagesResponse(node) {
776
+ return {
777
+ lang: node.lang,
778
+ version: node.version,
779
+ notModified: node.notModified,
780
+ messages: node.messages === null ? null : pickStringEntries(node.messages)
781
+ };
782
+ }
742
783
  function normaliseRefreshLocalesPayload(node) {
743
784
  let versions;
744
785
  try {
@@ -772,10 +813,33 @@ var REFRESH_LOCALES_MUTATION = `mutation RefreshLocales {
772
813
  versions
773
814
  }
774
815
  }`;
816
+ var ERROR_MESSAGES_QUERY = `query ErrorMessages($input: LocalesInput!) {
817
+ errorMessages(input: $input) {
818
+ lang
819
+ version
820
+ notModified
821
+ messages
822
+ }
823
+ }`;
775
824
 
776
825
  // src/modules/locale/index.ts
777
826
  var LocaleModule = class {
778
827
  #graphql;
828
+ /**
829
+ * The loaded error-message catalog. Instance state, not module state, so two
830
+ * clients in one process (tests, multi-tenant hosts) never share a catalog.
831
+ *
832
+ * Null until a load succeeds. It is NEVER reset to null afterwards — a failed
833
+ * refresh keeps the last-known-good copy, because losing all error copy is
834
+ * strictly worse than showing slightly stale copy.
835
+ */
836
+ #errorMessages = null;
837
+ /**
838
+ * In-flight catalog loads, keyed by normalised language. Collapses concurrent
839
+ * callers onto one request; entries are removed as soon as a load settles, so
840
+ * this never becomes a cache of results (or of failures).
841
+ */
842
+ #errorMessagesInFlight = /* @__PURE__ */ new Map();
779
843
  constructor(deps) {
780
844
  this.#graphql = deps.graphql;
781
845
  }
@@ -798,6 +862,94 @@ var LocaleModule = class {
798
862
  const body = await this.#graphql.request(LOCALES_QUERY, { input }, opts);
799
863
  return normaliseLocaleResponse(body.locales);
800
864
  }
865
+ /**
866
+ * Fetch the error-message catalog for a single language.
867
+ *
868
+ * The throwing sibling of {@link loadErrorMessages}: errors surface as
869
+ * TaphubError subclasses with backend `extensions.code` values
870
+ * (`LangNotSupported`, `FeatureDisabled`, …), because a caller doing an
871
+ * explicit fetch wants to see why it failed. Hosts that just want copy on
872
+ * screen should call `loadErrorMessages` instead.
873
+ *
874
+ * `lang` is passed through as given — normalisation is `loadErrorMessages`'
875
+ * job, so an explicit caller keeps full control of the tag.
876
+ */
877
+ async getErrorMessages(lang, knownVersion, opts) {
878
+ const input = { lang };
879
+ if (knownVersion !== void 0 && knownVersion !== "") {
880
+ input.knownVersion = knownVersion;
881
+ }
882
+ const body = await this.#graphql.request(
883
+ ERROR_MESSAGES_QUERY,
884
+ { input },
885
+ opts
886
+ );
887
+ return normaliseErrorMessagesResponse(body.errorMessages);
888
+ }
889
+ /**
890
+ * Load the error-message catalog for `lang` into this client.
891
+ *
892
+ * Background hydration with nobody waiting on it, so it **never throws and
893
+ * never rejects**. Every failure path — network error, backend down,
894
+ * `FeatureDisabled` when the source sheet is not configured, an unsupported
895
+ * tag, a malformed body — leaves the previously loaded catalog in place.
896
+ * Failing to translate an error must not itself become an error.
897
+ *
898
+ * The tag is normalised first (region dropped, Chinese scripts kept apart),
899
+ * so a host can hand over `navigator.language` untouched.
900
+ *
901
+ * Concurrent calls for the same language share one request. The catalog lives
902
+ * on the client while the thing that triggers a load (a mounted provider)
903
+ * lives a level below, so two providers sharing one client would otherwise
904
+ * fetch the same bytes into the same place twice.
905
+ *
906
+ * This is dedup, not caching — it lasts only while a request is in flight. A
907
+ * host calling again after one settles gets a fresh fetch.
908
+ *
909
+ * Deliberately takes NO AbortSignal. A follower piggybacking on someone
910
+ * else's request would otherwise have its outcome decided by whether that
911
+ * other caller happened to unmount first. Letting a background hydration run
912
+ * to completion costs one small request; stranding a caller costs all the
913
+ * error copy on screen.
914
+ *
915
+ * No persistence and no polling: the host decides when to call this. The
916
+ * React provider calls it on mount and on language change.
917
+ */
918
+ async loadErrorMessages(lang) {
919
+ const key = normaliseLang(lang);
920
+ const inFlight = this.#errorMessagesInFlight.get(key);
921
+ if (inFlight) return inFlight;
922
+ const run = (async () => {
923
+ try {
924
+ const res = await this.getErrorMessages(key);
925
+ if (res.notModified || res.messages === null) return;
926
+ if (Object.keys(res.messages).length === 0) return;
927
+ this.#errorMessages = res.messages;
928
+ } catch {
929
+ } finally {
930
+ this.#errorMessagesInFlight.delete(key);
931
+ }
932
+ })();
933
+ this.#errorMessagesInFlight.set(key, run);
934
+ return run;
935
+ }
936
+ /**
937
+ * The sentence to show for a backend error `code`.
938
+ *
939
+ * **Synchronous by design** — error copy is read when a toast is built, inside
940
+ * a `catch` or an error handler, where React hooks are illegal. That is why
941
+ * this is a plain method and not a hook.
942
+ *
943
+ * Returns `fallback` whenever the catalog cannot produce a usable sentence: no
944
+ * catalog loaded yet, no row for the code, or a blank row. Never returns an
945
+ * empty string, and never returns the raw code.
946
+ *
947
+ * `meta` fills `{name}` placeholders in a catalog row; any placeholder left
948
+ * unfilled is stripped, so raw braces never reach the user.
949
+ */
950
+ errorMessage(code, fallback, meta) {
951
+ return resolveErrorMessage(code, this.#errorMessages, fallback, meta);
952
+ }
801
953
  /**
802
954
  * Trigger an admin refresh — backend re-pulls the source Sheet, invalidates
803
955
  * its cache, and writes fresh entries for every supported language.
@@ -928,6 +1080,9 @@ var BUILDER_AVAILABLE_GAME_PAIRS_QUERY = `query BuilderAvailableGamePairs($gamep
928
1080
  }
929
1081
  }`;
930
1082
 
1083
+ // src/modules/pair/types.ts
1084
+ var DEFAULT_CHART_HISTORY_LIMIT = 600;
1085
+
931
1086
  // src/modules/pair/index.ts
932
1087
  var PairModule = class {
933
1088
  #graphql;
@@ -985,11 +1140,8 @@ var PairModule = class {
985
1140
  return body.builderAvailableGamePairs.map(normalisePairInfo);
986
1141
  }
987
1142
  // REVIEW[bid-260602-v2]: chartHistory param/variable renamed pairId (was: agencyPairId/gameId)
988
- async chartHistory(pairId, limit, opts) {
989
- const variables = { pairId };
990
- if (limit !== void 0) {
991
- variables.limit = limit;
992
- }
1143
+ async chartHistory(pairId, limit = DEFAULT_CHART_HISTORY_LIMIT, opts) {
1144
+ const variables = { pairId, limit };
993
1145
  const body = await this.#graphql.request(
994
1146
  CHART_HISTORY_QUERY,
995
1147
  variables,
@@ -2940,6 +3092,12 @@ var TaphubClient = class {
2940
3092
  }
2941
3093
  };
2942
3094
 
3095
+ // src/modules/realtime/types.ts
3096
+ var CANDLE_EVENT = {
3097
+ NEW: "new",
3098
+ UPDATE: "update"
3099
+ };
3100
+
2943
3101
  // src/utils/coefficient.ts
2944
3102
  function errorFunction(x) {
2945
3103
  const a1 = 0.254829592;
@@ -3028,6 +3186,8 @@ export {
3028
3186
  AgencyPairModule,
3029
3187
  AuthModule,
3030
3188
  BidModule,
3189
+ CANDLE_EVENT,
3190
+ DEFAULT_CHART_HISTORY_LIMIT,
3031
3191
  LeaderboardModule,
3032
3192
  LocaleModule,
3033
3193
  NetworkQualityMonitor,
@@ -3055,5 +3215,6 @@ export {
3055
3215
  isWin,
3056
3216
  normalCDF,
3057
3217
  normalPDF,
3218
+ normaliseLang,
3058
3219
  pairIdFromBidResultTopic
3059
3220
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taphubhq/sdk-core",
3
- "version": "0.25.3",
3
+ "version": "0.25.5",
4
4
  "description": "Core SDK for building on the TabHub platform",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",