@taphubhq/sdk-core 0.24.1 → 0.24.2

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
@@ -64,6 +64,35 @@ __export(index_exports, {
64
64
  });
65
65
  module.exports = __toCommonJS(index_exports);
66
66
 
67
+ // src/clock/ClockSync.ts
68
+ var ClockSync = class {
69
+ #offsetMs = 0;
70
+ #synced = false;
71
+ /** True once an offset has been recorded. */
72
+ get synced() {
73
+ return this.#synced;
74
+ }
75
+ /**
76
+ * Milliseconds to add to the client clock to reach the server clock.
77
+ * Returns 0 until the first successful sync.
78
+ */
79
+ getOffset() {
80
+ return this.#offsetMs;
81
+ }
82
+ /**
83
+ * Record a single clock sample from a round-trip that returned the server time.
84
+ * No-op if already synced (set-once), or if any input is not a finite number — a
85
+ * missing/garbage `serverTime` must never corrupt the offset.
86
+ */
87
+ recordSync(sample) {
88
+ if (this.#synced) return;
89
+ const { t0, serverTime, t1 } = sample;
90
+ if (!Number.isFinite(t0) || !Number.isFinite(serverTime) || !Number.isFinite(t1)) return;
91
+ this.#offsetMs = Math.round(serverTime + (t1 - t0) / 2 - t1);
92
+ this.#synced = true;
93
+ }
94
+ };
95
+
67
96
  // src/errors/index.ts
68
97
  var TaphubError = class _TaphubError extends Error {
69
98
  code;
@@ -518,8 +547,10 @@ var isCancelled = (bid) => bid.status === "cancelled";
518
547
  // src/modules/bid/index.ts
519
548
  var BidModule = class {
520
549
  #graphql;
550
+ #getClockOffset;
521
551
  constructor(deps) {
522
552
  this.#graphql = deps.graphql;
553
+ this.#getClockOffset = deps.getClockOffset;
523
554
  }
524
555
  async placeBid(input, opts) {
525
556
  const variables = {
@@ -537,6 +568,12 @@ var BidModule = class {
537
568
  coefficient: input.coefficient,
538
569
  amount: input.amount,
539
570
  slippage: input.slippage,
571
+ // bid-260619-client-time-meta: auto-stamp the client's RAW send time and the
572
+ // measured clock offset so the server can record skew-corrected latency into
573
+ // bid.meta. Stamped here at dispatch (not caller-supplied) so `ct` is the actual
574
+ // send moment. The server treats both as untrusted tracing data only.
575
+ ct: Date.now(),
576
+ clockOffset: this.#getClockOffset(),
540
577
  // QA-only late-bid passthrough (bid-260610). Omitted entirely when absent
541
578
  // so no `bidToken: null` is sent. sdk-react sets it from localStorage.
542
579
  ...input.bidToken ? { bidToken: input.bidToken } : {}
@@ -930,6 +967,7 @@ function normalisePairInfo(node) {
930
967
 
931
968
  // src/modules/pair/queries.ts
932
969
  var PAIR_QUERY = `query AgencyPair($pairId: ID!) {
970
+ serverTime
933
971
  agencyPair(pairId: $pairId) {
934
972
  id pair { id pair source thumb } status createdAt
935
973
  config {
@@ -959,8 +997,10 @@ var BUILDER_AVAILABLE_GAME_PAIRS_QUERY = `query BuilderAvailableGamePairs($gamep
959
997
  // src/modules/pair/index.ts
960
998
  var PairModule = class {
961
999
  #graphql;
1000
+ #clockSync;
962
1001
  constructor(deps) {
963
1002
  this.#graphql = deps.graphql;
1003
+ this.#clockSync = deps.clockSync;
964
1004
  }
965
1005
  // REVIEW[bid-260602-v2]: keyed by pairId (the #2 game_pairs.id, e.g. "grid-ETH-USD").
966
1006
  // Dropped the `pair` symbol arg + `gameplaySlug` option — the gameplay is already
@@ -968,6 +1008,7 @@ var PairModule = class {
968
1008
  async get(pairId, opts) {
969
1009
  const variables = { pairId };
970
1010
  let body;
1011
+ const t0 = Date.now();
971
1012
  try {
972
1013
  body = await this.#graphql.request(PAIR_QUERY, variables, opts);
973
1014
  } catch (err) {
@@ -979,6 +1020,7 @@ var PairModule = class {
979
1020
  }
980
1021
  throw err;
981
1022
  }
1023
+ this.#clockSync.recordSync({ t0, serverTime: body.serverTime, t1: Date.now() });
982
1024
  if (body.agencyPair === null) {
983
1025
  throw new TaphubValidationError("Game not found for pair", {
984
1026
  code: "GameNotFound",
@@ -2858,8 +2900,12 @@ var TaphubClient = class {
2858
2900
  this.user.clearCurrencies();
2859
2901
  }
2860
2902
  });
2861
- this.pair = new PairModule({ graphql: this.#graphql });
2862
- this.bid = new BidModule({ graphql: this.#graphql });
2903
+ const clockSync = new ClockSync();
2904
+ this.pair = new PairModule({ graphql: this.#graphql, clockSync });
2905
+ this.bid = new BidModule({
2906
+ graphql: this.#graphql,
2907
+ getClockOffset: () => clockSync.getOffset()
2908
+ });
2863
2909
  this.leaderboard = new LeaderboardModule({ graphql: this.#graphql });
2864
2910
  this.agencyPairs = new AgencyPairModule({ graphql: this.#graphql });
2865
2911
  this.locale = new LocaleModule({ graphql: this.#graphql });
package/dist/index.d.mts CHANGED
@@ -270,6 +270,7 @@ declare const isCancelled: (bid: Bid) => boolean;
270
270
 
271
271
  interface BidModuleDeps {
272
272
  graphql: GraphQLTransport;
273
+ getClockOffset: () => number;
273
274
  }
274
275
  declare class BidModule {
275
276
  #private;
@@ -496,6 +497,44 @@ declare class LocaleModule {
496
497
  }): Promise<LocaleRefreshResult>;
497
498
  }
498
499
 
500
+ /**
501
+ * ClockSync estimates the offset between the client clock and the server clock, so that
502
+ * timestamps the client reports (e.g. a bid's send time) can be expressed on the server's
503
+ * timeline and latency measurements are not distorted by client clock skew.
504
+ *
505
+ * The offset is measured ONCE per session (set-once) from a single round-trip whose
506
+ * response carries the server time, using Cristian's algorithm:
507
+ *
508
+ * offset = serverTime + (t1 - t0) / 2 - t1
509
+ *
510
+ * where `t0` / `t1` are the client clock immediately before / after the request. `offset`
511
+ * is the number of milliseconds to ADD to the client clock to obtain the server clock.
512
+ *
513
+ * This is adequate for millisecond-grade latency tracing (the `bid-260619-server-time-sync`
514
+ * capability); it is intentionally NOT a high-precision NTP implementation, and it does not
515
+ * re-sync to correct drift.
516
+ */
517
+ declare class ClockSync {
518
+ #private;
519
+ /** True once an offset has been recorded. */
520
+ get synced(): boolean;
521
+ /**
522
+ * Milliseconds to add to the client clock to reach the server clock.
523
+ * Returns 0 until the first successful sync.
524
+ */
525
+ getOffset(): number;
526
+ /**
527
+ * Record a single clock sample from a round-trip that returned the server time.
528
+ * No-op if already synced (set-once), or if any input is not a finite number — a
529
+ * missing/garbage `serverTime` must never corrupt the offset.
530
+ */
531
+ recordSync(sample: {
532
+ t0: number;
533
+ serverTime: number;
534
+ t1: number;
535
+ }): void;
536
+ }
537
+
499
538
  interface Pair {
500
539
  id: string;
501
540
  pair: string;
@@ -572,6 +611,7 @@ interface Candle {
572
611
 
573
612
  interface PairModuleDeps {
574
613
  graphql: GraphQLTransport;
614
+ clockSync: ClockSync;
575
615
  }
576
616
  declare class PairModule {
577
617
  #private;
package/dist/index.d.ts CHANGED
@@ -270,6 +270,7 @@ declare const isCancelled: (bid: Bid) => boolean;
270
270
 
271
271
  interface BidModuleDeps {
272
272
  graphql: GraphQLTransport;
273
+ getClockOffset: () => number;
273
274
  }
274
275
  declare class BidModule {
275
276
  #private;
@@ -496,6 +497,44 @@ declare class LocaleModule {
496
497
  }): Promise<LocaleRefreshResult>;
497
498
  }
498
499
 
500
+ /**
501
+ * ClockSync estimates the offset between the client clock and the server clock, so that
502
+ * timestamps the client reports (e.g. a bid's send time) can be expressed on the server's
503
+ * timeline and latency measurements are not distorted by client clock skew.
504
+ *
505
+ * The offset is measured ONCE per session (set-once) from a single round-trip whose
506
+ * response carries the server time, using Cristian's algorithm:
507
+ *
508
+ * offset = serverTime + (t1 - t0) / 2 - t1
509
+ *
510
+ * where `t0` / `t1` are the client clock immediately before / after the request. `offset`
511
+ * is the number of milliseconds to ADD to the client clock to obtain the server clock.
512
+ *
513
+ * This is adequate for millisecond-grade latency tracing (the `bid-260619-server-time-sync`
514
+ * capability); it is intentionally NOT a high-precision NTP implementation, and it does not
515
+ * re-sync to correct drift.
516
+ */
517
+ declare class ClockSync {
518
+ #private;
519
+ /** True once an offset has been recorded. */
520
+ get synced(): boolean;
521
+ /**
522
+ * Milliseconds to add to the client clock to reach the server clock.
523
+ * Returns 0 until the first successful sync.
524
+ */
525
+ getOffset(): number;
526
+ /**
527
+ * Record a single clock sample from a round-trip that returned the server time.
528
+ * No-op if already synced (set-once), or if any input is not a finite number — a
529
+ * missing/garbage `serverTime` must never corrupt the offset.
530
+ */
531
+ recordSync(sample: {
532
+ t0: number;
533
+ serverTime: number;
534
+ t1: number;
535
+ }): void;
536
+ }
537
+
499
538
  interface Pair {
500
539
  id: string;
501
540
  pair: string;
@@ -572,6 +611,7 @@ interface Candle {
572
611
 
573
612
  interface PairModuleDeps {
574
613
  graphql: GraphQLTransport;
614
+ clockSync: ClockSync;
575
615
  }
576
616
  declare class PairModule {
577
617
  #private;
package/dist/index.js CHANGED
@@ -1,3 +1,32 @@
1
+ // src/clock/ClockSync.ts
2
+ var ClockSync = class {
3
+ #offsetMs = 0;
4
+ #synced = false;
5
+ /** True once an offset has been recorded. */
6
+ get synced() {
7
+ return this.#synced;
8
+ }
9
+ /**
10
+ * Milliseconds to add to the client clock to reach the server clock.
11
+ * Returns 0 until the first successful sync.
12
+ */
13
+ getOffset() {
14
+ return this.#offsetMs;
15
+ }
16
+ /**
17
+ * Record a single clock sample from a round-trip that returned the server time.
18
+ * No-op if already synced (set-once), or if any input is not a finite number — a
19
+ * missing/garbage `serverTime` must never corrupt the offset.
20
+ */
21
+ recordSync(sample) {
22
+ if (this.#synced) return;
23
+ const { t0, serverTime, t1 } = sample;
24
+ if (!Number.isFinite(t0) || !Number.isFinite(serverTime) || !Number.isFinite(t1)) return;
25
+ this.#offsetMs = Math.round(serverTime + (t1 - t0) / 2 - t1);
26
+ this.#synced = true;
27
+ }
28
+ };
29
+
1
30
  // src/errors/index.ts
2
31
  var TaphubError = class _TaphubError extends Error {
3
32
  code;
@@ -452,8 +481,10 @@ var isCancelled = (bid) => bid.status === "cancelled";
452
481
  // src/modules/bid/index.ts
453
482
  var BidModule = class {
454
483
  #graphql;
484
+ #getClockOffset;
455
485
  constructor(deps) {
456
486
  this.#graphql = deps.graphql;
487
+ this.#getClockOffset = deps.getClockOffset;
457
488
  }
458
489
  async placeBid(input, opts) {
459
490
  const variables = {
@@ -471,6 +502,12 @@ var BidModule = class {
471
502
  coefficient: input.coefficient,
472
503
  amount: input.amount,
473
504
  slippage: input.slippage,
505
+ // bid-260619-client-time-meta: auto-stamp the client's RAW send time and the
506
+ // measured clock offset so the server can record skew-corrected latency into
507
+ // bid.meta. Stamped here at dispatch (not caller-supplied) so `ct` is the actual
508
+ // send moment. The server treats both as untrusted tracing data only.
509
+ ct: Date.now(),
510
+ clockOffset: this.#getClockOffset(),
474
511
  // QA-only late-bid passthrough (bid-260610). Omitted entirely when absent
475
512
  // so no `bidToken: null` is sent. sdk-react sets it from localStorage.
476
513
  ...input.bidToken ? { bidToken: input.bidToken } : {}
@@ -864,6 +901,7 @@ function normalisePairInfo(node) {
864
901
 
865
902
  // src/modules/pair/queries.ts
866
903
  var PAIR_QUERY = `query AgencyPair($pairId: ID!) {
904
+ serverTime
867
905
  agencyPair(pairId: $pairId) {
868
906
  id pair { id pair source thumb } status createdAt
869
907
  config {
@@ -893,8 +931,10 @@ var BUILDER_AVAILABLE_GAME_PAIRS_QUERY = `query BuilderAvailableGamePairs($gamep
893
931
  // src/modules/pair/index.ts
894
932
  var PairModule = class {
895
933
  #graphql;
934
+ #clockSync;
896
935
  constructor(deps) {
897
936
  this.#graphql = deps.graphql;
937
+ this.#clockSync = deps.clockSync;
898
938
  }
899
939
  // REVIEW[bid-260602-v2]: keyed by pairId (the #2 game_pairs.id, e.g. "grid-ETH-USD").
900
940
  // Dropped the `pair` symbol arg + `gameplaySlug` option — the gameplay is already
@@ -902,6 +942,7 @@ var PairModule = class {
902
942
  async get(pairId, opts) {
903
943
  const variables = { pairId };
904
944
  let body;
945
+ const t0 = Date.now();
905
946
  try {
906
947
  body = await this.#graphql.request(PAIR_QUERY, variables, opts);
907
948
  } catch (err) {
@@ -913,6 +954,7 @@ var PairModule = class {
913
954
  }
914
955
  throw err;
915
956
  }
957
+ this.#clockSync.recordSync({ t0, serverTime: body.serverTime, t1: Date.now() });
916
958
  if (body.agencyPair === null) {
917
959
  throw new TaphubValidationError("Game not found for pair", {
918
960
  code: "GameNotFound",
@@ -2792,8 +2834,12 @@ var TaphubClient = class {
2792
2834
  this.user.clearCurrencies();
2793
2835
  }
2794
2836
  });
2795
- this.pair = new PairModule({ graphql: this.#graphql });
2796
- this.bid = new BidModule({ graphql: this.#graphql });
2837
+ const clockSync = new ClockSync();
2838
+ this.pair = new PairModule({ graphql: this.#graphql, clockSync });
2839
+ this.bid = new BidModule({
2840
+ graphql: this.#graphql,
2841
+ getClockOffset: () => clockSync.getOffset()
2842
+ });
2797
2843
  this.leaderboard = new LeaderboardModule({ graphql: this.#graphql });
2798
2844
  this.agencyPairs = new AgencyPairModule({ graphql: this.#graphql });
2799
2845
  this.locale = new LocaleModule({ graphql: this.#graphql });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taphubhq/sdk-core",
3
- "version": "0.24.1",
3
+ "version": "0.24.2",
4
4
  "description": "Core SDK for building on the TabHub platform",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",