@taphubhq/sdk-core 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2448 @@
1
+ // src/errors/index.ts
2
+ var TaphubError = class _TaphubError extends Error {
3
+ code;
4
+ details;
5
+ constructor(message, options) {
6
+ super(message);
7
+ this.name = "TaphubError";
8
+ this.code = options.code;
9
+ this.details = options.details;
10
+ Object.setPrototypeOf(this, _TaphubError.prototype);
11
+ }
12
+ };
13
+ var TaphubAuthError = class _TaphubAuthError extends TaphubError {
14
+ constructor(message, options) {
15
+ super(message, options);
16
+ this.name = "TaphubAuthError";
17
+ Object.setPrototypeOf(this, _TaphubAuthError.prototype);
18
+ }
19
+ };
20
+ var TaphubNetworkError = class _TaphubNetworkError extends TaphubError {
21
+ constructor(message, options) {
22
+ super(message, options);
23
+ this.name = "TaphubNetworkError";
24
+ Object.setPrototypeOf(this, _TaphubNetworkError.prototype);
25
+ }
26
+ };
27
+ var TaphubValidationError = class _TaphubValidationError extends TaphubError {
28
+ constructor(message, options) {
29
+ super(message, options);
30
+ this.name = "TaphubValidationError";
31
+ Object.setPrototypeOf(this, _TaphubValidationError.prototype);
32
+ }
33
+ };
34
+ var TaphubServerError = class _TaphubServerError extends TaphubError {
35
+ constructor(message, options) {
36
+ super(message, options);
37
+ this.name = "TaphubServerError";
38
+ Object.setPrototypeOf(this, _TaphubServerError.prototype);
39
+ }
40
+ };
41
+ var TaphubSlippageError = class _TaphubSlippageError extends TaphubValidationError {
42
+ clientCoef;
43
+ serverCoef;
44
+ slippage;
45
+ constructor(message, options) {
46
+ super(message, { code: options.code, details: options.details });
47
+ this.name = "TaphubSlippageError";
48
+ this.clientCoef = options.clientCoef;
49
+ this.serverCoef = options.serverCoef;
50
+ this.slippage = options.slippage;
51
+ Object.setPrototypeOf(this, _TaphubSlippageError.prototype);
52
+ }
53
+ };
54
+
55
+ // src/events/bus.ts
56
+ var TaphubEventBus = class {
57
+ target = new EventTarget();
58
+ on(event, handler) {
59
+ this.target.addEventListener(event, handler);
60
+ }
61
+ off(event, handler) {
62
+ this.target.removeEventListener(event, handler);
63
+ }
64
+ emit(event, payload) {
65
+ this.target.dispatchEvent(new CustomEvent(event, { detail: payload }));
66
+ }
67
+ };
68
+
69
+ // src/modules/auth/normalise.ts
70
+ function normaliseGoogleResponse(body) {
71
+ return {
72
+ accessToken: body.access_token,
73
+ user: {
74
+ id: body.user.id,
75
+ displayName: body.user.name ?? "",
76
+ agencyUid: body.user.agency_uid ?? null,
77
+ currency: body.user.currency ?? "",
78
+ wallet: {
79
+ id: body.user.default_wallet?.id ?? null,
80
+ balance: body.user.default_wallet?.amount ?? "0",
81
+ currency: body.user.default_wallet?.currency ?? "",
82
+ isEnabled: body.user.default_wallet?.is_enable ?? false
83
+ }
84
+ },
85
+ isDemo: false
86
+ };
87
+ }
88
+ function normaliseDemoResponse(body) {
89
+ return {
90
+ accessToken: body.access_token,
91
+ user: {
92
+ id: body.user.id,
93
+ displayName: body.user.username ?? "",
94
+ agencyUid: null,
95
+ currency: body.user.currency ?? "",
96
+ wallet: {
97
+ id: null,
98
+ balance: body.user.balance ?? "0",
99
+ currency: body.user.currency ?? "",
100
+ isEnabled: true
101
+ }
102
+ },
103
+ isDemo: body.demo ?? true
104
+ };
105
+ }
106
+ function normaliseCreateDemoUserResponse(body) {
107
+ const node = body.createDemoUser;
108
+ if (!node || typeof node.token !== "string" || node.token === "") {
109
+ throw new TaphubServerError("Invalid response from server", { code: "InvalidResponse" });
110
+ }
111
+ if (!node.user || typeof node.user.id !== "string" || node.user.id === "") {
112
+ throw new TaphubServerError("Invalid response from server", { code: "InvalidResponse" });
113
+ }
114
+ return {
115
+ accessToken: node.token,
116
+ user: {
117
+ id: node.user.id,
118
+ displayName: node.user.username ?? "",
119
+ agencyUid: null,
120
+ currency: "",
121
+ wallet: {
122
+ id: null,
123
+ balance: node.user.balance ?? "0",
124
+ currency: "",
125
+ isEnabled: true
126
+ }
127
+ },
128
+ isDemo: node.user.is_demo
129
+ };
130
+ }
131
+ function normaliseUserLoginResponse(body) {
132
+ const node = body.userLogin;
133
+ if (!node || typeof node.accessToken !== "string" || node.accessToken === "") {
134
+ throw new TaphubServerError("Invalid response from server", { code: "InvalidResponse" });
135
+ }
136
+ if (!node.user || typeof node.user.id !== "string" || node.user.id === "") {
137
+ throw new TaphubServerError("Invalid response from server", { code: "InvalidResponse" });
138
+ }
139
+ const wallet = node.user.defaultWallet;
140
+ return {
141
+ accessToken: node.accessToken,
142
+ user: {
143
+ id: node.user.id,
144
+ displayName: node.user.name,
145
+ agencyUid: node.user.agencyUid,
146
+ currency: node.user.currency,
147
+ wallet: {
148
+ id: wallet.id,
149
+ balance: String(wallet.amount),
150
+ currency: wallet.currency,
151
+ isEnabled: wallet.isEnable
152
+ }
153
+ },
154
+ isDemo: false
155
+ };
156
+ }
157
+ function validateLoginBody(body) {
158
+ if (typeof body.access_token !== "string" || body.access_token === "") {
159
+ throw new TaphubServerError("Invalid response from server", {
160
+ code: "InvalidResponse"
161
+ });
162
+ }
163
+ if (!body.user || typeof body.user.id !== "string") {
164
+ throw new TaphubServerError("Invalid response from server", {
165
+ code: "InvalidResponse"
166
+ });
167
+ }
168
+ }
169
+
170
+ // src/modules/auth/queries.ts
171
+ var CREATE_DEMO_USER_MUTATION = `
172
+ mutation CreateDemoUser($username: String) {
173
+ createDemoUser(username: $username) {
174
+ token
175
+ user {
176
+ id
177
+ username
178
+ wallet_address
179
+ balance
180
+ is_demo
181
+ }
182
+ }
183
+ }
184
+ `;
185
+ var USER_LOGIN_MUTATION = `
186
+ mutation UserLogin($input: LoginInput!) {
187
+ userLogin(input: $input) {
188
+ accessToken
189
+ user {
190
+ id
191
+ agencyUid
192
+ name
193
+ currency
194
+ defaultWallet {
195
+ id
196
+ amount
197
+ currency
198
+ isEnable
199
+ }
200
+ }
201
+ }
202
+ }
203
+ `;
204
+
205
+ // src/modules/auth/index.ts
206
+ var AuthModule = class {
207
+ #rest;
208
+ #graphql;
209
+ #graphqlUser;
210
+ #setToken;
211
+ #agencyId;
212
+ #onLoginSuccess;
213
+ #onLogout;
214
+ constructor(deps) {
215
+ this.#rest = deps.rest;
216
+ this.#graphql = deps.graphql;
217
+ this.#graphqlUser = deps.graphqlUser;
218
+ this.#setToken = deps.setToken;
219
+ this.#agencyId = deps.agencyId;
220
+ this.#onLoginSuccess = deps.onLoginSuccess;
221
+ this.#onLogout = deps.onLogout;
222
+ }
223
+ /** @deprecated REST auth endpoints are being retired; use `loginWithSession` instead. */
224
+ async loginWithGoogle(idToken) {
225
+ const body = {
226
+ id_token: idToken,
227
+ aid: this.#agencyId
228
+ };
229
+ const response = await this.#rest.post("/auth/google", body);
230
+ validateLoginBody(response);
231
+ const result = normaliseGoogleResponse(response);
232
+ this.#setToken(result.accessToken, { isDemo: false });
233
+ if (this.#onLoginSuccess) {
234
+ try {
235
+ await this.#onLoginSuccess();
236
+ } catch {
237
+ }
238
+ }
239
+ return result;
240
+ }
241
+ /** @deprecated REST auth endpoints are being retired; use `loginWithSession` or `createDemoUser` instead. */
242
+ async demoLogin(username) {
243
+ const body = username !== void 0 ? { aid: this.#agencyId, username } : { aid: this.#agencyId };
244
+ const response = await this.#rest.post("/demo-login", body);
245
+ validateLoginBody(response);
246
+ const result = normaliseDemoResponse(response);
247
+ this.#setToken(result.accessToken, { isDemo: true });
248
+ if (this.#onLoginSuccess) {
249
+ try {
250
+ await this.#onLoginSuccess();
251
+ } catch {
252
+ }
253
+ }
254
+ return result;
255
+ }
256
+ async createDemoUser(username, opts) {
257
+ const variables = username !== void 0 ? { username } : {};
258
+ const body = await this.#graphql.request(
259
+ CREATE_DEMO_USER_MUTATION,
260
+ variables,
261
+ opts
262
+ );
263
+ const result = normaliseCreateDemoUserResponse(body);
264
+ this.#setToken(result.accessToken, { isDemo: true });
265
+ if (this.#onLoginSuccess) {
266
+ try {
267
+ await this.#onLoginSuccess();
268
+ } catch {
269
+ }
270
+ }
271
+ return result;
272
+ }
273
+ async loginWithSession(sessionToken, opts) {
274
+ const variables = { input: { sessionToken, agencyId: this.#agencyId } };
275
+ const body = await this.#graphqlUser.request(
276
+ USER_LOGIN_MUTATION,
277
+ variables,
278
+ opts
279
+ );
280
+ const result = normaliseUserLoginResponse(body);
281
+ this.#setToken(result.accessToken, { isDemo: false });
282
+ if (this.#onLoginSuccess) {
283
+ try {
284
+ await this.#onLoginSuccess();
285
+ } catch {
286
+ }
287
+ }
288
+ return result;
289
+ }
290
+ async logout() {
291
+ this.#setToken(null);
292
+ if (this.#onLogout) {
293
+ try {
294
+ await this.#onLogout();
295
+ } catch {
296
+ }
297
+ }
298
+ }
299
+ };
300
+
301
+ // src/modules/bid/normalise.ts
302
+ var VALID_STATUSES = /* @__PURE__ */ new Set(["pending", "win", "lose", "cancelled"]);
303
+ var PAST_TENSE_MAP = {
304
+ won: "win",
305
+ lost: "lose"
306
+ };
307
+ function coerceStatus(raw) {
308
+ if (VALID_STATUSES.has(raw)) return raw;
309
+ const mapped = PAST_TENSE_MAP[raw];
310
+ if (mapped) return mapped;
311
+ console.warn(`Unknown bid status "${raw}", defaulting to "pending"`);
312
+ return "pending";
313
+ }
314
+ function normaliseBid(node) {
315
+ if (typeof node.id !== "string" || node.id === "" || typeof node.user_id !== "string" || node.user_id === "" || typeof node.game_id !== "string" || node.game_id === "" || typeof node.status !== "string" || node.status === "") {
316
+ throw new TaphubServerError("Invalid response from server", {
317
+ code: "InvalidResponse"
318
+ });
319
+ }
320
+ return {
321
+ id: node.id,
322
+ userId: node.user_id,
323
+ gameId: node.game_id,
324
+ currency: node.currency,
325
+ amount: node.amount,
326
+ coefficient: node.coefficient,
327
+ time1: node.time1,
328
+ time2: node.time2,
329
+ price1: node.price1,
330
+ price2: node.price2,
331
+ status: coerceStatus(node.status),
332
+ payout: node.payout || null,
333
+ slippage: node.slippage,
334
+ createdAt: node.created_at || null
335
+ };
336
+ }
337
+ function normaliseBids(list) {
338
+ if (!Array.isArray(list)) {
339
+ throw new TaphubServerError("Invalid response from server", {
340
+ code: "InvalidResponse"
341
+ });
342
+ }
343
+ return list.map(normaliseBid);
344
+ }
345
+
346
+ // src/modules/bid/queries.ts
347
+ var PLACE_BID_MUTATION = `mutation PlaceBid($input: PlaceBidInput!) {
348
+ placeBid(input: $input) {
349
+ id user_id game_id currency amount coefficient time1 time2 price1 price2 status payout slippage created_at
350
+ }
351
+ }`;
352
+ var MY_BIDS_QUERY = `query MyBids($status: String, $limit: Int, $offset: Int, $gameId: ID) {
353
+ myBids(status: $status, limit: $limit, offset: $offset, gameId: $gameId) {
354
+ id user_id game_id currency amount coefficient time1 time2 price1 price2 status payout slippage created_at
355
+ }
356
+ }`;
357
+ var CANCEL_BID_MUTATION = `mutation CancelBid($input: CancelBidInput!) {
358
+ cancelBid(input: $input) {
359
+ bid { id user_id game_id currency amount coefficient time1 time2 price1 price2 status cancelled payout slippage created_at }
360
+ refund_amount
361
+ new_balance
362
+ }
363
+ }`;
364
+
365
+ // src/modules/bid/classifiers.ts
366
+ var isPending = (bid) => bid.status === "pending";
367
+ var isTerminal = (bid) => bid.status !== "pending";
368
+ var isWin = (bid) => bid.status === "win";
369
+ var isLoss = (bid) => bid.status === "lose";
370
+ var isCancelled = (bid) => bid.status === "cancelled";
371
+
372
+ // src/modules/bid/index.ts
373
+ var BidModule = class {
374
+ #graphql;
375
+ constructor(deps) {
376
+ this.#graphql = deps.graphql;
377
+ }
378
+ async placeBid(input, opts) {
379
+ const variables = {
380
+ input: {
381
+ game_id: input.gameId,
382
+ // wallet_id is forwarded as-is — grid-api enforces ownership +
383
+ // currency validation. See openspec/bid-260518-wallet-id D1/D8.
384
+ wallet_id: input.walletId,
385
+ time1: input.time1,
386
+ time2: input.time2,
387
+ price1: input.price1,
388
+ price2: input.price2,
389
+ coefficient: input.coefficient,
390
+ amount: input.amount,
391
+ slippage: input.slippage
392
+ }
393
+ };
394
+ try {
395
+ const body = await this.#graphql.request(
396
+ PLACE_BID_MUTATION,
397
+ variables,
398
+ opts
399
+ );
400
+ return normaliseBid(body.placeBid);
401
+ } catch (err) {
402
+ if (err instanceof TaphubValidationError && err.code === "NotExist") {
403
+ throw new TaphubValidationError("Game not found", {
404
+ code: "GameNotFound",
405
+ details: { gameId: input.gameId }
406
+ });
407
+ }
408
+ throw err;
409
+ }
410
+ }
411
+ async cancelBid(bidId, opts) {
412
+ const body = await this.#graphql.request(
413
+ CANCEL_BID_MUTATION,
414
+ { input: { bid_id: bidId } },
415
+ opts
416
+ );
417
+ const { bid, refund_amount, new_balance } = body.cancelBid;
418
+ return {
419
+ bid: normaliseBid(bid),
420
+ refundAmount: refund_amount,
421
+ newBalance: new_balance
422
+ };
423
+ }
424
+ async listBids(opts) {
425
+ const variables = {};
426
+ if (opts?.status !== void 0) {
427
+ variables.status = opts.status;
428
+ }
429
+ if (opts?.limit !== void 0) {
430
+ variables.limit = opts.limit;
431
+ }
432
+ if (opts?.offset !== void 0) {
433
+ variables.offset = opts.offset;
434
+ }
435
+ if (opts?.gameId !== void 0) {
436
+ variables.gameId = opts.gameId;
437
+ }
438
+ const body = await this.#graphql.request(
439
+ MY_BIDS_QUERY,
440
+ variables,
441
+ opts?.signal ? { signal: opts.signal } : void 0
442
+ );
443
+ return normaliseBids(body.myBids);
444
+ }
445
+ };
446
+
447
+ // src/modules/game/normalise.ts
448
+ function normaliseGame(node) {
449
+ if (typeof node.id !== "string" || node.id === "" || typeof node.pair !== "string" || node.pair === "" || typeof node.status !== "string" || node.status === "" || !node.config) {
450
+ throw new TaphubServerError("Invalid response from server", {
451
+ code: "InvalidResponse"
452
+ });
453
+ }
454
+ return {
455
+ id: node.id,
456
+ pair: node.pair,
457
+ status: node.status,
458
+ config: normaliseGameConfig(node.config),
459
+ createdAt: node.created_at || null
460
+ };
461
+ }
462
+ function normaliseGameConfig(node) {
463
+ return {
464
+ gridConfig: normaliseGridConfig(node.gridConfig),
465
+ constraints: normaliseConstraints(node.constraints),
466
+ acceptableBids: node.acceptableBids,
467
+ minBidAmount: node.minBidAmount,
468
+ maxBidAmount: node.maxBidAmount
469
+ };
470
+ }
471
+ function normaliseGridConfig(node) {
472
+ return {
473
+ cellSizeTime: node.cellSizeTime,
474
+ cellSizeValue: node.cellSizeValue,
475
+ candleSize: node.candleSize,
476
+ baseline: node.baseline,
477
+ baselineTime: node.baselineTime
478
+ };
479
+ }
480
+ function normaliseConstraints(node) {
481
+ return {
482
+ minBetTime: node.minBetTime,
483
+ maxBetTime: node.maxBetTime,
484
+ slippage: node.slippage,
485
+ priceMinRange: node.priceMinRange,
486
+ priceMaxRange: node.priceMaxRange,
487
+ coefMults: node.coefMults,
488
+ maxCoef: node.maxCoef
489
+ };
490
+ }
491
+ function normaliseCandles(list) {
492
+ if (!Array.isArray(list)) {
493
+ throw new TaphubServerError("Invalid response from server", {
494
+ code: "InvalidResponse"
495
+ });
496
+ }
497
+ return list.map((c) => ({
498
+ time: c.time,
499
+ o: c.o,
500
+ h: c.h,
501
+ l: c.l,
502
+ c: c.c,
503
+ volatility: c.volatility,
504
+ coefMults: c.coefMults
505
+ }));
506
+ }
507
+ function normaliseGamePairInfo(node) {
508
+ if (typeof node.id !== "string" || node.id === "" || typeof node.pair !== "string" || node.pair === "") {
509
+ throw new TaphubServerError("Invalid response from server", { code: "InvalidResponse" });
510
+ }
511
+ return {
512
+ id: node.id,
513
+ pair: node.pair,
514
+ gameplayId: node.gameplayId,
515
+ gameplayName: node.gameplayName,
516
+ source: node.source,
517
+ gameId: node.gameId ?? null
518
+ };
519
+ }
520
+
521
+ // src/modules/game/queries.ts
522
+ var GAME_QUERY = `query Game($pair: String!, $gameplaySlug: String) {
523
+ game(pair: $pair, gameplaySlug: $gameplaySlug) {
524
+ id pair status created_at
525
+ config {
526
+ gridConfig { cellSizeTime cellSizeValue candleSize baseline baselineTime }
527
+ constraints { minBetTime maxBetTime slippage priceMinRange priceMaxRange coefMults maxCoef }
528
+ acceptableBids minBidAmount maxBidAmount
529
+ }
530
+ }
531
+ }`;
532
+ var CHART_HISTORY_QUERY = `query ChartHistory($gameId: ID!, $limit: Int) {
533
+ chartHistory(gameId: $gameId, limit: $limit) {
534
+ time o h l c volatility coefMults
535
+ }
536
+ }`;
537
+ var BUILDER_AVAILABLE_GAME_PAIRS_QUERY = `query BuilderAvailableGamePairs($gameplayId: ID) {
538
+ builderAvailableGamePairs(gameplayId: $gameplayId) {
539
+ id
540
+ pair
541
+ gameplayId
542
+ gameplayName
543
+ source
544
+ gameId
545
+ }
546
+ }`;
547
+
548
+ // src/modules/game/index.ts
549
+ var GameModule = class {
550
+ #graphql;
551
+ constructor(deps) {
552
+ this.#graphql = deps.graphql;
553
+ }
554
+ async get(pair, opts) {
555
+ const variables = { pair };
556
+ if (opts?.gameplaySlug) variables.gameplaySlug = opts.gameplaySlug;
557
+ let body;
558
+ try {
559
+ body = await this.#graphql.request(GAME_QUERY, variables, opts);
560
+ } catch (err) {
561
+ if (err instanceof TaphubValidationError && err.code === "NotExist") {
562
+ throw new TaphubValidationError("Game not found for pair", {
563
+ code: "GameNotFound",
564
+ details: { pair }
565
+ });
566
+ }
567
+ throw err;
568
+ }
569
+ if (body.game === null) {
570
+ throw new TaphubValidationError("Game not found for pair", {
571
+ code: "GameNotFound",
572
+ details: { pair }
573
+ });
574
+ }
575
+ return normaliseGame(body.game);
576
+ }
577
+ /**
578
+ * Returns available game pairs, optionally filtered by gameplay.
579
+ *
580
+ * When called with a valid JWT (authenticated builder), each entry includes
581
+ * `gameId` — use it directly as the MQTT topic `game/{gameId}/candle`.
582
+ *
583
+ * @example
584
+ * const pairs = await client.game.availableGamePairs({ gameplayId: 'taptrading' });
585
+ * const game = await client.game.get(pairs[0].pair, { gameplaySlug: pairs[0].gameplayId });
586
+ * const ch = client.realtime?.subscribe(pairs[0].gameId ?? game.id, userId);
587
+ */
588
+ async availableGamePairs(opts) {
589
+ const variables = {};
590
+ if (opts?.gameplayId) variables.gameplayId = opts.gameplayId;
591
+ const body = await this.#graphql.request(
592
+ BUILDER_AVAILABLE_GAME_PAIRS_QUERY,
593
+ variables,
594
+ opts
595
+ );
596
+ return body.builderAvailableGamePairs.map(normaliseGamePairInfo);
597
+ }
598
+ async chartHistory(gameId, limit, opts) {
599
+ const variables = { gameId };
600
+ if (limit !== void 0) {
601
+ variables.limit = limit;
602
+ }
603
+ const body = await this.#graphql.request(
604
+ CHART_HISTORY_QUERY,
605
+ variables,
606
+ opts
607
+ );
608
+ return normaliseCandles(body.chartHistory);
609
+ }
610
+ };
611
+
612
+ // src/modules/leaderboard/normalise.ts
613
+ function normaliseLeaderboard(rows) {
614
+ if (!Array.isArray(rows)) {
615
+ throw new TaphubServerError("Invalid response from server", {
616
+ code: "InvalidResponse"
617
+ });
618
+ }
619
+ return rows.map((row) => ({
620
+ userId: row.user_id,
621
+ username: row.username,
622
+ rank: row.rank,
623
+ totalBids: row.total_bids,
624
+ totalWins: row.total_wins,
625
+ totalWagered: row.total_wagered,
626
+ totalPayout: row.total_payout,
627
+ gain: row.gain
628
+ }));
629
+ }
630
+
631
+ // src/modules/leaderboard/queries.ts
632
+ var LEADERBOARD_QUERY = `query Leaderboard($period: String!, $sort_by: String!) {
633
+ leaderboard(period: $period, sort_by: $sort_by) {
634
+ user_id username rank total_bids total_wins total_wagered total_payout gain
635
+ }
636
+ }`;
637
+
638
+ // src/modules/leaderboard/index.ts
639
+ var LeaderboardModule = class {
640
+ #graphql;
641
+ constructor(deps) {
642
+ this.#graphql = deps.graphql;
643
+ }
644
+ async list(args) {
645
+ const body = await this.#graphql.request(
646
+ LEADERBOARD_QUERY,
647
+ { period: args.period, sort_by: args.sortBy },
648
+ args.signal ? { signal: args.signal } : void 0
649
+ );
650
+ return normaliseLeaderboard(body.leaderboard);
651
+ }
652
+ };
653
+
654
+ // src/modules/locale/normalise.ts
655
+ function normaliseLocaleResponse(node) {
656
+ let translations = null;
657
+ if (node.translations !== null) {
658
+ try {
659
+ translations = JSON.parse(node.translations);
660
+ } catch (err) {
661
+ throw new TaphubValidationError("Malformed translations JSON string from backend", {
662
+ code: "InvalidLocaleResponse",
663
+ details: {
664
+ lang: node.lang,
665
+ version: node.version,
666
+ parseError: err instanceof Error ? err.message : String(err)
667
+ }
668
+ });
669
+ }
670
+ }
671
+ return {
672
+ lang: node.lang,
673
+ version: node.version,
674
+ notModified: node.notModified,
675
+ translations
676
+ };
677
+ }
678
+ function normaliseRefreshLocalesPayload(node) {
679
+ let versions;
680
+ try {
681
+ versions = JSON.parse(node.versions);
682
+ } catch (err) {
683
+ throw new TaphubValidationError("Malformed versions JSON string from backend", {
684
+ code: "InvalidRefreshLocalesPayload",
685
+ details: {
686
+ parseError: err instanceof Error ? err.message : String(err)
687
+ }
688
+ });
689
+ }
690
+ return {
691
+ refreshed: node.refreshed,
692
+ versions
693
+ };
694
+ }
695
+
696
+ // src/modules/locale/queries.ts
697
+ var LOCALES_QUERY = `query Locales($input: LocalesInput!) {
698
+ locales(input: $input) {
699
+ lang
700
+ version
701
+ notModified
702
+ translations
703
+ }
704
+ }`;
705
+ var REFRESH_LOCALES_MUTATION = `mutation RefreshLocales {
706
+ refreshLocales {
707
+ refreshed
708
+ versions
709
+ }
710
+ }`;
711
+
712
+ // src/modules/locale/index.ts
713
+ var LocaleModule = class {
714
+ #graphql;
715
+ constructor(deps) {
716
+ this.#graphql = deps.graphql;
717
+ }
718
+ /**
719
+ * Fetch translations for a single language.
720
+ *
721
+ * Pass the last-seen `version` as `knownVersion` to opt into not-modified
722
+ * short-circuit semantics: backend returns `{notModified: true, translations: null}`
723
+ * when the cached body still matches, and the caller keeps prior state.
724
+ *
725
+ * Errors surface as TaphubError subclasses with `extensions.code` codes from
726
+ * the backend (e.g. `LangNotSupported`, `InsufficientUpstream`, `FeatureDisabled`).
727
+ * Caller decides the fallback strategy.
728
+ */
729
+ async get(lang, knownVersion, opts) {
730
+ const input = { lang };
731
+ if (knownVersion !== void 0 && knownVersion !== "") {
732
+ input.knownVersion = knownVersion;
733
+ }
734
+ const body = await this.#graphql.request(LOCALES_QUERY, { input }, opts);
735
+ return normaliseLocaleResponse(body.locales);
736
+ }
737
+ /**
738
+ * Trigger an admin refresh — backend re-pulls the source Sheet, invalidates
739
+ * its cache, and writes fresh entries for every supported language.
740
+ *
741
+ * Requires `X-API-Key` header equal to backend `InternalApiKey`. The transport
742
+ * layer is responsible for attaching the header; this method does not handle
743
+ * auth concerns directly.
744
+ */
745
+ async refresh(opts) {
746
+ const body = await this.#graphql.request(
747
+ REFRESH_LOCALES_MUTATION,
748
+ {},
749
+ opts
750
+ );
751
+ return normaliseRefreshLocalesPayload(body.refreshLocales);
752
+ }
753
+ };
754
+
755
+ // src/transport/mqtt/index.ts
756
+ import mqtt from "mqtt";
757
+ var GAME_SCOPED_SUFFIXES = ["candle", "config", "ideal_config"];
758
+ var USER_SCOPED_SUFFIXES = ["bid_result", "balance_update"];
759
+ var TOPIC_PREFIX = "game";
760
+ function topicFor(gameId, suffix, userId) {
761
+ if (USER_SCOPED_SUFFIXES.includes(suffix)) {
762
+ return `${TOPIC_PREFIX}/${gameId}/user/${userId}/${suffix}`;
763
+ }
764
+ return `${TOPIC_PREFIX}/${gameId}/${suffix}`;
765
+ }
766
+ function normaliseUserId(userId) {
767
+ return userId && userId !== "" ? userId : null;
768
+ }
769
+ function userScopedTopicsFor(gameId, userId) {
770
+ return USER_SCOPED_SUFFIXES.map((s) => topicFor(gameId, s, userId));
771
+ }
772
+ function subKey(gameId, userId) {
773
+ return `${gameId}::${userId ?? ""}`;
774
+ }
775
+ function createMqttTransport(endpoint, opts = {}) {
776
+ let client = null;
777
+ const subscriptions = /* @__PURE__ */ new Map();
778
+ const { onLifecycle } = opts;
779
+ let connectStartedAt = 0;
780
+ function fireLifecycle(event) {
781
+ if (!onLifecycle) return;
782
+ try {
783
+ onLifecycle(event);
784
+ } catch {
785
+ }
786
+ }
787
+ function ensureConnected() {
788
+ if (client) return client;
789
+ connectStartedAt = Date.now();
790
+ client = mqtt.connect(endpoint, {
791
+ clean: true,
792
+ reconnectPeriod: 2e3,
793
+ connectTimeout: 1e4,
794
+ keepalive: 6
795
+ });
796
+ let lastPingreqAt = 0;
797
+ client.on("packetsend", (packet) => {
798
+ if (packet.cmd === "pingreq") {
799
+ lastPingreqAt = Date.now();
800
+ }
801
+ });
802
+ client.on("packetreceive", (packet) => {
803
+ if (packet.cmd === "pingresp" && lastPingreqAt > 0) {
804
+ const rttMs = Date.now() - lastPingreqAt;
805
+ lastPingreqAt = 0;
806
+ fireLifecycle({ kind: "ping", rttMs });
807
+ }
808
+ });
809
+ client.on("connect", () => {
810
+ fireLifecycle({ kind: "connect", rttMs: Date.now() - connectStartedAt });
811
+ });
812
+ client.on("reconnect", () => {
813
+ const rttMs = Date.now() - connectStartedAt;
814
+ connectStartedAt = Date.now();
815
+ fireLifecycle({ kind: "reconnect", rttMs });
816
+ });
817
+ client.on("close", () => {
818
+ fireLifecycle({ kind: "disconnect" });
819
+ });
820
+ client.on("error", (err) => {
821
+ fireLifecycle({ kind: "error", err });
822
+ });
823
+ client.on("message", (receivedTopic, message) => {
824
+ let matched;
825
+ for (const sub of subscriptions.values()) {
826
+ if (sub.topics.includes(receivedTopic)) {
827
+ matched = sub;
828
+ break;
829
+ }
830
+ }
831
+ if (!matched) return;
832
+ let payload;
833
+ try {
834
+ payload = JSON.parse(message.toString());
835
+ } catch {
836
+ matched.onError(new Error(`Invalid JSON on topic ${receivedTopic}`));
837
+ return;
838
+ }
839
+ matched.onMessage(matched.gameId, receivedTopic, payload);
840
+ });
841
+ return client;
842
+ }
843
+ function entriesForGame(gameId) {
844
+ const out = [];
845
+ for (const sub of subscriptions.values()) {
846
+ if (sub.gameId === gameId) out.push(sub);
847
+ }
848
+ return out;
849
+ }
850
+ return {
851
+ subscribe(gameId, userId, onMessage, onError) {
852
+ const cleanUserId = normaliseUserId(userId);
853
+ const key = subKey(gameId, cleanUserId);
854
+ if (subscriptions.has(key)) return;
855
+ const mqttClient = ensureConnected();
856
+ const fullTopics = [
857
+ ...GAME_SCOPED_SUFFIXES.map((s) => topicFor(gameId, s)),
858
+ ...cleanUserId ? userScopedTopicsFor(gameId, cleanUserId) : []
859
+ ];
860
+ subscriptions.set(key, {
861
+ gameId,
862
+ topics: fullTopics,
863
+ userId: cleanUserId,
864
+ onMessage,
865
+ onError
866
+ });
867
+ for (const t of fullTopics) {
868
+ mqttClient.subscribe(t, { qos: t.endsWith("candle") ? 0 : 1 });
869
+ }
870
+ },
871
+ unsubscribeAll(gameId, userId) {
872
+ const matches = entriesForGame(gameId);
873
+ if (matches.length === 0) return;
874
+ let target;
875
+ if (userId !== void 0) {
876
+ const cleanUserId = normaliseUserId(userId);
877
+ target = matches.find((m) => m.userId === cleanUserId);
878
+ if (!target) return;
879
+ } else if (matches.length === 1) {
880
+ target = matches[0];
881
+ } else {
882
+ const activeUserIds = matches.map((m) => m.userId ?? "<anonymous>");
883
+ throw new TaphubError(
884
+ `Ambiguous unsubscribe for gameId='${gameId}': multiple active userIds (${activeUserIds.join(", ")}). Pass userId explicitly to disambiguate.`,
885
+ { code: "AmbiguousUnsubscribe", details: { gameId, activeUserIds } }
886
+ );
887
+ }
888
+ if (client) {
889
+ for (const t of target.topics) {
890
+ client.unsubscribe(t);
891
+ }
892
+ }
893
+ subscriptions.delete(subKey(target.gameId, target.userId));
894
+ },
895
+ close() {
896
+ if (client) {
897
+ for (const sub of subscriptions.values()) {
898
+ for (const t of sub.topics) {
899
+ client.unsubscribe(t);
900
+ }
901
+ }
902
+ }
903
+ subscriptions.clear();
904
+ if (client) {
905
+ client.end(true);
906
+ client = null;
907
+ }
908
+ }
909
+ };
910
+ }
911
+
912
+ // src/modules/realtime/index.ts
913
+ import EventEmitter2 from "eventemitter3";
914
+
915
+ // src/modules/realtime/GameChannel.ts
916
+ import EventEmitter from "eventemitter3";
917
+ var GameChannel = class extends EventEmitter {
918
+ gameId;
919
+ constructor(gameId) {
920
+ super();
921
+ this.gameId = gameId;
922
+ }
923
+ };
924
+
925
+ // src/modules/realtime/index.ts
926
+ var TOPIC_SUFFIX_CANDLE = "candle";
927
+ var TOPIC_SUFFIX_BID_RESULT = "bid_result";
928
+ var TOPIC_SUFFIX_BALANCE_UPDATE = "balance_update";
929
+ var TOPIC_SUFFIX_CONFIG = "config";
930
+ var TOPIC_SUFFIX_IDEAL_CONFIG = "ideal_config";
931
+ function mapWireCandle(raw) {
932
+ const p = raw;
933
+ const c = p.candle ?? p;
934
+ const result = {
935
+ type: p.type,
936
+ time: c.time,
937
+ o: c.o,
938
+ h: c.h,
939
+ l: c.l,
940
+ c: c.c
941
+ };
942
+ if (c.volatility !== void 0) result.volatility = c.volatility;
943
+ if (c.coef_mults !== void 0) result.coefMults = c.coef_mults;
944
+ if (c.coefMults !== void 0) result.coefMults = c.coefMults;
945
+ return result;
946
+ }
947
+ function mapWireBidResult(raw) {
948
+ const p = raw;
949
+ switch (p.type) {
950
+ case "accepted": {
951
+ const data = {
952
+ bid: p.bid
953
+ };
954
+ if (typeof p.balance === "string") data.balance = p.balance;
955
+ return { event: "bidAccepted", data };
956
+ }
957
+ case "won": {
958
+ const data = {
959
+ bidId: p.bidId,
960
+ payout: p.payout,
961
+ userId: p.user_id
962
+ };
963
+ if (typeof p.balance === "string") data.balance = p.balance;
964
+ return { event: "bidWon", data };
965
+ }
966
+ case "lost": {
967
+ return {
968
+ event: "bidLost",
969
+ data: { bidId: p.bidId }
970
+ };
971
+ }
972
+ default:
973
+ console.warn(`Unknown bid_result type "${String(p.type)}"`);
974
+ return null;
975
+ }
976
+ }
977
+ function mapWireBalanceUpdate(raw) {
978
+ const p = raw;
979
+ return {
980
+ userId: p.userId,
981
+ balance: p.balance
982
+ };
983
+ }
984
+ function mapWireConfig(raw) {
985
+ const p = raw;
986
+ return {
987
+ minBidAmount: p.min_bid_amount,
988
+ maxBidAmount: p.max_bid_amount,
989
+ acceptableBids: p.acceptable_bids
990
+ };
991
+ }
992
+ function mapWireIdealConfig(raw) {
993
+ const p = raw;
994
+ return {
995
+ cellSizeValue: p.cellSizeValue,
996
+ currentPrice: p.currentPrice,
997
+ reason: p.reason ?? "volatility_shift"
998
+ };
999
+ }
1000
+ function mapWireToEvent(topic, payload) {
1001
+ if (topic.endsWith(`/${TOPIC_SUFFIX_CANDLE}`)) {
1002
+ return { event: "candle", data: mapWireCandle(payload) };
1003
+ }
1004
+ if (topic.endsWith(`/${TOPIC_SUFFIX_BID_RESULT}`)) {
1005
+ return mapWireBidResult(payload);
1006
+ }
1007
+ if (topic.endsWith(`/${TOPIC_SUFFIX_BALANCE_UPDATE}`)) {
1008
+ return { event: "balanceUpdate", data: mapWireBalanceUpdate(payload) };
1009
+ }
1010
+ if (topic.endsWith(`/${TOPIC_SUFFIX_IDEAL_CONFIG}`)) {
1011
+ return { event: "idealConfigUpdate", data: mapWireIdealConfig(payload) };
1012
+ }
1013
+ if (topic.endsWith(`/${TOPIC_SUFFIX_CONFIG}`)) {
1014
+ return { event: "configUpdate", data: mapWireConfig(payload) };
1015
+ }
1016
+ return null;
1017
+ }
1018
+ function normaliseUserId2(userId) {
1019
+ return userId && userId !== "" ? userId : null;
1020
+ }
1021
+ function subKey2(gameId, userId) {
1022
+ return `${gameId}::${userId ?? ""}`;
1023
+ }
1024
+ var RealtimeModule = class extends EventEmitter2 {
1025
+ #transport;
1026
+ #entries = /* @__PURE__ */ new Map();
1027
+ constructor(mqttEndpointOrOptions) {
1028
+ super();
1029
+ if (typeof mqttEndpointOrOptions === "string") {
1030
+ this.#transport = createMqttTransport(mqttEndpointOrOptions);
1031
+ } else {
1032
+ this.#transport = mqttEndpointOrOptions.transport ?? createMqttTransport(mqttEndpointOrOptions.mqttEndpoint, {
1033
+ onLifecycle: mqttEndpointOrOptions.onMqttLifecycle
1034
+ });
1035
+ }
1036
+ }
1037
+ /** @internal Access transport for testing */
1038
+ get _transport() {
1039
+ return this.#transport;
1040
+ }
1041
+ subscribe(gameId, userId) {
1042
+ const cleanUserId = normaliseUserId2(userId);
1043
+ const key = subKey2(gameId, cleanUserId);
1044
+ const existing = this.#entries.get(key);
1045
+ if (existing) {
1046
+ existing.refcount += 1;
1047
+ return existing.channel;
1048
+ }
1049
+ const channel = new GameChannel(gameId);
1050
+ const onMessage = (_gid, topic, payload) => {
1051
+ const mapped = mapWireToEvent(topic, payload);
1052
+ if (mapped) {
1053
+ if (mapped.event === "candle") channel.emit("candle", mapped.data);
1054
+ else if (mapped.event === "bidAccepted") channel.emit("bidAccepted", mapped.data);
1055
+ else if (mapped.event === "bidWon") channel.emit("bidWon", mapped.data);
1056
+ else if (mapped.event === "bidLost") channel.emit("bidLost", mapped.data);
1057
+ else if (mapped.event === "balanceUpdate") channel.emit("balanceUpdate", mapped.data);
1058
+ else if (mapped.event === "configUpdate") channel.emit("configUpdate", mapped.data);
1059
+ else if (mapped.event === "idealConfigUpdate")
1060
+ channel.emit("idealConfigUpdate", mapped.data);
1061
+ }
1062
+ };
1063
+ const onError = (err) => {
1064
+ channel.emit("error", err);
1065
+ };
1066
+ this.#entries.set(key, {
1067
+ gameId,
1068
+ userId: cleanUserId,
1069
+ channel,
1070
+ refcount: 1,
1071
+ onMessage,
1072
+ onError
1073
+ });
1074
+ this.#transport.subscribe(gameId, cleanUserId, onMessage, onError);
1075
+ return channel;
1076
+ }
1077
+ unsubscribe(gameId, userId) {
1078
+ const matches = [];
1079
+ for (const entry of this.#entries.values()) {
1080
+ if (entry.gameId === gameId) matches.push(entry);
1081
+ }
1082
+ if (matches.length === 0) return;
1083
+ let target;
1084
+ if (userId !== void 0) {
1085
+ const cleanUserId = normaliseUserId2(userId);
1086
+ target = matches.find((m) => m.userId === cleanUserId);
1087
+ if (!target) return;
1088
+ } else if (matches.length === 1) {
1089
+ target = matches[0];
1090
+ } else {
1091
+ const activeUserIds = matches.map((m) => m.userId ?? "<anonymous>");
1092
+ throw new TaphubError(
1093
+ `Ambiguous unsubscribe for gameId='${gameId}': multiple active userIds (${activeUserIds.join(", ")}). Pass userId explicitly to disambiguate.`,
1094
+ { code: "AmbiguousUnsubscribe", details: { gameId, activeUserIds } }
1095
+ );
1096
+ }
1097
+ target.refcount -= 1;
1098
+ if (target.refcount > 0) return;
1099
+ const key = subKey2(target.gameId, target.userId);
1100
+ this.#entries.delete(key);
1101
+ target.channel.removeAllListeners();
1102
+ this.#transport.unsubscribeAll(gameId, target.userId);
1103
+ }
1104
+ disconnect() {
1105
+ for (const entry of this.#entries.values()) {
1106
+ entry.channel.removeAllListeners();
1107
+ }
1108
+ this.#entries.clear();
1109
+ this.#transport.close();
1110
+ }
1111
+ };
1112
+
1113
+ // src/modules/user/normalise.ts
1114
+ function normaliseMeResponse(body) {
1115
+ const me = body.me;
1116
+ if (typeof me.id !== "string" || me.id === "" || typeof me.username !== "string" || me.username === "" || typeof me.balance !== "string" || me.balance === "" || typeof me.is_demo !== "boolean") {
1117
+ throw new TaphubServerError("Invalid response from server", {
1118
+ code: "InvalidResponse"
1119
+ });
1120
+ }
1121
+ const walletAddress = me.wallet_address || null;
1122
+ return {
1123
+ id: me.id,
1124
+ username: me.username,
1125
+ walletAddress,
1126
+ balance: me.balance,
1127
+ isDemo: me.is_demo
1128
+ };
1129
+ }
1130
+ function normaliseCurrencies(body) {
1131
+ if (!Array.isArray(body)) {
1132
+ throw new TaphubServerError("Invalid response from server", {
1133
+ code: "InvalidResponse"
1134
+ });
1135
+ }
1136
+ return body.map((entry) => ({
1137
+ code: entry.code,
1138
+ unit: entry.unit,
1139
+ unitSymbol: entry.unit_symbol
1140
+ }));
1141
+ }
1142
+ function normaliseWalletNode(node) {
1143
+ if (node === null || typeof node !== "object") {
1144
+ throw new TaphubServerError("Invalid response from server", {
1145
+ code: "InvalidResponse"
1146
+ });
1147
+ }
1148
+ const w = node;
1149
+ if (typeof w.id !== "string" || w.id === "" || typeof w.amount !== "string" || w.amount === "" || typeof w.currency !== "string" || w.currency === "" || typeof w.isEnable !== "boolean") {
1150
+ throw new TaphubServerError("Invalid response from server", {
1151
+ code: "InvalidResponse"
1152
+ });
1153
+ }
1154
+ return {
1155
+ id: w.id,
1156
+ amount: w.amount,
1157
+ currency: w.currency,
1158
+ isEnable: w.isEnable
1159
+ };
1160
+ }
1161
+ function normaliseMyWalletsResponse(body) {
1162
+ if (!body || !Array.isArray(body.myWallets)) {
1163
+ throw new TaphubServerError("Invalid response from server", {
1164
+ code: "InvalidResponse"
1165
+ });
1166
+ }
1167
+ return body.myWallets.map(normaliseWalletNode);
1168
+ }
1169
+ function normaliseMyWalletByCurrencyResponse(body) {
1170
+ if (!body) {
1171
+ throw new TaphubServerError("Invalid response from server", {
1172
+ code: "InvalidResponse"
1173
+ });
1174
+ }
1175
+ return normaliseWalletNode(body.myWalletByCurrency);
1176
+ }
1177
+
1178
+ // src/modules/user/queries.ts
1179
+ var ME_QUERY = "query Me { me { id username wallet_address balance is_demo } }";
1180
+ var MY_WALLETS_QUERY = "query MyWallets { myWallets { id amount currency isEnable } }";
1181
+ var MY_WALLET_BY_CURRENCY_QUERY = "query MyWalletByCurrency($currency: String!) { myWalletByCurrency(currency: $currency) { id amount currency isEnable } }";
1182
+
1183
+ // src/modules/user/index.ts
1184
+ var UserModule = class {
1185
+ #rest;
1186
+ #graphql;
1187
+ #graphqlUser;
1188
+ #currencies = null;
1189
+ constructor(deps) {
1190
+ this.#rest = deps.rest;
1191
+ this.#graphql = deps.graphql;
1192
+ this.#graphqlUser = deps.graphqlUser;
1193
+ }
1194
+ async me() {
1195
+ const body = await this.#graphql.request(ME_QUERY);
1196
+ return normaliseMeResponse(body);
1197
+ }
1198
+ async wallets(opts) {
1199
+ const body = await this.#graphqlUser.request(
1200
+ MY_WALLETS_QUERY,
1201
+ void 0,
1202
+ opts
1203
+ );
1204
+ return normaliseMyWalletsResponse(body);
1205
+ }
1206
+ async walletByCurrency(currency, opts) {
1207
+ const body = await this.#graphqlUser.request(
1208
+ MY_WALLET_BY_CURRENCY_QUERY,
1209
+ { currency },
1210
+ opts
1211
+ );
1212
+ return normaliseMyWalletByCurrencyResponse(body);
1213
+ }
1214
+ get currencies() {
1215
+ return this.#currencies;
1216
+ }
1217
+ async refreshCurrencies() {
1218
+ const body = await this.#rest.post("/currency/list-enable");
1219
+ const currencies = normaliseCurrencies(body);
1220
+ this.#currencies = currencies;
1221
+ return currencies;
1222
+ }
1223
+ clearCurrencies() {
1224
+ this.#currencies = null;
1225
+ }
1226
+ };
1227
+
1228
+ // src/network/classify.ts
1229
+ var TIMEOUT_MS = 15e3;
1230
+ function isAbortError(err) {
1231
+ if (err === null || err === void 0) return false;
1232
+ if (typeof err !== "object") return false;
1233
+ const name = err.name;
1234
+ return name === "AbortError";
1235
+ }
1236
+ var EMPTY_GQL_ERRORS = (errors) => !errors || Array.isArray(errors) && errors.length === 0;
1237
+ function classifyHttpOutcome(outcome, ctx) {
1238
+ if (outcome.onlineHint === false) {
1239
+ return {
1240
+ ts: ctx.now,
1241
+ rtt: Number.POSITIVE_INFINITY,
1242
+ source: ctx.source,
1243
+ reason: "offline"
1244
+ };
1245
+ }
1246
+ if (outcome.rtt > TIMEOUT_MS) {
1247
+ return {
1248
+ ts: ctx.now,
1249
+ rtt: TIMEOUT_MS,
1250
+ source: ctx.source,
1251
+ reason: "timeout"
1252
+ };
1253
+ }
1254
+ const status = outcome.httpStatus;
1255
+ if (status !== null && status !== void 0) {
1256
+ if (status >= 500) {
1257
+ return { ts: ctx.now, rtt: outcome.rtt, source: ctx.source, reason: "backend5xx" };
1258
+ }
1259
+ if (status >= 400) {
1260
+ return { ts: ctx.now, rtt: outcome.rtt, source: ctx.source, reason: "backend4xx" };
1261
+ }
1262
+ if (!EMPTY_GQL_ERRORS(outcome.gqlErrors)) {
1263
+ return { ts: ctx.now, rtt: outcome.rtt, source: ctx.source, reason: "gqlError" };
1264
+ }
1265
+ return { ts: ctx.now, rtt: outcome.rtt, source: ctx.source, reason: "ok" };
1266
+ }
1267
+ if (outcome.threw) {
1268
+ return {
1269
+ ts: ctx.now,
1270
+ rtt: outcome.rtt,
1271
+ source: ctx.source,
1272
+ reason: "network"
1273
+ };
1274
+ }
1275
+ return { ts: ctx.now, rtt: outcome.rtt, source: ctx.source, reason: "ok" };
1276
+ }
1277
+ function classifyBackendHealth(samples) {
1278
+ const httpSamples = samples.filter((s) => s.source === "graphql" || s.source === "rest");
1279
+ if (httpSamples.length === 0) return "ok";
1280
+ const total = httpSamples.length;
1281
+ const fiveXxCount = httpSamples.filter((s) => s.reason === "backend5xx").length;
1282
+ const fiveXxRate = fiveXxCount / total;
1283
+ if (fiveXxRate >= 0.5) return "down";
1284
+ if (fiveXxRate >= 0.2) return "degraded";
1285
+ const gqlSamples = httpSamples.filter((s) => s.source === "graphql");
1286
+ if (gqlSamples.length > 0) {
1287
+ const gqlErrorRate = gqlSamples.filter((s) => s.reason === "gqlError").length / gqlSamples.length;
1288
+ if (gqlErrorRate >= 0.3) return "degraded";
1289
+ }
1290
+ return "ok";
1291
+ }
1292
+ var isPoor = (m) => m.rtt > 450 || m.jitter > 150 || m.lossRate > 0.05;
1293
+ var canLeavePoor = (m) => m.rtt < 350 && m.jitter < 120 && m.lossRate < 0.03;
1294
+ var isFairUpper = (rtt) => rtt > 180;
1295
+ var canReachGood = (rtt) => rtt < 120;
1296
+ function classifyNetworkLevel(metrics, current) {
1297
+ if (current === "offline") {
1298
+ return isPoor(metrics) ? "poor" : "fair";
1299
+ }
1300
+ if (current === "good") {
1301
+ if (isPoor(metrics)) return "fair";
1302
+ if (isFairUpper(metrics.rtt)) return "fair";
1303
+ return "good";
1304
+ }
1305
+ if (current === "fair") {
1306
+ if (isPoor(metrics)) return "poor";
1307
+ if (canReachGood(metrics.rtt)) return "good";
1308
+ return "fair";
1309
+ }
1310
+ if (canLeavePoor(metrics)) return "fair";
1311
+ return "poor";
1312
+ }
1313
+
1314
+ // src/network/RttSmoother.ts
1315
+ var EMA_ALPHA = 0.25;
1316
+ var OUTLIER_MULTIPLIER = 3;
1317
+ var DEBOUNCE_DEGRADE = 2;
1318
+ var DEBOUNCE_IMPROVE = 5;
1319
+ var LEVEL_RANK = {
1320
+ good: 0,
1321
+ fair: 1,
1322
+ poor: 2,
1323
+ offline: 3
1324
+ };
1325
+ var RttSmoother = class {
1326
+ ema = 0;
1327
+ level = "good";
1328
+ committed = "good";
1329
+ candidate = "good";
1330
+ candidateCount = 0;
1331
+ overrideOffline = false;
1332
+ consecutiveOutliers = 0;
1333
+ add(rtt, metrics) {
1334
+ if (this.ema > 0 && rtt > OUTLIER_MULTIPLIER * this.ema) {
1335
+ this.consecutiveOutliers += 1;
1336
+ if (this.consecutiveOutliers < 2) return;
1337
+ } else {
1338
+ this.consecutiveOutliers = 0;
1339
+ }
1340
+ this.ema = this.ema === 0 ? rtt : EMA_ALPHA * rtt + (1 - EMA_ALPHA) * this.ema;
1341
+ const next = classifyNetworkLevel(
1342
+ { rtt, jitter: metrics.jitter, lossRate: metrics.lossRate },
1343
+ this.committed
1344
+ );
1345
+ if (next === this.candidate) {
1346
+ this.candidateCount += 1;
1347
+ } else {
1348
+ this.candidate = next;
1349
+ this.candidateCount = 1;
1350
+ }
1351
+ const required = this.requiredSamplesFor(next);
1352
+ if (this.candidateCount >= required) {
1353
+ this.committed = next;
1354
+ }
1355
+ this.level = this.overrideOffline ? "offline" : this.committed;
1356
+ }
1357
+ forceOffline() {
1358
+ this.overrideOffline = true;
1359
+ this.level = "offline";
1360
+ }
1361
+ releaseOffline() {
1362
+ this.overrideOffline = false;
1363
+ this.level = this.committed;
1364
+ }
1365
+ reset() {
1366
+ this.ema = 0;
1367
+ this.committed = "good";
1368
+ this.candidate = "good";
1369
+ this.candidateCount = 0;
1370
+ this.consecutiveOutliers = 0;
1371
+ this.overrideOffline = false;
1372
+ this.level = "good";
1373
+ }
1374
+ requiredSamplesFor(next) {
1375
+ return LEVEL_RANK[next] > LEVEL_RANK[this.committed] ? DEBOUNCE_DEGRADE : DEBOUNCE_IMPROVE;
1376
+ }
1377
+ };
1378
+
1379
+ // src/network/NetworkQualityMonitor.ts
1380
+ var WINDOW_MS = 6e4;
1381
+ var WINDOW_MAX_SAMPLES_PER_SOURCE = 30;
1382
+ var OFFLINE_HOLD_MS = 1e4;
1383
+ var TICK_INTERVAL_MS = 5e3;
1384
+ var COLD_START_REAL_SAMPLE_THRESHOLD = 3;
1385
+ var EFFECTIVE_TYPE_TO_RTT = {
1386
+ "4g": 80,
1387
+ "3g": 250,
1388
+ "2g": 700,
1389
+ "slow-2g": 1500
1390
+ };
1391
+ function emptyBuckets() {
1392
+ return { graphql: [], rest: [], mqtt: [], browser: [], connection: [] };
1393
+ }
1394
+ function isHttpSource(s) {
1395
+ return s === "graphql" || s === "rest";
1396
+ }
1397
+ var NetworkQualityMonitor = class {
1398
+ bus;
1399
+ buckets = emptyBuckets();
1400
+ smoother = new RttSmoother();
1401
+ mqttConnected = false;
1402
+ mqttDisconnectedAt = null;
1403
+ lastSuccessfulHttpAt = null;
1404
+ committedNetwork = "good";
1405
+ committedBackend = "ok";
1406
+ tickHandle = null;
1407
+ onlineListener = null;
1408
+ offlineListener = null;
1409
+ disposed = false;
1410
+ constructor(opts) {
1411
+ this.bus = opts.bus;
1412
+ this.attachBrowserListeners();
1413
+ this.startTick();
1414
+ }
1415
+ addSample(sample, prevMqttConnectedOverride) {
1416
+ if (this.disposed) return;
1417
+ const bucket = this.buckets[sample.source];
1418
+ bucket.push(sample);
1419
+ if (bucket.length > WINDOW_MAX_SAMPLES_PER_SOURCE) {
1420
+ bucket.shift();
1421
+ }
1422
+ if (sample.source !== "connection" && sample.source !== "browser") {
1423
+ if (sample.reason === "ok") {
1424
+ this.lastSuccessfulHttpAt = sample.ts;
1425
+ this.smoother.add(sample.rtt, this.deriveMetrics());
1426
+ } else if (sample.reason === "network" || sample.reason === "timeout") {
1427
+ this.smoother.add(sample.rtt, this.deriveMetrics());
1428
+ }
1429
+ }
1430
+ this.recompute(prevMqttConnectedOverride);
1431
+ }
1432
+ seedFromConnection(seed) {
1433
+ const rtt = this.resolveSeedRtt(seed);
1434
+ if (rtt === null) return;
1435
+ this.addSample({
1436
+ ts: this.now(),
1437
+ rtt,
1438
+ source: "connection",
1439
+ reason: "ok"
1440
+ });
1441
+ }
1442
+ notifyMqttConnect(rttMs) {
1443
+ const prevMqttConnected = this.mqttConnected;
1444
+ this.mqttConnected = true;
1445
+ this.mqttDisconnectedAt = null;
1446
+ this.addSample(
1447
+ {
1448
+ ts: this.now(),
1449
+ rtt: rttMs,
1450
+ source: "mqtt",
1451
+ reason: "ok"
1452
+ },
1453
+ prevMqttConnected
1454
+ );
1455
+ }
1456
+ notifyMqttDisconnect() {
1457
+ const prevMqttConnected = this.mqttConnected;
1458
+ this.mqttConnected = false;
1459
+ this.mqttDisconnectedAt = this.now();
1460
+ this.recompute(prevMqttConnected);
1461
+ }
1462
+ notifyMqttPing(rttMs) {
1463
+ this.addSample({
1464
+ ts: this.now(),
1465
+ rtt: rttMs,
1466
+ source: "mqtt",
1467
+ reason: "ok"
1468
+ });
1469
+ }
1470
+ subscribe(listener) {
1471
+ const wrapped = ((evt) => {
1472
+ const detail = evt.detail;
1473
+ if (detail) listener(detail);
1474
+ });
1475
+ this.bus.on("network:change", wrapped);
1476
+ return () => this.bus.off("network:change", wrapped);
1477
+ }
1478
+ getCurrent() {
1479
+ this.pruneWindow();
1480
+ return this.snapshot();
1481
+ }
1482
+ reset() {
1483
+ const prev = this.snapshot();
1484
+ for (const key of Object.keys(this.buckets)) {
1485
+ this.buckets[key] = [];
1486
+ }
1487
+ this.smoother.reset();
1488
+ this.mqttConnected = false;
1489
+ this.mqttDisconnectedAt = null;
1490
+ this.lastSuccessfulHttpAt = null;
1491
+ this.committedNetwork = "good";
1492
+ this.committedBackend = "ok";
1493
+ const current = this.snapshot();
1494
+ if (prev.network !== current.network || prev.backend !== current.backend) {
1495
+ this.bus.emit("network:change", { previous: prev, current });
1496
+ }
1497
+ }
1498
+ tick() {
1499
+ this.pruneWindow();
1500
+ this.recompute();
1501
+ }
1502
+ dispose() {
1503
+ if (this.disposed) return;
1504
+ this.disposed = true;
1505
+ if (this.tickHandle !== null) {
1506
+ clearInterval(this.tickHandle);
1507
+ this.tickHandle = null;
1508
+ }
1509
+ this.detachBrowserListeners();
1510
+ }
1511
+ resolveSeedRtt(seed) {
1512
+ if (typeof seed.rtt === "number" && seed.rtt > 0) return seed.rtt;
1513
+ if (seed.effectiveType && seed.effectiveType in EFFECTIVE_TYPE_TO_RTT) {
1514
+ return EFFECTIVE_TYPE_TO_RTT[seed.effectiveType] ?? null;
1515
+ }
1516
+ return null;
1517
+ }
1518
+ startTick() {
1519
+ if (typeof setInterval === "undefined") return;
1520
+ this.tickHandle = setInterval(() => this.tick(), TICK_INTERVAL_MS);
1521
+ }
1522
+ attachBrowserListeners() {
1523
+ if (typeof globalThis === "undefined") return;
1524
+ const w = globalThis;
1525
+ if (typeof w.addEventListener !== "function") return;
1526
+ this.onlineListener = () => this.tick();
1527
+ this.offlineListener = () => this.tick();
1528
+ try {
1529
+ w.addEventListener("online", this.onlineListener);
1530
+ w.addEventListener("offline", this.offlineListener);
1531
+ } catch {
1532
+ this.onlineListener = null;
1533
+ this.offlineListener = null;
1534
+ }
1535
+ }
1536
+ detachBrowserListeners() {
1537
+ const w = globalThis;
1538
+ if (typeof w.removeEventListener !== "function") return;
1539
+ try {
1540
+ if (this.onlineListener) w.removeEventListener("online", this.onlineListener);
1541
+ if (this.offlineListener) w.removeEventListener("offline", this.offlineListener);
1542
+ } catch {
1543
+ }
1544
+ }
1545
+ pruneWindow() {
1546
+ const cutoff = this.now() - WINDOW_MS;
1547
+ for (const key of Object.keys(this.buckets)) {
1548
+ this.buckets[key] = this.buckets[key].filter((s) => s.ts >= cutoff);
1549
+ }
1550
+ }
1551
+ recompute(prevMqttConnectedOverride) {
1552
+ this.pruneWindow();
1553
+ const prev = this.snapshot();
1554
+ const allSamples = this.allHttpAndMqttSamples();
1555
+ const realSamples = allSamples.filter((s) => s.source !== "connection");
1556
+ const useConnection = realSamples.length < COLD_START_REAL_SAMPLE_THRESHOLD;
1557
+ const effectiveSamples = useConnection ? allSamples : realSamples;
1558
+ const httpSamples = effectiveSamples.filter((s) => isHttpSource(s.source));
1559
+ const backend = classifyBackendHealth(httpSamples);
1560
+ const metrics = this.deriveMetricsFrom(effectiveSamples);
1561
+ const candidate = classifyNetworkLevel(
1562
+ { rtt: metrics.emaForLevel, jitter: metrics.jitter, lossRate: metrics.lossRate },
1563
+ this.committedNetwork
1564
+ );
1565
+ let nextNetwork = this.smoother.level;
1566
+ if (effectiveSamples.length === 0) {
1567
+ nextNetwork = this.committedNetwork;
1568
+ } else if (this.smoother.ema > 0) {
1569
+ nextNetwork = this.smoother.level;
1570
+ } else {
1571
+ nextNetwork = candidate;
1572
+ }
1573
+ if (this.isHardOffline()) {
1574
+ this.smoother.forceOffline();
1575
+ nextNetwork = "offline";
1576
+ } else if (this.smoother.level === "offline") {
1577
+ this.smoother.releaseOffline();
1578
+ nextNetwork = this.smoother.level;
1579
+ }
1580
+ const prevMqttConnected = prevMqttConnectedOverride ?? prev.mqttConnected;
1581
+ const changed = nextNetwork !== this.committedNetwork || backend !== this.committedBackend || this.mqttConnected !== prevMqttConnected;
1582
+ this.committedNetwork = nextNetwork;
1583
+ this.committedBackend = backend;
1584
+ if (changed) {
1585
+ const current = this.snapshot();
1586
+ this.bus.emit("network:change", { previous: prev, current });
1587
+ }
1588
+ }
1589
+ deriveMetrics() {
1590
+ return this.deriveMetricsFrom(this.allHttpAndMqttSamples());
1591
+ }
1592
+ deriveMetricsFrom(samples) {
1593
+ const httpSamples = samples.filter((s) => isHttpSource(s.source));
1594
+ const total = httpSamples.length;
1595
+ const lossCount = httpSamples.filter(
1596
+ (s) => s.reason === "network" || s.reason === "timeout" || s.reason === "offline"
1597
+ ).length;
1598
+ const lossRate = total > 0 ? lossCount / total : 0;
1599
+ const okRtts = httpSamples.filter(
1600
+ (s) => s.reason === "ok" || s.reason === "backend5xx" || s.reason === "backend4xx" || s.reason === "gqlError"
1601
+ ).map((s) => s.rtt).filter((r) => Number.isFinite(r));
1602
+ let jitter = 0;
1603
+ if (okRtts.length > 1) {
1604
+ const mean = okRtts.reduce((a, b) => a + b, 0) / okRtts.length;
1605
+ const variance = okRtts.reduce((a, b) => a + (b - mean) ** 2, 0) / okRtts.length;
1606
+ jitter = Math.sqrt(variance);
1607
+ }
1608
+ const connectionFallback = samples.filter((s) => s.source === "connection" && Number.isFinite(s.rtt)).slice(-1)[0]?.rtt;
1609
+ const emaForLevel = this.smoother.ema > 0 ? this.smoother.ema : okRtts.length > 0 ? okRtts[okRtts.length - 1] ?? 0 : connectionFallback ?? 0;
1610
+ return { jitter, lossRate, emaForLevel };
1611
+ }
1612
+ isHardOffline() {
1613
+ if (typeof navigator !== "undefined" && navigator && navigator.onLine === false) {
1614
+ return true;
1615
+ }
1616
+ const now = this.now();
1617
+ const recent = this.allHttpAndMqttSamples().filter(
1618
+ (s) => s.ts >= now - OFFLINE_HOLD_MS && (s.source === "graphql" || s.source === "rest")
1619
+ );
1620
+ if (recent.length >= 3 && recent.every(
1621
+ (s) => s.reason === "network" || s.reason === "timeout" || s.reason === "offline"
1622
+ )) {
1623
+ return true;
1624
+ }
1625
+ if (this.mqttDisconnectedAt !== null && now - this.mqttDisconnectedAt > OFFLINE_HOLD_MS && (this.lastSuccessfulHttpAt === null || now - this.lastSuccessfulHttpAt > OFFLINE_HOLD_MS)) {
1626
+ return true;
1627
+ }
1628
+ return false;
1629
+ }
1630
+ allHttpAndMqttSamples() {
1631
+ return [
1632
+ ...this.buckets.graphql,
1633
+ ...this.buckets.rest,
1634
+ ...this.buckets.mqtt,
1635
+ ...this.buckets.connection
1636
+ ];
1637
+ }
1638
+ snapshot() {
1639
+ const samples = this.allHttpAndMqttSamples();
1640
+ const metrics = this.deriveMetricsFrom(samples);
1641
+ return {
1642
+ network: this.committedNetwork,
1643
+ backend: this.committedBackend,
1644
+ rtt: Math.round(metrics.emaForLevel),
1645
+ jitter: Math.round(metrics.jitter),
1646
+ lossRate: metrics.lossRate,
1647
+ mqttConnected: this.mqttConnected,
1648
+ samplesInWindow: samples.filter((s) => s.source !== "connection").length,
1649
+ lastUpdated: this.now()
1650
+ };
1651
+ }
1652
+ now() {
1653
+ return Date.now();
1654
+ }
1655
+ };
1656
+
1657
+ // src/network/probes/connectionProbe.ts
1658
+ function attachConnectionProbe(monitor) {
1659
+ const nav = typeof navigator !== "undefined" ? navigator : void 0;
1660
+ const connection = nav?.connection;
1661
+ if (!connection) {
1662
+ return { dispose: () => void 0 };
1663
+ }
1664
+ const emit = () => {
1665
+ const seed = {
1666
+ rtt: connection.rtt,
1667
+ effectiveType: connection.effectiveType
1668
+ };
1669
+ monitor.seedFromConnection(seed);
1670
+ };
1671
+ emit();
1672
+ const listener = () => emit();
1673
+ connection.addEventListener?.("change", listener);
1674
+ return {
1675
+ dispose() {
1676
+ connection.removeEventListener?.("change", listener);
1677
+ }
1678
+ };
1679
+ }
1680
+
1681
+ // src/network/probes/graphqlProbe.ts
1682
+ function createGraphqlProbe(monitor) {
1683
+ return (event) => {
1684
+ if (isAbortError(event.threw)) return;
1685
+ const sample = classifyHttpOutcome(
1686
+ {
1687
+ rtt: event.endedAt - event.startedAt,
1688
+ httpStatus: event.httpStatus,
1689
+ gqlErrors: event.gqlErrors ?? null,
1690
+ threw: event.threw
1691
+ },
1692
+ { now: event.endedAt, source: "graphql" }
1693
+ );
1694
+ monitor.addSample(sample);
1695
+ };
1696
+ }
1697
+
1698
+ // src/network/probes/mqttProbe.ts
1699
+ function createMqttProbe(monitor) {
1700
+ return (event) => {
1701
+ switch (event.kind) {
1702
+ case "connect":
1703
+ case "reconnect":
1704
+ monitor.notifyMqttConnect(event.rttMs);
1705
+ return;
1706
+ case "disconnect":
1707
+ monitor.notifyMqttDisconnect();
1708
+ return;
1709
+ case "ping":
1710
+ monitor.notifyMqttPing(event.rttMs);
1711
+ return;
1712
+ case "error":
1713
+ return;
1714
+ }
1715
+ };
1716
+ }
1717
+
1718
+ // src/network/probes/restProbe.ts
1719
+ function createRestProbe(monitor) {
1720
+ return (event) => {
1721
+ if (isAbortError(event.threw)) return;
1722
+ const sample = classifyHttpOutcome(
1723
+ {
1724
+ rtt: event.endedAt - event.startedAt,
1725
+ httpStatus: event.httpStatus,
1726
+ threw: event.threw
1727
+ },
1728
+ { now: event.endedAt, source: "rest" }
1729
+ );
1730
+ monitor.addSample(sample);
1731
+ };
1732
+ }
1733
+
1734
+ // src/storage/index.ts
1735
+ var MemoryAdapter = class {
1736
+ store = /* @__PURE__ */ new Map();
1737
+ get(key) {
1738
+ return this.store.get(key) ?? null;
1739
+ }
1740
+ set(key, value) {
1741
+ this.store.set(key, value);
1742
+ }
1743
+ remove(key) {
1744
+ this.store.delete(key);
1745
+ }
1746
+ };
1747
+ var BrowserStorageAdapter = class {
1748
+ storage;
1749
+ constructor(storage) {
1750
+ this.storage = storage;
1751
+ }
1752
+ get(key) {
1753
+ return this.storage.getItem(key);
1754
+ }
1755
+ set(key, value) {
1756
+ this.storage.setItem(key, value);
1757
+ }
1758
+ remove(key) {
1759
+ this.storage.removeItem(key);
1760
+ }
1761
+ };
1762
+ function autoDetectStorage() {
1763
+ if (typeof localStorage !== "undefined") {
1764
+ return TaphubStorage.localStorage();
1765
+ }
1766
+ return TaphubStorage.memory();
1767
+ }
1768
+ var TaphubStorage = {
1769
+ memory() {
1770
+ return new MemoryAdapter();
1771
+ },
1772
+ localStorage() {
1773
+ if (typeof localStorage === "undefined") {
1774
+ throw new TaphubError("localStorage is not available in this environment", {
1775
+ code: "StorageUnavailable"
1776
+ });
1777
+ }
1778
+ return new BrowserStorageAdapter(localStorage);
1779
+ },
1780
+ sessionStorage() {
1781
+ if (typeof sessionStorage === "undefined") {
1782
+ throw new TaphubError("sessionStorage is not available in this environment", {
1783
+ code: "StorageUnavailable"
1784
+ });
1785
+ }
1786
+ return new BrowserStorageAdapter(sessionStorage);
1787
+ }
1788
+ };
1789
+
1790
+ // src/transport/shared/errors.ts
1791
+ function mapNetworkErrorToTaphubError(err) {
1792
+ if (err instanceof DOMException && err.name === "AbortError") {
1793
+ throw new TaphubNetworkError("Request aborted", { code: "Aborted" });
1794
+ }
1795
+ if (err instanceof Error && (err.name === "AbortError" || err.message.includes("aborted"))) {
1796
+ throw new TaphubNetworkError("Request aborted", { code: "Aborted" });
1797
+ }
1798
+ throw new TaphubNetworkError(err instanceof Error ? err.message : "Network error", {
1799
+ code: "NetworkUnreachable"
1800
+ });
1801
+ }
1802
+
1803
+ // src/transport/shared/headers.ts
1804
+ function buildHeaders(opts) {
1805
+ const headers = new Headers();
1806
+ if (opts.token !== null) {
1807
+ headers.set("Authorization", `Bearer ${opts.token}`);
1808
+ }
1809
+ if (opts.hasBody) {
1810
+ headers.set("Content-Type", "application/json");
1811
+ }
1812
+ if (opts.agencyId) {
1813
+ headers.set("x-builder-code", opts.agencyId);
1814
+ }
1815
+ return headers;
1816
+ }
1817
+
1818
+ // src/transport/graphql/errors.ts
1819
+ var AUTH_ERROR_CODES = [
1820
+ "Unauthorized",
1821
+ "TokenExpired",
1822
+ "NotAuthenticated",
1823
+ "ErrAccessTokenInvalid",
1824
+ "ErrSessionInvalid"
1825
+ ];
1826
+ var SLIPPAGE_CODE = "Bid_CoefficientMismatch";
1827
+ function extractSlippageMeta(entry) {
1828
+ const meta = entry?.extensions?.meta;
1829
+ if (!meta) return null;
1830
+ const clientCoef = meta.clientCoef;
1831
+ const serverCoef = meta.serverCoef;
1832
+ const slippage = meta.slippage;
1833
+ if (typeof clientCoef !== "number" || typeof serverCoef !== "number" || typeof slippage !== "number" || !Number.isFinite(clientCoef) || !Number.isFinite(serverCoef) || !Number.isFinite(slippage)) {
1834
+ return null;
1835
+ }
1836
+ return { clientCoef, serverCoef, slippage };
1837
+ }
1838
+ function isAuthCode(code) {
1839
+ return AUTH_ERROR_CODES.includes(code);
1840
+ }
1841
+ function firstErrorCode(body) {
1842
+ const code = body.errors?.[0]?.extensions?.code;
1843
+ if (typeof code === "string") return code;
1844
+ return "Unknown";
1845
+ }
1846
+ function firstErrorMessage(body) {
1847
+ return body.errors?.[0]?.message ?? "GraphQL error";
1848
+ }
1849
+ function buildDetails(body) {
1850
+ const details = {};
1851
+ if (body.errors && body.errors.length > 0) {
1852
+ details.errors = body.errors;
1853
+ }
1854
+ if (body.data !== void 0 && body.data !== null) {
1855
+ details.partialData = body.data;
1856
+ }
1857
+ return details;
1858
+ }
1859
+ function mapGraphQLHttpError(status, body) {
1860
+ const parsed = typeof body === "object" && body !== null ? body : null;
1861
+ if (status === 401) {
1862
+ const code = parsed?.errors?.[0]?.extensions?.code ?? "Unauthorized";
1863
+ const message = parsed?.errors?.[0]?.message ?? "Unauthorized";
1864
+ throw new TaphubAuthError(message, {
1865
+ code,
1866
+ details: parsed ? buildDetails(parsed) : body
1867
+ });
1868
+ }
1869
+ if (status >= 400 && status < 500) {
1870
+ const code = parsed?.errors?.[0]?.extensions?.code ?? "BadRequest";
1871
+ const message = parsed?.errors?.[0]?.message ?? "Bad Request";
1872
+ throw new TaphubValidationError(message, {
1873
+ code,
1874
+ details: parsed ? buildDetails(parsed) : body
1875
+ });
1876
+ }
1877
+ if (status >= 500) {
1878
+ if (!parsed) {
1879
+ throw new TaphubServerError("Invalid response from server", {
1880
+ code: "InvalidResponse",
1881
+ details: body ?? void 0
1882
+ });
1883
+ }
1884
+ const code = parsed.errors?.[0]?.extensions?.code ?? "ServerError";
1885
+ const message = parsed.errors?.[0]?.message ?? "Internal Server Error";
1886
+ throw new TaphubServerError(message, {
1887
+ code,
1888
+ details: buildDetails(parsed)
1889
+ });
1890
+ }
1891
+ throw new TaphubServerError("Unexpected status", {
1892
+ code: "ServerError",
1893
+ details: body ?? void 0
1894
+ });
1895
+ }
1896
+ function mapGraphQLLevelError(body) {
1897
+ const code = firstErrorCode(body);
1898
+ const message = firstErrorMessage(body);
1899
+ if (isAuthCode(code)) {
1900
+ throw new TaphubAuthError(message, {
1901
+ code,
1902
+ details: buildDetails(body)
1903
+ });
1904
+ }
1905
+ if (code === SLIPPAGE_CODE) {
1906
+ const meta = extractSlippageMeta(body.errors?.[0]);
1907
+ if (meta) {
1908
+ throw new TaphubSlippageError(message, {
1909
+ code,
1910
+ clientCoef: meta.clientCoef,
1911
+ serverCoef: meta.serverCoef,
1912
+ slippage: meta.slippage,
1913
+ details: buildDetails(body)
1914
+ });
1915
+ }
1916
+ }
1917
+ throw new TaphubValidationError(message, {
1918
+ code,
1919
+ details: buildDetails(body)
1920
+ });
1921
+ }
1922
+
1923
+ // src/transport/graphql/index.ts
1924
+ function extractOp(query) {
1925
+ const named = query.match(/(?:query|mutation|subscription)\s+([A-Za-z]\w*)/);
1926
+ if (named) return named[1];
1927
+ return query.match(/{\s*([A-Za-z]\w*)/)?.[1] ?? "unknown";
1928
+ }
1929
+ function createGraphQLTransport(deps) {
1930
+ const { baseUrl, getToken, agencyId, onRequest } = deps;
1931
+ function resolveFetch() {
1932
+ const fetchImpl = deps.fetch ?? globalThis.fetch;
1933
+ if (!fetchImpl) {
1934
+ throw new TaphubError(
1935
+ "fetch is not available. Provide a custom fetch or ensure globalThis.fetch exists.",
1936
+ {
1937
+ code: "FetchUnavailable"
1938
+ }
1939
+ );
1940
+ }
1941
+ return fetchImpl;
1942
+ }
1943
+ return {
1944
+ async request(query, variables, opts) {
1945
+ const fetchImpl = resolveFetch();
1946
+ const base = baseUrl.replace(/\/+$/, "");
1947
+ const op = extractOp(query);
1948
+ const url = `${base}?${op}`;
1949
+ const token = getToken();
1950
+ const headers = buildHeaders({ token, hasBody: true, agencyId });
1951
+ const body = { query };
1952
+ if (variables !== void 0) {
1953
+ body.variables = variables;
1954
+ }
1955
+ const startedAt = Date.now();
1956
+ let httpStatus = null;
1957
+ let gqlErrors = null;
1958
+ let threw = null;
1959
+ const fireProbe = () => {
1960
+ if (!onRequest) return;
1961
+ try {
1962
+ onRequest({
1963
+ startedAt,
1964
+ endedAt: Date.now(),
1965
+ httpStatus,
1966
+ gqlErrors,
1967
+ threw
1968
+ });
1969
+ } catch {
1970
+ }
1971
+ };
1972
+ try {
1973
+ let response;
1974
+ try {
1975
+ response = await fetchImpl.call(void 0, url, {
1976
+ method: "POST",
1977
+ headers,
1978
+ body: JSON.stringify(body),
1979
+ signal: opts?.signal
1980
+ });
1981
+ } catch (err) {
1982
+ threw = err;
1983
+ mapNetworkErrorToTaphubError(err);
1984
+ }
1985
+ httpStatus = response.status;
1986
+ let responseBody;
1987
+ try {
1988
+ responseBody = await response.json();
1989
+ } catch {
1990
+ throw new TaphubServerError("Invalid response from server", {
1991
+ code: "InvalidResponse"
1992
+ });
1993
+ }
1994
+ if (!response.ok) {
1995
+ mapGraphQLHttpError(response.status, responseBody);
1996
+ }
1997
+ const graphqlBody = responseBody;
1998
+ if (graphqlBody.errors && graphqlBody.errors.length > 0) {
1999
+ gqlErrors = graphqlBody.errors;
2000
+ mapGraphQLLevelError(graphqlBody);
2001
+ }
2002
+ if (graphqlBody.data === void 0 || graphqlBody.data === null) {
2003
+ throw new TaphubServerError("Invalid response from server", {
2004
+ code: "InvalidResponse"
2005
+ });
2006
+ }
2007
+ return graphqlBody.data;
2008
+ } catch (err) {
2009
+ if (threw === null) threw = err;
2010
+ throw err;
2011
+ } finally {
2012
+ fireProbe();
2013
+ }
2014
+ }
2015
+ };
2016
+ }
2017
+
2018
+ // src/transport/rest/errors.ts
2019
+ function tryParseBody(body) {
2020
+ if (typeof body === "object" && body !== null && "code" in body) {
2021
+ return body;
2022
+ }
2023
+ return null;
2024
+ }
2025
+ function mapHttpToTaphubError(status, body) {
2026
+ const parsed = tryParseBody(body);
2027
+ const code = parsed?.code ?? "";
2028
+ const details = body ?? void 0;
2029
+ if (status === 401) {
2030
+ throw new TaphubAuthError(parsed?.message ?? "Unauthorized", {
2031
+ code: code || "Unauthorized",
2032
+ details
2033
+ });
2034
+ }
2035
+ if (status >= 400 && status < 500) {
2036
+ throw new TaphubValidationError(parsed?.message ?? "Bad Request", {
2037
+ code: code || "BadRequest",
2038
+ details
2039
+ });
2040
+ }
2041
+ if (status >= 500) {
2042
+ if (!parsed) {
2043
+ throw new TaphubServerError("Invalid response from server", {
2044
+ code: "InvalidResponse",
2045
+ details
2046
+ });
2047
+ }
2048
+ throw new TaphubServerError(parsed.message ?? "Internal Server Error", {
2049
+ code: code || "ServerError",
2050
+ details
2051
+ });
2052
+ }
2053
+ throw new TaphubServerError("Unexpected status", {
2054
+ code: "ServerError",
2055
+ details
2056
+ });
2057
+ }
2058
+
2059
+ // src/transport/rest/url.ts
2060
+ var API_PREFIX = "/api/user/v1";
2061
+ function buildUrl(baseUrl, path) {
2062
+ const base = baseUrl.replace(/\/+$/, "");
2063
+ const cleanPath = path.replace(/^\/+/, "");
2064
+ return `${base}${API_PREFIX}/${cleanPath}`;
2065
+ }
2066
+
2067
+ // src/transport/rest/index.ts
2068
+ function createRestTransport(deps) {
2069
+ const { baseUrl, getToken, onRequest } = deps;
2070
+ function resolveFetch() {
2071
+ const fetchImpl = deps.fetch ?? globalThis.fetch;
2072
+ if (!fetchImpl) {
2073
+ throw new TaphubError(
2074
+ "fetch is not available. Provide a custom fetch or ensure globalThis.fetch exists.",
2075
+ {
2076
+ code: "FetchUnavailable"
2077
+ }
2078
+ );
2079
+ }
2080
+ return fetchImpl;
2081
+ }
2082
+ async function request(method, path, body, opts) {
2083
+ const fetchImpl = resolveFetch();
2084
+ const url = buildUrl(baseUrl, path);
2085
+ const token = getToken();
2086
+ const hasBody = body !== void 0;
2087
+ const headers = buildHeaders({ token, hasBody });
2088
+ const startedAt = Date.now();
2089
+ let httpStatus = null;
2090
+ let threw = null;
2091
+ const fireProbe = () => {
2092
+ if (!onRequest) return;
2093
+ try {
2094
+ onRequest({ startedAt, endedAt: Date.now(), httpStatus, threw });
2095
+ } catch {
2096
+ }
2097
+ };
2098
+ try {
2099
+ let response;
2100
+ try {
2101
+ response = await fetchImpl.call(void 0, url, {
2102
+ method,
2103
+ headers,
2104
+ body: hasBody ? JSON.stringify(body) : void 0,
2105
+ signal: opts?.signal
2106
+ });
2107
+ } catch (err) {
2108
+ threw = err;
2109
+ mapNetworkErrorToTaphubError(err);
2110
+ }
2111
+ httpStatus = response.status;
2112
+ let responseBody;
2113
+ try {
2114
+ responseBody = await response.json();
2115
+ } catch {
2116
+ throw new TaphubServerError("Invalid response from server", {
2117
+ code: "InvalidResponse"
2118
+ });
2119
+ }
2120
+ if (!response.ok) {
2121
+ mapHttpToTaphubError(response.status, responseBody);
2122
+ }
2123
+ const envelope = responseBody;
2124
+ if (envelope.code !== "Success" || !("data" in envelope)) {
2125
+ throw new TaphubServerError("Invalid response from server", {
2126
+ code: "InvalidResponse"
2127
+ });
2128
+ }
2129
+ return envelope.data;
2130
+ } catch (err) {
2131
+ if (threw === null) threw = err;
2132
+ throw err;
2133
+ } finally {
2134
+ fireProbe();
2135
+ }
2136
+ }
2137
+ return {
2138
+ get(path, opts) {
2139
+ return request("GET", path, void 0, opts);
2140
+ },
2141
+ post(path, body, opts) {
2142
+ return request("POST", path, body, opts);
2143
+ }
2144
+ };
2145
+ }
2146
+
2147
+ // src/client.ts
2148
+ function tokenStorageKey(agencyId) {
2149
+ return `taphub:${agencyId}:token`;
2150
+ }
2151
+ function isDemoStorageKey(agencyId) {
2152
+ return `taphub:${agencyId}:isDemo`;
2153
+ }
2154
+ var TaphubClient = class {
2155
+ agencyId;
2156
+ endpoint;
2157
+ storage;
2158
+ /** @readonly User module — reassignment has no effect at runtime. */
2159
+ user;
2160
+ /** @readonly Auth module — reassignment has no effect at runtime. */
2161
+ auth;
2162
+ /** @readonly Game module — reassignment has no effect at runtime. */
2163
+ game;
2164
+ /** @readonly Bid module — reassignment has no effect at runtime. */
2165
+ bid;
2166
+ /** @readonly Leaderboard module — reassignment has no effect at runtime. */
2167
+ leaderboard;
2168
+ /** @readonly Locale module — reassignment has no effect at runtime. */
2169
+ locale;
2170
+ /** @readonly Realtime module — undefined when mqttEndpoint not configured. */
2171
+ realtime;
2172
+ /** @readonly Network quality monitor — always present (operates passively). */
2173
+ network;
2174
+ bus;
2175
+ #token;
2176
+ #isDemo;
2177
+ #tokenKey;
2178
+ #isDemoKey;
2179
+ #rest;
2180
+ #graphql;
2181
+ #graphqlUser;
2182
+ constructor(config) {
2183
+ if (!config.agencyId) {
2184
+ throw new TaphubValidationError("agencyId is required", {
2185
+ code: "InvalidConfig",
2186
+ details: { field: "agencyId" }
2187
+ });
2188
+ }
2189
+ try {
2190
+ new URL(config.endpoint);
2191
+ } catch {
2192
+ throw new TaphubValidationError("endpoint must be a valid absolute URL", {
2193
+ code: "InvalidConfig",
2194
+ details: { field: "endpoint" }
2195
+ });
2196
+ }
2197
+ this.agencyId = config.agencyId;
2198
+ this.endpoint = config.endpoint;
2199
+ this.storage = config.storage ?? autoDetectStorage();
2200
+ this.bus = new TaphubEventBus();
2201
+ this.network = new NetworkQualityMonitor({ bus: this.bus });
2202
+ const graphqlProbe = createGraphqlProbe(this.network);
2203
+ const restProbe = createRestProbe(this.network);
2204
+ this.#tokenKey = tokenStorageKey(this.agencyId);
2205
+ this.#isDemoKey = isDemoStorageKey(this.agencyId);
2206
+ this.#token = this.storage.get(this.#tokenKey);
2207
+ this.#isDemo = this.storage.get(this.#isDemoKey) === "1";
2208
+ this.#rest = createRestTransport({
2209
+ baseUrl: this.endpoint,
2210
+ getToken: () => this.getToken(),
2211
+ fetch: config.fetch,
2212
+ onRequest: restProbe
2213
+ });
2214
+ const baseApi = this.endpoint.replace(/\/+$/, "");
2215
+ this.#graphql = createGraphQLTransport({
2216
+ baseUrl: `${baseApi}/grid-api/grid-gql`,
2217
+ getToken: () => this.getToken(),
2218
+ agencyId: this.agencyId,
2219
+ fetch: config.fetch,
2220
+ onRequest: graphqlProbe
2221
+ });
2222
+ this.#graphqlUser = createGraphQLTransport({
2223
+ baseUrl: `${baseApi}/taphub-user-service/th-user-gql`,
2224
+ getToken: () => this.getToken(),
2225
+ agencyId: this.agencyId,
2226
+ fetch: config.fetch,
2227
+ onRequest: graphqlProbe
2228
+ });
2229
+ this.user = new UserModule({
2230
+ rest: this.#rest,
2231
+ graphql: this.#graphql,
2232
+ graphqlUser: this.#graphqlUser
2233
+ });
2234
+ this.auth = new AuthModule({
2235
+ rest: this.#rest,
2236
+ graphql: this.#graphql,
2237
+ graphqlUser: this.#graphqlUser,
2238
+ setToken: (t, opts) => this.setToken(t, opts),
2239
+ agencyId: this.agencyId,
2240
+ onLoginSuccess: () => this.user.refreshCurrencies().then(() => void 0).catch(() => void 0),
2241
+ onLogout: () => {
2242
+ this.user.clearCurrencies();
2243
+ }
2244
+ });
2245
+ this.game = new GameModule({ graphql: this.#graphql });
2246
+ this.bid = new BidModule({ graphql: this.#graphql });
2247
+ this.leaderboard = new LeaderboardModule({ graphql: this.#graphql });
2248
+ this.locale = new LocaleModule({ graphql: this.#graphql });
2249
+ this.realtime = config.mqttEndpoint ? new RealtimeModule({
2250
+ mqttEndpoint: config.mqttEndpoint,
2251
+ onMqttLifecycle: createMqttProbe(this.network)
2252
+ }) : void 0;
2253
+ attachConnectionProbe(this.network);
2254
+ Object.defineProperty(this, "user", {
2255
+ value: this.user,
2256
+ writable: false,
2257
+ enumerable: true,
2258
+ configurable: false
2259
+ });
2260
+ Object.defineProperty(this, "auth", {
2261
+ value: this.auth,
2262
+ writable: false,
2263
+ enumerable: true,
2264
+ configurable: false
2265
+ });
2266
+ Object.defineProperty(this, "game", {
2267
+ value: this.game,
2268
+ writable: false,
2269
+ enumerable: true,
2270
+ configurable: false
2271
+ });
2272
+ Object.defineProperty(this, "bid", {
2273
+ value: this.bid,
2274
+ writable: false,
2275
+ enumerable: true,
2276
+ configurable: false
2277
+ });
2278
+ Object.defineProperty(this, "leaderboard", {
2279
+ value: this.leaderboard,
2280
+ writable: false,
2281
+ enumerable: true,
2282
+ configurable: false
2283
+ });
2284
+ if (this.realtime) {
2285
+ Object.defineProperty(this, "realtime", {
2286
+ value: this.realtime,
2287
+ writable: false,
2288
+ enumerable: true,
2289
+ configurable: false
2290
+ });
2291
+ }
2292
+ Object.defineProperty(this, "network", {
2293
+ value: this.network,
2294
+ writable: false,
2295
+ enumerable: true,
2296
+ configurable: false
2297
+ });
2298
+ }
2299
+ getToken() {
2300
+ return this.#token;
2301
+ }
2302
+ setToken(token, opts) {
2303
+ this.#token = token;
2304
+ if (token === null) {
2305
+ this.#isDemo = false;
2306
+ this.storage.remove(this.#tokenKey);
2307
+ this.storage.remove(this.#isDemoKey);
2308
+ return;
2309
+ }
2310
+ const isDemo = opts?.isDemo ?? false;
2311
+ this.#isDemo = isDemo;
2312
+ this.storage.set(this.#tokenKey, token);
2313
+ this.storage.set(this.#isDemoKey, isDemo ? "1" : "0");
2314
+ }
2315
+ isDemo() {
2316
+ return this.#isDemo;
2317
+ }
2318
+ /** @internal Used by module integrations to access the REST transport. Not part of the public API. */
2319
+ get _rest() {
2320
+ return this.#rest;
2321
+ }
2322
+ /** @internal Used by module integrations to access the GraphQL transport. Not part of the public API. */
2323
+ get _graphql() {
2324
+ return this.#graphql;
2325
+ }
2326
+ on(event, handler) {
2327
+ this.bus.on(event, handler);
2328
+ }
2329
+ off(event, handler) {
2330
+ this.bus.off(event, handler);
2331
+ }
2332
+ };
2333
+
2334
+ // src/utils/coefficient.ts
2335
+ function errorFunction(x) {
2336
+ const a1 = 0.254829592;
2337
+ const a2 = -0.284496736;
2338
+ const a3 = 1.421413741;
2339
+ const a4 = -1.453152027;
2340
+ const a5 = 1.061405429;
2341
+ const p = 0.3275911;
2342
+ const sign = x >= 0 ? 1 : -1;
2343
+ const absX = Math.abs(x);
2344
+ const t = 1 / (1 + p * absX);
2345
+ const y = 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-absX * absX);
2346
+ return sign * y;
2347
+ }
2348
+ function normalCDF(x) {
2349
+ return 0.5 * (1 + errorFunction(x / Math.SQRT2));
2350
+ }
2351
+ function normalPDF(x) {
2352
+ return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
2353
+ }
2354
+ function adaptiveSimpson(f, a, b, tolerance = 1e-6, maxDepth = 50) {
2355
+ function simpson(a2, b2, fa2, fm2, fb2) {
2356
+ return (b2 - a2) / 6 * (fa2 + 4 * fm2 + fb2);
2357
+ }
2358
+ function recurse(a2, b2, fa2, fm2, fb2, whole2, depth) {
2359
+ const m = (a2 + b2) / 2;
2360
+ const m1 = (a2 + m) / 2;
2361
+ const m2 = (m + b2) / 2;
2362
+ const fm1 = f(m1);
2363
+ const fm22 = f(m2);
2364
+ const left = simpson(a2, m, fa2, fm1, fm2);
2365
+ const right = simpson(m, b2, fm2, fm22, fb2);
2366
+ const combined = left + right;
2367
+ if (depth >= maxDepth || Math.abs(combined - whole2) <= 15 * tolerance) {
2368
+ return combined + (combined - whole2) / 15;
2369
+ }
2370
+ return recurse(a2, m, fa2, fm1, fm2, left, depth + 1) + recurse(m, b2, fm2, fm22, fb2, right, depth + 1);
2371
+ }
2372
+ const fa = f(a);
2373
+ const fm = f((a + b) / 2);
2374
+ const fb = f(b);
2375
+ const whole = simpson(a, b, fa, fm, fb);
2376
+ return recurse(a, b, fa, fm, fb, whole, 0);
2377
+ }
2378
+ function calculateProbWin(time1, time2, _price1, price2, creationTime, currentPrice, volatility) {
2379
+ if (volatility <= 0 || time2 <= time1) {
2380
+ return 0;
2381
+ }
2382
+ const _elapsed = time1 - creationTime;
2383
+ const remaining = time2 - time1;
2384
+ const totalDuration = time2 - creationTime;
2385
+ if (totalDuration <= 0) {
2386
+ return 0;
2387
+ }
2388
+ const targetMove = price2 - currentPrice;
2389
+ const timeFraction = remaining / totalDuration;
2390
+ const scaledVolatility = volatility * Math.sqrt(timeFraction);
2391
+ if (scaledVolatility === 0) {
2392
+ return currentPrice >= price2 ? 1 : 0;
2393
+ }
2394
+ const z = targetMove / scaledVolatility;
2395
+ const prob = 1 - normalCDF(z);
2396
+ return Math.max(0, Math.min(1, prob));
2397
+ }
2398
+ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, currentPrice, volatility) {
2399
+ if (volatility <= 0 || time2 <= time1) {
2400
+ return 0;
2401
+ }
2402
+ const elapsed = time1 - creationTime;
2403
+ if (elapsed <= 0) {
2404
+ return 0;
2405
+ }
2406
+ const remaining = time2 - time1;
2407
+ const totalDuration = time2 - creationTime;
2408
+ const timeFraction = remaining / totalDuration;
2409
+ const scaledVolatility = volatility * Math.sqrt(timeFraction);
2410
+ if (scaledVolatility === 0) {
2411
+ return currentPrice >= price2 ? 1 : 0;
2412
+ }
2413
+ const targetMove = price2 - currentPrice;
2414
+ const z = targetMove / scaledVolatility;
2415
+ const prob = 1 - normalCDF(z);
2416
+ return Math.max(0, Math.min(1, prob));
2417
+ }
2418
+ export {
2419
+ AuthModule,
2420
+ BidModule,
2421
+ GameModule,
2422
+ LeaderboardModule,
2423
+ LocaleModule,
2424
+ NetworkQualityMonitor,
2425
+ RealtimeModule,
2426
+ TaphubAuthError,
2427
+ TaphubClient,
2428
+ TaphubError,
2429
+ TaphubEventBus,
2430
+ TaphubNetworkError,
2431
+ TaphubServerError,
2432
+ TaphubSlippageError,
2433
+ TaphubStorage,
2434
+ TaphubValidationError,
2435
+ UserModule,
2436
+ adaptiveSimpson,
2437
+ autoDetectStorage,
2438
+ calculateProbWin,
2439
+ calculateProbWin_v2,
2440
+ errorFunction,
2441
+ isCancelled,
2442
+ isLoss,
2443
+ isPending,
2444
+ isTerminal,
2445
+ isWin,
2446
+ normalCDF,
2447
+ normalPDF
2448
+ };