@taphubhq/sdk-core 0.24.0 → 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 +55 -56
- package/dist/index.d.mts +40 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +55 -56
- package/package.json +1 -1
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",
|
|
@@ -1983,20 +2025,9 @@ function classifyNetworkLevel(rtt) {
|
|
|
1983
2025
|
// src/network/RttSmoother.ts
|
|
1984
2026
|
var EMA_ALPHA = 0.25;
|
|
1985
2027
|
var OUTLIER_MULTIPLIER = 3;
|
|
1986
|
-
var DEBOUNCE_DEGRADE = 2;
|
|
1987
|
-
var DEBOUNCE_IMPROVE = 5;
|
|
1988
|
-
var LEVEL_RANK = {
|
|
1989
|
-
good: 0,
|
|
1990
|
-
fair: 1,
|
|
1991
|
-
poor: 2,
|
|
1992
|
-
offline: 3
|
|
1993
|
-
};
|
|
1994
2028
|
var RttSmoother = class {
|
|
1995
2029
|
ema = 0;
|
|
1996
2030
|
level = "good";
|
|
1997
|
-
committed = "good";
|
|
1998
|
-
candidate = "good";
|
|
1999
|
-
candidateCount = 0;
|
|
2000
2031
|
overrideOffline = false;
|
|
2001
2032
|
consecutiveOutliers = 0;
|
|
2002
2033
|
add(rtt) {
|
|
@@ -2008,23 +2039,7 @@ var RttSmoother = class {
|
|
|
2008
2039
|
}
|
|
2009
2040
|
const coldStart = this.ema === 0;
|
|
2010
2041
|
this.ema = coldStart ? rtt : EMA_ALPHA * rtt + (1 - EMA_ALPHA) * this.ema;
|
|
2011
|
-
|
|
2012
|
-
if (coldStart) {
|
|
2013
|
-
this.committed = next;
|
|
2014
|
-
this.candidate = next;
|
|
2015
|
-
this.candidateCount = 1;
|
|
2016
|
-
} else {
|
|
2017
|
-
if (next === this.candidate) {
|
|
2018
|
-
this.candidateCount += 1;
|
|
2019
|
-
} else {
|
|
2020
|
-
this.candidate = next;
|
|
2021
|
-
this.candidateCount = 1;
|
|
2022
|
-
}
|
|
2023
|
-
if (this.candidateCount >= this.requiredSamplesFor(next)) {
|
|
2024
|
-
this.committed = next;
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
this.level = this.overrideOffline ? "offline" : this.committed;
|
|
2042
|
+
this.level = this.overrideOffline ? "offline" : classifyNetworkLevel(this.ema);
|
|
2028
2043
|
}
|
|
2029
2044
|
forceOffline() {
|
|
2030
2045
|
this.overrideOffline = true;
|
|
@@ -2032,20 +2047,14 @@ var RttSmoother = class {
|
|
|
2032
2047
|
}
|
|
2033
2048
|
releaseOffline() {
|
|
2034
2049
|
this.overrideOffline = false;
|
|
2035
|
-
this.level = this.
|
|
2050
|
+
this.level = classifyNetworkLevel(this.ema);
|
|
2036
2051
|
}
|
|
2037
2052
|
reset() {
|
|
2038
2053
|
this.ema = 0;
|
|
2039
|
-
this.committed = "good";
|
|
2040
|
-
this.candidate = "good";
|
|
2041
|
-
this.candidateCount = 0;
|
|
2042
2054
|
this.consecutiveOutliers = 0;
|
|
2043
2055
|
this.overrideOffline = false;
|
|
2044
2056
|
this.level = "good";
|
|
2045
2057
|
}
|
|
2046
|
-
requiredSamplesFor(next) {
|
|
2047
|
-
return LEVEL_RANK[next] > LEVEL_RANK[this.committed] ? DEBOUNCE_DEGRADE : DEBOUNCE_IMPROVE;
|
|
2048
|
-
}
|
|
2049
2058
|
};
|
|
2050
2059
|
|
|
2051
2060
|
// src/network/NetworkQualityMonitor.ts
|
|
@@ -2072,7 +2081,6 @@ var NetworkQualityMonitor = class {
|
|
|
2072
2081
|
smoother = new RttSmoother();
|
|
2073
2082
|
mqttConnected = false;
|
|
2074
2083
|
mqttDisconnectedAt = null;
|
|
2075
|
-
committedNetwork = "good";
|
|
2076
2084
|
committedBackend = "ok";
|
|
2077
2085
|
// The last snapshot we emitted, used to decide whether the next recompute is
|
|
2078
2086
|
// worth emitting. Seeded with the initial default so the first real change
|
|
@@ -2150,7 +2158,6 @@ var NetworkQualityMonitor = class {
|
|
|
2150
2158
|
this.smoother.reset();
|
|
2151
2159
|
this.mqttConnected = false;
|
|
2152
2160
|
this.mqttDisconnectedAt = null;
|
|
2153
|
-
this.committedNetwork = "good";
|
|
2154
2161
|
this.committedBackend = "ok";
|
|
2155
2162
|
this.emitIfChanged(this.snapshot());
|
|
2156
2163
|
}
|
|
@@ -2214,26 +2221,12 @@ var NetworkQualityMonitor = class {
|
|
|
2214
2221
|
const useConnection = realSamples.length < COLD_START_REAL_SAMPLE_THRESHOLD;
|
|
2215
2222
|
const effectiveSamples = useConnection ? allSamples : realSamples;
|
|
2216
2223
|
const httpSamples = effectiveSamples.filter((s) => isHttpSource(s.source));
|
|
2217
|
-
|
|
2218
|
-
const metrics = this.deriveMetricsFrom(effectiveSamples);
|
|
2219
|
-
const candidate = classifyNetworkLevel(metrics.emaForLevel);
|
|
2220
|
-
let nextNetwork = this.smoother.level;
|
|
2221
|
-
if (effectiveSamples.length === 0) {
|
|
2222
|
-
nextNetwork = this.committedNetwork;
|
|
2223
|
-
} else if (this.smoother.ema > 0) {
|
|
2224
|
-
nextNetwork = this.smoother.level;
|
|
2225
|
-
} else {
|
|
2226
|
-
nextNetwork = candidate;
|
|
2227
|
-
}
|
|
2224
|
+
this.committedBackend = classifyBackendHealth(httpSamples);
|
|
2228
2225
|
if (this.isHardOffline()) {
|
|
2229
2226
|
this.smoother.forceOffline();
|
|
2230
|
-
nextNetwork = "offline";
|
|
2231
2227
|
} else if (this.smoother.level === "offline") {
|
|
2232
2228
|
this.smoother.releaseOffline();
|
|
2233
|
-
nextNetwork = this.smoother.level;
|
|
2234
2229
|
}
|
|
2235
|
-
this.committedNetwork = nextNetwork;
|
|
2236
|
-
this.committedBackend = backend;
|
|
2237
2230
|
this.emitIfChanged(this.snapshot());
|
|
2238
2231
|
}
|
|
2239
2232
|
// Emits network:change when any *displayed* field of the snapshot moves. This
|
|
@@ -2294,10 +2287,12 @@ var NetworkQualityMonitor = class {
|
|
|
2294
2287
|
snapshot() {
|
|
2295
2288
|
const samples = this.allHttpAndMqttSamples();
|
|
2296
2289
|
const metrics = this.deriveMetricsFrom(samples);
|
|
2290
|
+
const rtt = Math.round(metrics.emaForLevel);
|
|
2291
|
+
const network = this.smoother.level === "offline" ? "offline" : classifyNetworkLevel(rtt);
|
|
2297
2292
|
return {
|
|
2298
|
-
network
|
|
2293
|
+
network,
|
|
2299
2294
|
backend: this.committedBackend,
|
|
2300
|
-
rtt
|
|
2295
|
+
rtt,
|
|
2301
2296
|
jitter: Math.round(metrics.jitter),
|
|
2302
2297
|
lossRate: metrics.lossRate,
|
|
2303
2298
|
mqttConnected: this.mqttConnected,
|
|
@@ -2905,8 +2900,12 @@ var TaphubClient = class {
|
|
|
2905
2900
|
this.user.clearCurrencies();
|
|
2906
2901
|
}
|
|
2907
2902
|
});
|
|
2908
|
-
|
|
2909
|
-
this.
|
|
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
|
+
});
|
|
2910
2909
|
this.leaderboard = new LeaderboardModule({ graphql: this.#graphql });
|
|
2911
2910
|
this.agencyPairs = new AgencyPairModule({ graphql: this.#graphql });
|
|
2912
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;
|
|
@@ -1099,7 +1139,6 @@ declare class NetworkQualityMonitor {
|
|
|
1099
1139
|
private readonly smoother;
|
|
1100
1140
|
private mqttConnected;
|
|
1101
1141
|
private mqttDisconnectedAt;
|
|
1102
|
-
private committedNetwork;
|
|
1103
1142
|
private committedBackend;
|
|
1104
1143
|
private lastEmitted;
|
|
1105
1144
|
private tickHandle;
|
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;
|
|
@@ -1099,7 +1139,6 @@ declare class NetworkQualityMonitor {
|
|
|
1099
1139
|
private readonly smoother;
|
|
1100
1140
|
private mqttConnected;
|
|
1101
1141
|
private mqttDisconnectedAt;
|
|
1102
|
-
private committedNetwork;
|
|
1103
1142
|
private committedBackend;
|
|
1104
1143
|
private lastEmitted;
|
|
1105
1144
|
private tickHandle;
|
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",
|
|
@@ -1917,20 +1959,9 @@ function classifyNetworkLevel(rtt) {
|
|
|
1917
1959
|
// src/network/RttSmoother.ts
|
|
1918
1960
|
var EMA_ALPHA = 0.25;
|
|
1919
1961
|
var OUTLIER_MULTIPLIER = 3;
|
|
1920
|
-
var DEBOUNCE_DEGRADE = 2;
|
|
1921
|
-
var DEBOUNCE_IMPROVE = 5;
|
|
1922
|
-
var LEVEL_RANK = {
|
|
1923
|
-
good: 0,
|
|
1924
|
-
fair: 1,
|
|
1925
|
-
poor: 2,
|
|
1926
|
-
offline: 3
|
|
1927
|
-
};
|
|
1928
1962
|
var RttSmoother = class {
|
|
1929
1963
|
ema = 0;
|
|
1930
1964
|
level = "good";
|
|
1931
|
-
committed = "good";
|
|
1932
|
-
candidate = "good";
|
|
1933
|
-
candidateCount = 0;
|
|
1934
1965
|
overrideOffline = false;
|
|
1935
1966
|
consecutiveOutliers = 0;
|
|
1936
1967
|
add(rtt) {
|
|
@@ -1942,23 +1973,7 @@ var RttSmoother = class {
|
|
|
1942
1973
|
}
|
|
1943
1974
|
const coldStart = this.ema === 0;
|
|
1944
1975
|
this.ema = coldStart ? rtt : EMA_ALPHA * rtt + (1 - EMA_ALPHA) * this.ema;
|
|
1945
|
-
|
|
1946
|
-
if (coldStart) {
|
|
1947
|
-
this.committed = next;
|
|
1948
|
-
this.candidate = next;
|
|
1949
|
-
this.candidateCount = 1;
|
|
1950
|
-
} else {
|
|
1951
|
-
if (next === this.candidate) {
|
|
1952
|
-
this.candidateCount += 1;
|
|
1953
|
-
} else {
|
|
1954
|
-
this.candidate = next;
|
|
1955
|
-
this.candidateCount = 1;
|
|
1956
|
-
}
|
|
1957
|
-
if (this.candidateCount >= this.requiredSamplesFor(next)) {
|
|
1958
|
-
this.committed = next;
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
this.level = this.overrideOffline ? "offline" : this.committed;
|
|
1976
|
+
this.level = this.overrideOffline ? "offline" : classifyNetworkLevel(this.ema);
|
|
1962
1977
|
}
|
|
1963
1978
|
forceOffline() {
|
|
1964
1979
|
this.overrideOffline = true;
|
|
@@ -1966,20 +1981,14 @@ var RttSmoother = class {
|
|
|
1966
1981
|
}
|
|
1967
1982
|
releaseOffline() {
|
|
1968
1983
|
this.overrideOffline = false;
|
|
1969
|
-
this.level = this.
|
|
1984
|
+
this.level = classifyNetworkLevel(this.ema);
|
|
1970
1985
|
}
|
|
1971
1986
|
reset() {
|
|
1972
1987
|
this.ema = 0;
|
|
1973
|
-
this.committed = "good";
|
|
1974
|
-
this.candidate = "good";
|
|
1975
|
-
this.candidateCount = 0;
|
|
1976
1988
|
this.consecutiveOutliers = 0;
|
|
1977
1989
|
this.overrideOffline = false;
|
|
1978
1990
|
this.level = "good";
|
|
1979
1991
|
}
|
|
1980
|
-
requiredSamplesFor(next) {
|
|
1981
|
-
return LEVEL_RANK[next] > LEVEL_RANK[this.committed] ? DEBOUNCE_DEGRADE : DEBOUNCE_IMPROVE;
|
|
1982
|
-
}
|
|
1983
1992
|
};
|
|
1984
1993
|
|
|
1985
1994
|
// src/network/NetworkQualityMonitor.ts
|
|
@@ -2006,7 +2015,6 @@ var NetworkQualityMonitor = class {
|
|
|
2006
2015
|
smoother = new RttSmoother();
|
|
2007
2016
|
mqttConnected = false;
|
|
2008
2017
|
mqttDisconnectedAt = null;
|
|
2009
|
-
committedNetwork = "good";
|
|
2010
2018
|
committedBackend = "ok";
|
|
2011
2019
|
// The last snapshot we emitted, used to decide whether the next recompute is
|
|
2012
2020
|
// worth emitting. Seeded with the initial default so the first real change
|
|
@@ -2084,7 +2092,6 @@ var NetworkQualityMonitor = class {
|
|
|
2084
2092
|
this.smoother.reset();
|
|
2085
2093
|
this.mqttConnected = false;
|
|
2086
2094
|
this.mqttDisconnectedAt = null;
|
|
2087
|
-
this.committedNetwork = "good";
|
|
2088
2095
|
this.committedBackend = "ok";
|
|
2089
2096
|
this.emitIfChanged(this.snapshot());
|
|
2090
2097
|
}
|
|
@@ -2148,26 +2155,12 @@ var NetworkQualityMonitor = class {
|
|
|
2148
2155
|
const useConnection = realSamples.length < COLD_START_REAL_SAMPLE_THRESHOLD;
|
|
2149
2156
|
const effectiveSamples = useConnection ? allSamples : realSamples;
|
|
2150
2157
|
const httpSamples = effectiveSamples.filter((s) => isHttpSource(s.source));
|
|
2151
|
-
|
|
2152
|
-
const metrics = this.deriveMetricsFrom(effectiveSamples);
|
|
2153
|
-
const candidate = classifyNetworkLevel(metrics.emaForLevel);
|
|
2154
|
-
let nextNetwork = this.smoother.level;
|
|
2155
|
-
if (effectiveSamples.length === 0) {
|
|
2156
|
-
nextNetwork = this.committedNetwork;
|
|
2157
|
-
} else if (this.smoother.ema > 0) {
|
|
2158
|
-
nextNetwork = this.smoother.level;
|
|
2159
|
-
} else {
|
|
2160
|
-
nextNetwork = candidate;
|
|
2161
|
-
}
|
|
2158
|
+
this.committedBackend = classifyBackendHealth(httpSamples);
|
|
2162
2159
|
if (this.isHardOffline()) {
|
|
2163
2160
|
this.smoother.forceOffline();
|
|
2164
|
-
nextNetwork = "offline";
|
|
2165
2161
|
} else if (this.smoother.level === "offline") {
|
|
2166
2162
|
this.smoother.releaseOffline();
|
|
2167
|
-
nextNetwork = this.smoother.level;
|
|
2168
2163
|
}
|
|
2169
|
-
this.committedNetwork = nextNetwork;
|
|
2170
|
-
this.committedBackend = backend;
|
|
2171
2164
|
this.emitIfChanged(this.snapshot());
|
|
2172
2165
|
}
|
|
2173
2166
|
// Emits network:change when any *displayed* field of the snapshot moves. This
|
|
@@ -2228,10 +2221,12 @@ var NetworkQualityMonitor = class {
|
|
|
2228
2221
|
snapshot() {
|
|
2229
2222
|
const samples = this.allHttpAndMqttSamples();
|
|
2230
2223
|
const metrics = this.deriveMetricsFrom(samples);
|
|
2224
|
+
const rtt = Math.round(metrics.emaForLevel);
|
|
2225
|
+
const network = this.smoother.level === "offline" ? "offline" : classifyNetworkLevel(rtt);
|
|
2231
2226
|
return {
|
|
2232
|
-
network
|
|
2227
|
+
network,
|
|
2233
2228
|
backend: this.committedBackend,
|
|
2234
|
-
rtt
|
|
2229
|
+
rtt,
|
|
2235
2230
|
jitter: Math.round(metrics.jitter),
|
|
2236
2231
|
lossRate: metrics.lossRate,
|
|
2237
2232
|
mqttConnected: this.mqttConnected,
|
|
@@ -2839,8 +2834,12 @@ var TaphubClient = class {
|
|
|
2839
2834
|
this.user.clearCurrencies();
|
|
2840
2835
|
}
|
|
2841
2836
|
});
|
|
2842
|
-
|
|
2843
|
-
this.
|
|
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
|
+
});
|
|
2844
2843
|
this.leaderboard = new LeaderboardModule({ graphql: this.#graphql });
|
|
2845
2844
|
this.agencyPairs = new AgencyPairModule({ graphql: this.#graphql });
|
|
2846
2845
|
this.locale = new LocaleModule({ graphql: this.#graphql });
|