@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/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/index.cjs +2513 -0
- package/dist/index.d.mts +780 -0
- package/dist/index.d.ts +780 -0
- package/dist/index.js +2448 -0
- package/package.json +49 -0
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
|
+
};
|