@taphubhq/sdk-core 0.25.2 → 0.25.4

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
@@ -60,6 +60,7 @@ __export(index_exports, {
60
60
  isWin: () => isWin,
61
61
  normalCDF: () => normalCDF,
62
62
  normalPDF: () => normalPDF,
63
+ normaliseLang: () => normaliseLang,
63
64
  pairIdFromBidResultTopic: () => pairIdFromBidResultTopic
64
65
  });
65
66
  module.exports = __toCommonJS(index_exports);
@@ -781,6 +782,39 @@ var LeaderboardModule = class {
781
782
  }
782
783
  };
783
784
 
785
+ // src/modules/locale/errorMessages.ts
786
+ function normaliseLang(tag) {
787
+ const clean = (tag || "").trim().toLowerCase();
788
+ if (!clean) return "en";
789
+ if (clean === "zh" || clean.startsWith("zh-")) {
790
+ if (/(^|-)(hant|tw|hk|mo)(-|$)/.test(clean)) return "zh-TW";
791
+ return "zh-CN";
792
+ }
793
+ return clean.split("-")[0] || "en";
794
+ }
795
+ function pickStringEntries(value) {
796
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
797
+ const out = {};
798
+ for (const [key, entry] of Object.entries(value)) {
799
+ if (typeof entry === "string") out[key] = entry;
800
+ }
801
+ return out;
802
+ }
803
+ function interpolate(template, meta) {
804
+ const filled = template.replace(/\{(\w+)\}/g, (_match, key) => {
805
+ const value = meta?.[key];
806
+ if (value === void 0 || value === null) return "";
807
+ return String(value);
808
+ });
809
+ return filled.replace(/\s{2,}/g, " ").trim();
810
+ }
811
+ function resolveErrorMessage(code, catalog, fallback, meta) {
812
+ const row = catalog?.[code];
813
+ if (typeof row !== "string" || row.trim() === "") return fallback;
814
+ const filled = interpolate(row, meta);
815
+ return filled === "" ? fallback : filled;
816
+ }
817
+
784
818
  // src/modules/locale/normalise.ts
785
819
  function normaliseLocaleResponse(node) {
786
820
  let translations = null;
@@ -805,6 +839,14 @@ function normaliseLocaleResponse(node) {
805
839
  translations
806
840
  };
807
841
  }
842
+ function normaliseErrorMessagesResponse(node) {
843
+ return {
844
+ lang: node.lang,
845
+ version: node.version,
846
+ notModified: node.notModified,
847
+ messages: node.messages === null ? null : pickStringEntries(node.messages)
848
+ };
849
+ }
808
850
  function normaliseRefreshLocalesPayload(node) {
809
851
  let versions;
810
852
  try {
@@ -838,10 +880,33 @@ var REFRESH_LOCALES_MUTATION = `mutation RefreshLocales {
838
880
  versions
839
881
  }
840
882
  }`;
883
+ var ERROR_MESSAGES_QUERY = `query ErrorMessages($input: LocalesInput!) {
884
+ errorMessages(input: $input) {
885
+ lang
886
+ version
887
+ notModified
888
+ messages
889
+ }
890
+ }`;
841
891
 
842
892
  // src/modules/locale/index.ts
843
893
  var LocaleModule = class {
844
894
  #graphql;
895
+ /**
896
+ * The loaded error-message catalog. Instance state, not module state, so two
897
+ * clients in one process (tests, multi-tenant hosts) never share a catalog.
898
+ *
899
+ * Null until a load succeeds. It is NEVER reset to null afterwards — a failed
900
+ * refresh keeps the last-known-good copy, because losing all error copy is
901
+ * strictly worse than showing slightly stale copy.
902
+ */
903
+ #errorMessages = null;
904
+ /**
905
+ * In-flight catalog loads, keyed by normalised language. Collapses concurrent
906
+ * callers onto one request; entries are removed as soon as a load settles, so
907
+ * this never becomes a cache of results (or of failures).
908
+ */
909
+ #errorMessagesInFlight = /* @__PURE__ */ new Map();
845
910
  constructor(deps) {
846
911
  this.#graphql = deps.graphql;
847
912
  }
@@ -864,6 +929,94 @@ var LocaleModule = class {
864
929
  const body = await this.#graphql.request(LOCALES_QUERY, { input }, opts);
865
930
  return normaliseLocaleResponse(body.locales);
866
931
  }
932
+ /**
933
+ * Fetch the error-message catalog for a single language.
934
+ *
935
+ * The throwing sibling of {@link loadErrorMessages}: errors surface as
936
+ * TaphubError subclasses with backend `extensions.code` values
937
+ * (`LangNotSupported`, `FeatureDisabled`, …), because a caller doing an
938
+ * explicit fetch wants to see why it failed. Hosts that just want copy on
939
+ * screen should call `loadErrorMessages` instead.
940
+ *
941
+ * `lang` is passed through as given — normalisation is `loadErrorMessages`'
942
+ * job, so an explicit caller keeps full control of the tag.
943
+ */
944
+ async getErrorMessages(lang, knownVersion, opts) {
945
+ const input = { lang };
946
+ if (knownVersion !== void 0 && knownVersion !== "") {
947
+ input.knownVersion = knownVersion;
948
+ }
949
+ const body = await this.#graphql.request(
950
+ ERROR_MESSAGES_QUERY,
951
+ { input },
952
+ opts
953
+ );
954
+ return normaliseErrorMessagesResponse(body.errorMessages);
955
+ }
956
+ /**
957
+ * Load the error-message catalog for `lang` into this client.
958
+ *
959
+ * Background hydration with nobody waiting on it, so it **never throws and
960
+ * never rejects**. Every failure path — network error, backend down,
961
+ * `FeatureDisabled` when the source sheet is not configured, an unsupported
962
+ * tag, a malformed body — leaves the previously loaded catalog in place.
963
+ * Failing to translate an error must not itself become an error.
964
+ *
965
+ * The tag is normalised first (region dropped, Chinese scripts kept apart),
966
+ * so a host can hand over `navigator.language` untouched.
967
+ *
968
+ * Concurrent calls for the same language share one request. The catalog lives
969
+ * on the client while the thing that triggers a load (a mounted provider)
970
+ * lives a level below, so two providers sharing one client would otherwise
971
+ * fetch the same bytes into the same place twice.
972
+ *
973
+ * This is dedup, not caching — it lasts only while a request is in flight. A
974
+ * host calling again after one settles gets a fresh fetch.
975
+ *
976
+ * Deliberately takes NO AbortSignal. A follower piggybacking on someone
977
+ * else's request would otherwise have its outcome decided by whether that
978
+ * other caller happened to unmount first. Letting a background hydration run
979
+ * to completion costs one small request; stranding a caller costs all the
980
+ * error copy on screen.
981
+ *
982
+ * No persistence and no polling: the host decides when to call this. The
983
+ * React provider calls it on mount and on language change.
984
+ */
985
+ async loadErrorMessages(lang) {
986
+ const key = normaliseLang(lang);
987
+ const inFlight = this.#errorMessagesInFlight.get(key);
988
+ if (inFlight) return inFlight;
989
+ const run = (async () => {
990
+ try {
991
+ const res = await this.getErrorMessages(key);
992
+ if (res.notModified || res.messages === null) return;
993
+ if (Object.keys(res.messages).length === 0) return;
994
+ this.#errorMessages = res.messages;
995
+ } catch {
996
+ } finally {
997
+ this.#errorMessagesInFlight.delete(key);
998
+ }
999
+ })();
1000
+ this.#errorMessagesInFlight.set(key, run);
1001
+ return run;
1002
+ }
1003
+ /**
1004
+ * The sentence to show for a backend error `code`.
1005
+ *
1006
+ * **Synchronous by design** — error copy is read when a toast is built, inside
1007
+ * a `catch` or an error handler, where React hooks are illegal. That is why
1008
+ * this is a plain method and not a hook.
1009
+ *
1010
+ * Returns `fallback` whenever the catalog cannot produce a usable sentence: no
1011
+ * catalog loaded yet, no row for the code, or a blank row. Never returns an
1012
+ * empty string, and never returns the raw code.
1013
+ *
1014
+ * `meta` fills `{name}` placeholders in a catalog row; any placeholder left
1015
+ * unfilled is stripped, so raw braces never reach the user.
1016
+ */
1017
+ errorMessage(code, fallback, meta) {
1018
+ return resolveErrorMessage(code, this.#errorMessages, fallback, meta);
1019
+ }
867
1020
  /**
868
1021
  * Trigger an admin refresh — backend re-pulls the source Sheet, invalidates
869
1022
  * its cache, and writes fresh entries for every supported language.
@@ -1453,6 +1606,7 @@ function mapWireCandle(raw) {
1453
1606
  c: c.c
1454
1607
  };
1455
1608
  if (c.volatility !== void 0) result.volatility = c.volatility;
1609
+ if (c.slowVolatility !== void 0) result.slowVolatility = c.slowVolatility;
1456
1610
  if (c.coef_mults !== void 0) result.coefMults = c.coef_mults;
1457
1611
  if (c.coefMults !== void 0) result.coefMults = c.coefMults;
1458
1612
  return result;
@@ -3121,5 +3275,6 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
3121
3275
  isWin,
3122
3276
  normalCDF,
3123
3277
  normalPDF,
3278
+ normaliseLang,
3124
3279
  pairIdFromBidResultTopic
3125
3280
  });
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.
@@ -647,6 +758,8 @@ interface MqttWireCandle {
647
758
  l: number;
648
759
  c: number;
649
760
  volatility?: number;
761
+ /** SLOW σ — grid geometry only; `volatility` above (fast σ) prices coefficients. */
762
+ slowVolatility?: number;
650
763
  coef_mults?: number[];
651
764
  }
652
765
  interface MqttWireAcceptedBid {
@@ -808,6 +921,8 @@ interface MqttCandleEvent {
808
921
  l: number;
809
922
  c: number;
810
923
  volatility?: number;
924
+ /** SLOW σ — grid geometry only (cell-size ideal); fast σ in `volatility` prices coefficients. */
925
+ slowVolatility?: number;
811
926
  coefMults?: number[];
812
927
  }
813
928
  /**
@@ -1300,4 +1415,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
1300
1415
  declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1301
1416
  declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1302
1417
 
1303
- 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 };
1418
+ export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, 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.
@@ -647,6 +758,8 @@ interface MqttWireCandle {
647
758
  l: number;
648
759
  c: number;
649
760
  volatility?: number;
761
+ /** SLOW σ — grid geometry only; `volatility` above (fast σ) prices coefficients. */
762
+ slowVolatility?: number;
650
763
  coef_mults?: number[];
651
764
  }
652
765
  interface MqttWireAcceptedBid {
@@ -808,6 +921,8 @@ interface MqttCandleEvent {
808
921
  l: number;
809
922
  c: number;
810
923
  volatility?: number;
924
+ /** SLOW σ — grid geometry only (cell-size ideal); fast σ in `volatility` prices coefficients. */
925
+ slowVolatility?: number;
811
926
  coefMults?: number[];
812
927
  }
813
928
  /**
@@ -1300,4 +1415,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
1300
1415
  declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1301
1416
  declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
1302
1417
 
1303
- 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 };
1418
+ export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, 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.
@@ -1387,6 +1539,7 @@ function mapWireCandle(raw) {
1387
1539
  c: c.c
1388
1540
  };
1389
1541
  if (c.volatility !== void 0) result.volatility = c.volatility;
1542
+ if (c.slowVolatility !== void 0) result.slowVolatility = c.slowVolatility;
1390
1543
  if (c.coef_mults !== void 0) result.coefMults = c.coef_mults;
1391
1544
  if (c.coefMults !== void 0) result.coefMults = c.coefMults;
1392
1545
  return result;
@@ -3054,5 +3207,6 @@ export {
3054
3207
  isWin,
3055
3208
  normalCDF,
3056
3209
  normalPDF,
3210
+ normaliseLang,
3057
3211
  pairIdFromBidResultTopic
3058
3212
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taphubhq/sdk-core",
3
- "version": "0.25.2",
3
+ "version": "0.25.4",
4
4
  "description": "Core SDK for building on the TabHub platform",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",