@taphubhq/sdk-core 0.23.4 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +155 -66
- package/dist/index.d.mts +72 -2
- package/dist/index.d.ts +72 -2
- package/dist/index.js +153 -65
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -59,7 +59,8 @@ __export(index_exports, {
|
|
|
59
59
|
isTerminal: () => isTerminal,
|
|
60
60
|
isWin: () => isWin,
|
|
61
61
|
normalCDF: () => normalCDF,
|
|
62
|
-
normalPDF: () => normalPDF
|
|
62
|
+
normalPDF: () => normalPDF,
|
|
63
|
+
pairIdFromBidResultTopic: () => pairIdFromBidResultTopic
|
|
63
64
|
});
|
|
64
65
|
module.exports = __toCommonJS(index_exports);
|
|
65
66
|
|
|
@@ -1043,6 +1044,19 @@ function walletBalanceTopic(userId) {
|
|
|
1043
1044
|
function agencyPairStatsTopic(aid, pairId) {
|
|
1044
1045
|
return `public/agency/${aid}/pair/${pairId}/stats`;
|
|
1045
1046
|
}
|
|
1047
|
+
function userBidsWildcardTopic(userId) {
|
|
1048
|
+
return `${TOPIC_PREFIX}/+/user/${userId}/bid_result`;
|
|
1049
|
+
}
|
|
1050
|
+
function topicMatchesWildcard(pattern, topic) {
|
|
1051
|
+
const pp = pattern.split("/");
|
|
1052
|
+
const tp = topic.split("/");
|
|
1053
|
+
if (pp.length !== tp.length) return false;
|
|
1054
|
+
for (let i = 0; i < pp.length; i++) {
|
|
1055
|
+
if (pp[i] === "+") continue;
|
|
1056
|
+
if (pp[i] !== tp[i]) return false;
|
|
1057
|
+
}
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1046
1060
|
function topicFor(gameId, suffix, userId) {
|
|
1047
1061
|
if (USER_SCOPED_SUFFIXES.includes(suffix)) {
|
|
1048
1062
|
return `${TOPIC_PREFIX}/${gameId}/user/${userId}/${suffix}`;
|
|
@@ -1061,6 +1075,7 @@ function createMqttTransport(endpoint, opts = {}) {
|
|
|
1061
1075
|
const candleSubscriptions = /* @__PURE__ */ new Map();
|
|
1062
1076
|
const statsSubscriptions = /* @__PURE__ */ new Map();
|
|
1063
1077
|
const walletSubscriptions = /* @__PURE__ */ new Map();
|
|
1078
|
+
const userBidsSubscriptions = /* @__PURE__ */ new Map();
|
|
1064
1079
|
const { onLifecycle, auth } = opts;
|
|
1065
1080
|
let connectStartedAt = 0;
|
|
1066
1081
|
function fireLifecycle(event) {
|
|
@@ -1111,6 +1126,9 @@ function createMqttTransport(endpoint, opts = {}) {
|
|
|
1111
1126
|
for (const entry of walletSubscriptions.values()) {
|
|
1112
1127
|
c.subscribe(entry.topic, { qos: 1 });
|
|
1113
1128
|
}
|
|
1129
|
+
for (const entry of userBidsSubscriptions.values()) {
|
|
1130
|
+
c.subscribe(entry.pattern, { qos: 1 });
|
|
1131
|
+
}
|
|
1114
1132
|
fireLifecycle({ kind: "connect", rttMs: Date.now() - connectStartedAt });
|
|
1115
1133
|
});
|
|
1116
1134
|
client.on("reconnect", () => {
|
|
@@ -1159,6 +1177,19 @@ function createMqttTransport(endpoint, opts = {}) {
|
|
|
1159
1177
|
walletSub.onMessage(receivedTopic, payload2);
|
|
1160
1178
|
return;
|
|
1161
1179
|
}
|
|
1180
|
+
let userBidsPayload;
|
|
1181
|
+
for (const sub of userBidsSubscriptions.values()) {
|
|
1182
|
+
if (!topicMatchesWildcard(sub.pattern, receivedTopic)) continue;
|
|
1183
|
+
if (userBidsPayload === void 0) {
|
|
1184
|
+
try {
|
|
1185
|
+
userBidsPayload = JSON.parse(message.toString());
|
|
1186
|
+
} catch {
|
|
1187
|
+
userBidsPayload = null;
|
|
1188
|
+
sub.onError(new Error(`Invalid JSON on topic ${receivedTopic}`));
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (userBidsPayload != null) sub.onMessage(receivedTopic, userBidsPayload);
|
|
1192
|
+
}
|
|
1162
1193
|
let matched;
|
|
1163
1194
|
for (const sub of subscriptions.values()) {
|
|
1164
1195
|
if (sub.topics.includes(receivedTopic)) {
|
|
@@ -1246,6 +1277,19 @@ function createMqttTransport(endpoint, opts = {}) {
|
|
|
1246
1277
|
if (client) client.unsubscribe(entry.topic);
|
|
1247
1278
|
walletSubscriptions.delete(userId);
|
|
1248
1279
|
},
|
|
1280
|
+
subscribeUserBids(userId, onMessage, onError) {
|
|
1281
|
+
if (userBidsSubscriptions.has(userId)) return;
|
|
1282
|
+
const pattern = userBidsWildcardTopic(userId);
|
|
1283
|
+
const mqttClient = ensureConnected();
|
|
1284
|
+
userBidsSubscriptions.set(userId, { pattern, onMessage, onError });
|
|
1285
|
+
mqttClient.subscribe(pattern, { qos: 1 });
|
|
1286
|
+
},
|
|
1287
|
+
unsubscribeUserBids(userId) {
|
|
1288
|
+
const entry = userBidsSubscriptions.get(userId);
|
|
1289
|
+
if (!entry) return;
|
|
1290
|
+
if (client) client.unsubscribe(entry.pattern);
|
|
1291
|
+
userBidsSubscriptions.delete(userId);
|
|
1292
|
+
},
|
|
1249
1293
|
unsubscribeAll(gameId, userId) {
|
|
1250
1294
|
const matches = entriesForGame(gameId);
|
|
1251
1295
|
if (matches.length === 0) return;
|
|
@@ -1286,11 +1330,15 @@ function createMqttTransport(endpoint, opts = {}) {
|
|
|
1286
1330
|
for (const entry of walletSubscriptions.values()) {
|
|
1287
1331
|
client.unsubscribe(entry.topic);
|
|
1288
1332
|
}
|
|
1333
|
+
for (const entry of userBidsSubscriptions.values()) {
|
|
1334
|
+
client.unsubscribe(entry.pattern);
|
|
1335
|
+
}
|
|
1289
1336
|
}
|
|
1290
1337
|
subscriptions.clear();
|
|
1291
1338
|
candleSubscriptions.clear();
|
|
1292
1339
|
statsSubscriptions.clear();
|
|
1293
1340
|
walletSubscriptions.clear();
|
|
1341
|
+
userBidsSubscriptions.clear();
|
|
1294
1342
|
if (client) {
|
|
1295
1343
|
client.end(true);
|
|
1296
1344
|
client = null;
|
|
@@ -1300,7 +1348,7 @@ function createMqttTransport(endpoint, opts = {}) {
|
|
|
1300
1348
|
}
|
|
1301
1349
|
|
|
1302
1350
|
// src/modules/realtime/index.ts
|
|
1303
|
-
var
|
|
1351
|
+
var import_eventemitter34 = __toESM(require("eventemitter3"));
|
|
1304
1352
|
|
|
1305
1353
|
// src/modules/realtime/GameChannel.ts
|
|
1306
1354
|
var import_eventemitter3 = __toESM(require("eventemitter3"));
|
|
@@ -1312,9 +1360,19 @@ var GameChannel = class extends import_eventemitter3.default {
|
|
|
1312
1360
|
}
|
|
1313
1361
|
};
|
|
1314
1362
|
|
|
1315
|
-
// src/modules/realtime/
|
|
1363
|
+
// src/modules/realtime/UserBidsChannel.ts
|
|
1316
1364
|
var import_eventemitter32 = __toESM(require("eventemitter3"));
|
|
1317
|
-
var
|
|
1365
|
+
var UserBidsChannel = class extends import_eventemitter32.default {
|
|
1366
|
+
userId;
|
|
1367
|
+
constructor(userId) {
|
|
1368
|
+
super();
|
|
1369
|
+
this.userId = userId;
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1373
|
+
// src/modules/realtime/WalletChannel.ts
|
|
1374
|
+
var import_eventemitter33 = __toESM(require("eventemitter3"));
|
|
1375
|
+
var WalletChannel = class extends import_eventemitter33.default {
|
|
1318
1376
|
userId;
|
|
1319
1377
|
constructor(userId) {
|
|
1320
1378
|
super();
|
|
@@ -1322,6 +1380,19 @@ var WalletChannel = class extends import_eventemitter32.default {
|
|
|
1322
1380
|
}
|
|
1323
1381
|
};
|
|
1324
1382
|
|
|
1383
|
+
// src/modules/realtime/topic.ts
|
|
1384
|
+
var BID_RESULT_SUFFIX = "bid_result";
|
|
1385
|
+
function pairIdFromBidResultTopic(topic) {
|
|
1386
|
+
if (!topic.endsWith(`/${BID_RESULT_SUFFIX}`)) return void 0;
|
|
1387
|
+
const parts = topic.split("/");
|
|
1388
|
+
if (parts.length !== 5 || parts[0] !== "game" || parts[2] !== "user") return void 0;
|
|
1389
|
+
const gameId = parts[1];
|
|
1390
|
+
const sep = gameId.indexOf(":");
|
|
1391
|
+
if (sep < 0) return void 0;
|
|
1392
|
+
const pairId = gameId.slice(sep + 1);
|
|
1393
|
+
return pairId === "" ? void 0 : pairId;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1325
1396
|
// src/modules/realtime/index.ts
|
|
1326
1397
|
var TOPIC_SUFFIX_CANDLE = "candle";
|
|
1327
1398
|
var TOPIC_SUFFIX_BID_RESULT = "bid_result";
|
|
@@ -1344,7 +1415,7 @@ function mapWireCandle(raw) {
|
|
|
1344
1415
|
if (c.coefMults !== void 0) result.coefMults = c.coefMults;
|
|
1345
1416
|
return result;
|
|
1346
1417
|
}
|
|
1347
|
-
function mapWireBidResult(raw) {
|
|
1418
|
+
function mapWireBidResult(raw, topicPairId) {
|
|
1348
1419
|
const p = raw;
|
|
1349
1420
|
switch (p.type) {
|
|
1350
1421
|
case "accepted": {
|
|
@@ -1361,13 +1432,13 @@ function mapWireBidResult(raw) {
|
|
|
1361
1432
|
userId: p.user_id
|
|
1362
1433
|
};
|
|
1363
1434
|
if (typeof p.balance === "string") data.balance = p.balance;
|
|
1435
|
+
if (topicPairId !== void 0) data.pairId = topicPairId;
|
|
1364
1436
|
return { event: "bidWon", data };
|
|
1365
1437
|
}
|
|
1366
1438
|
case "lost": {
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
};
|
|
1439
|
+
const data = { bidId: p.bidId };
|
|
1440
|
+
if (topicPairId !== void 0) data.pairId = topicPairId;
|
|
1441
|
+
return { event: "bidLost", data };
|
|
1371
1442
|
}
|
|
1372
1443
|
default:
|
|
1373
1444
|
console.warn(`Unknown bid_result type "${String(p.type)}"`);
|
|
@@ -1415,7 +1486,7 @@ function mapWireToEvent(topic, payload) {
|
|
|
1415
1486
|
return { event: "candle", data: mapWireCandle(payload) };
|
|
1416
1487
|
}
|
|
1417
1488
|
if (topic.endsWith(`/${TOPIC_SUFFIX_BID_RESULT}`)) {
|
|
1418
|
-
return mapWireBidResult(payload);
|
|
1489
|
+
return mapWireBidResult(payload, pairIdFromBidResultTopic(topic));
|
|
1419
1490
|
}
|
|
1420
1491
|
if (topic.endsWith(`/${TOPIC_SUFFIX_BALANCE_UPDATE}`)) {
|
|
1421
1492
|
return { event: "balanceUpdate", data: mapWireBalanceUpdate(payload) };
|
|
@@ -1431,11 +1502,13 @@ function mapWireToEvent(topic, payload) {
|
|
|
1431
1502
|
function normaliseUserId2(userId) {
|
|
1432
1503
|
return userId && userId !== "" ? userId : null;
|
|
1433
1504
|
}
|
|
1434
|
-
var RealtimeModule = class extends
|
|
1505
|
+
var RealtimeModule = class extends import_eventemitter34.default {
|
|
1435
1506
|
#transport;
|
|
1436
1507
|
#entries = /* @__PURE__ */ new Map();
|
|
1437
1508
|
#walletEntries = /* @__PURE__ */ new Map();
|
|
1438
1509
|
// keyed by userId
|
|
1510
|
+
#userBidsEntries = /* @__PURE__ */ new Map();
|
|
1511
|
+
// keyed by userId
|
|
1439
1512
|
#agencyId;
|
|
1440
1513
|
constructor(mqttEndpointOrOptions) {
|
|
1441
1514
|
super();
|
|
@@ -1566,6 +1639,64 @@ var RealtimeModule = class extends import_eventemitter33.default {
|
|
|
1566
1639
|
entry.channel.removeAllListeners();
|
|
1567
1640
|
this.#transport.unsubscribeWallet(cleanUserId);
|
|
1568
1641
|
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Subscribe to the user's bid-result stream across ALL pairs, via the wildcard
|
|
1644
|
+
* topic `game/+/user/{userId}/bid_result`. Returns a `UserBidsChannel` that
|
|
1645
|
+
* emits `bidWon` / `bidLost` for settlements on any pair the user holds a bid
|
|
1646
|
+
* on — independent of which pair is currently displayed (mirrors
|
|
1647
|
+
* `subscribeWallet`). Each emitted event carries the bid's `pairId`, parsed
|
|
1648
|
+
* from the concrete result topic, so consumers can attribute and label it.
|
|
1649
|
+
*
|
|
1650
|
+
* Reference-counted by `userId`. The active pair's bid_result is ALSO delivered
|
|
1651
|
+
* on the per-pair `subscribe` channel — that is intentional: the per-pair
|
|
1652
|
+
* channel drives state + sound, this cross-pair channel drives notifications.
|
|
1653
|
+
*/
|
|
1654
|
+
subscribeUserBids(userId) {
|
|
1655
|
+
const cleanUserId = normaliseUserId2(userId);
|
|
1656
|
+
if (!cleanUserId) {
|
|
1657
|
+
throw new TaphubError("userId is required to subscribe to user bids", {
|
|
1658
|
+
code: "UserIdRequired"
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
if (/[/+#]/.test(cleanUserId)) {
|
|
1662
|
+
throw new TaphubError("userId must not contain MQTT topic separators (/ + #)", {
|
|
1663
|
+
code: "InvalidUserId"
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
const existing = this.#userBidsEntries.get(cleanUserId);
|
|
1667
|
+
if (existing) {
|
|
1668
|
+
existing.refcount += 1;
|
|
1669
|
+
return existing.channel;
|
|
1670
|
+
}
|
|
1671
|
+
const channel = new UserBidsChannel(cleanUserId);
|
|
1672
|
+
const onMessage = (topic, payload) => {
|
|
1673
|
+
const mapped = mapWireBidResult(payload, pairIdFromBidResultTopic(topic));
|
|
1674
|
+
if (!mapped) return;
|
|
1675
|
+
if (mapped.event === "bidWon") channel.emit("bidWon", mapped.data);
|
|
1676
|
+
else if (mapped.event === "bidLost") channel.emit("bidLost", mapped.data);
|
|
1677
|
+
};
|
|
1678
|
+
const onError = (err) => {
|
|
1679
|
+
channel.emit("error", err);
|
|
1680
|
+
};
|
|
1681
|
+
this.#userBidsEntries.set(cleanUserId, { userId: cleanUserId, channel, refcount: 1 });
|
|
1682
|
+
this.#transport.subscribeUserBids(cleanUserId, onMessage, onError);
|
|
1683
|
+
return channel;
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Decrement the cross-pair user-bids subscription refcount for `userId`. Tears
|
|
1687
|
+
* down the wildcard topic and removes the channel only at zero. No-op if absent.
|
|
1688
|
+
*/
|
|
1689
|
+
unsubscribeUserBids(userId) {
|
|
1690
|
+
const cleanUserId = normaliseUserId2(userId);
|
|
1691
|
+
if (!cleanUserId) return;
|
|
1692
|
+
const entry = this.#userBidsEntries.get(cleanUserId);
|
|
1693
|
+
if (!entry) return;
|
|
1694
|
+
entry.refcount -= 1;
|
|
1695
|
+
if (entry.refcount > 0) return;
|
|
1696
|
+
this.#userBidsEntries.delete(cleanUserId);
|
|
1697
|
+
entry.channel.removeAllListeners();
|
|
1698
|
+
this.#transport.unsubscribeUserBids(cleanUserId);
|
|
1699
|
+
}
|
|
1569
1700
|
/**
|
|
1570
1701
|
* Subscribe to public market candle data for a pair, keyed by pairId
|
|
1571
1702
|
* (the #2 game_pairs.id, e.g. "grid-ETH-USD"). Independent of game/agency —
|
|
@@ -1617,6 +1748,10 @@ var RealtimeModule = class extends import_eventemitter33.default {
|
|
|
1617
1748
|
entry.channel.removeAllListeners();
|
|
1618
1749
|
}
|
|
1619
1750
|
this.#walletEntries.clear();
|
|
1751
|
+
for (const entry of this.#userBidsEntries.values()) {
|
|
1752
|
+
entry.channel.removeAllListeners();
|
|
1753
|
+
}
|
|
1754
|
+
this.#userBidsEntries.clear();
|
|
1620
1755
|
this.#transport.close();
|
|
1621
1756
|
}
|
|
1622
1757
|
};
|
|
@@ -1848,20 +1983,9 @@ function classifyNetworkLevel(rtt) {
|
|
|
1848
1983
|
// src/network/RttSmoother.ts
|
|
1849
1984
|
var EMA_ALPHA = 0.25;
|
|
1850
1985
|
var OUTLIER_MULTIPLIER = 3;
|
|
1851
|
-
var DEBOUNCE_DEGRADE = 2;
|
|
1852
|
-
var DEBOUNCE_IMPROVE = 5;
|
|
1853
|
-
var LEVEL_RANK = {
|
|
1854
|
-
good: 0,
|
|
1855
|
-
fair: 1,
|
|
1856
|
-
poor: 2,
|
|
1857
|
-
offline: 3
|
|
1858
|
-
};
|
|
1859
1986
|
var RttSmoother = class {
|
|
1860
1987
|
ema = 0;
|
|
1861
1988
|
level = "good";
|
|
1862
|
-
committed = "good";
|
|
1863
|
-
candidate = "good";
|
|
1864
|
-
candidateCount = 0;
|
|
1865
1989
|
overrideOffline = false;
|
|
1866
1990
|
consecutiveOutliers = 0;
|
|
1867
1991
|
add(rtt) {
|
|
@@ -1873,23 +1997,7 @@ var RttSmoother = class {
|
|
|
1873
1997
|
}
|
|
1874
1998
|
const coldStart = this.ema === 0;
|
|
1875
1999
|
this.ema = coldStart ? rtt : EMA_ALPHA * rtt + (1 - EMA_ALPHA) * this.ema;
|
|
1876
|
-
|
|
1877
|
-
if (coldStart) {
|
|
1878
|
-
this.committed = next;
|
|
1879
|
-
this.candidate = next;
|
|
1880
|
-
this.candidateCount = 1;
|
|
1881
|
-
} else {
|
|
1882
|
-
if (next === this.candidate) {
|
|
1883
|
-
this.candidateCount += 1;
|
|
1884
|
-
} else {
|
|
1885
|
-
this.candidate = next;
|
|
1886
|
-
this.candidateCount = 1;
|
|
1887
|
-
}
|
|
1888
|
-
if (this.candidateCount >= this.requiredSamplesFor(next)) {
|
|
1889
|
-
this.committed = next;
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
this.level = this.overrideOffline ? "offline" : this.committed;
|
|
2000
|
+
this.level = this.overrideOffline ? "offline" : classifyNetworkLevel(this.ema);
|
|
1893
2001
|
}
|
|
1894
2002
|
forceOffline() {
|
|
1895
2003
|
this.overrideOffline = true;
|
|
@@ -1897,20 +2005,14 @@ var RttSmoother = class {
|
|
|
1897
2005
|
}
|
|
1898
2006
|
releaseOffline() {
|
|
1899
2007
|
this.overrideOffline = false;
|
|
1900
|
-
this.level = this.
|
|
2008
|
+
this.level = classifyNetworkLevel(this.ema);
|
|
1901
2009
|
}
|
|
1902
2010
|
reset() {
|
|
1903
2011
|
this.ema = 0;
|
|
1904
|
-
this.committed = "good";
|
|
1905
|
-
this.candidate = "good";
|
|
1906
|
-
this.candidateCount = 0;
|
|
1907
2012
|
this.consecutiveOutliers = 0;
|
|
1908
2013
|
this.overrideOffline = false;
|
|
1909
2014
|
this.level = "good";
|
|
1910
2015
|
}
|
|
1911
|
-
requiredSamplesFor(next) {
|
|
1912
|
-
return LEVEL_RANK[next] > LEVEL_RANK[this.committed] ? DEBOUNCE_DEGRADE : DEBOUNCE_IMPROVE;
|
|
1913
|
-
}
|
|
1914
2016
|
};
|
|
1915
2017
|
|
|
1916
2018
|
// src/network/NetworkQualityMonitor.ts
|
|
@@ -1937,7 +2039,6 @@ var NetworkQualityMonitor = class {
|
|
|
1937
2039
|
smoother = new RttSmoother();
|
|
1938
2040
|
mqttConnected = false;
|
|
1939
2041
|
mqttDisconnectedAt = null;
|
|
1940
|
-
committedNetwork = "good";
|
|
1941
2042
|
committedBackend = "ok";
|
|
1942
2043
|
// The last snapshot we emitted, used to decide whether the next recompute is
|
|
1943
2044
|
// worth emitting. Seeded with the initial default so the first real change
|
|
@@ -2015,7 +2116,6 @@ var NetworkQualityMonitor = class {
|
|
|
2015
2116
|
this.smoother.reset();
|
|
2016
2117
|
this.mqttConnected = false;
|
|
2017
2118
|
this.mqttDisconnectedAt = null;
|
|
2018
|
-
this.committedNetwork = "good";
|
|
2019
2119
|
this.committedBackend = "ok";
|
|
2020
2120
|
this.emitIfChanged(this.snapshot());
|
|
2021
2121
|
}
|
|
@@ -2079,26 +2179,12 @@ var NetworkQualityMonitor = class {
|
|
|
2079
2179
|
const useConnection = realSamples.length < COLD_START_REAL_SAMPLE_THRESHOLD;
|
|
2080
2180
|
const effectiveSamples = useConnection ? allSamples : realSamples;
|
|
2081
2181
|
const httpSamples = effectiveSamples.filter((s) => isHttpSource(s.source));
|
|
2082
|
-
|
|
2083
|
-
const metrics = this.deriveMetricsFrom(effectiveSamples);
|
|
2084
|
-
const candidate = classifyNetworkLevel(metrics.emaForLevel);
|
|
2085
|
-
let nextNetwork = this.smoother.level;
|
|
2086
|
-
if (effectiveSamples.length === 0) {
|
|
2087
|
-
nextNetwork = this.committedNetwork;
|
|
2088
|
-
} else if (this.smoother.ema > 0) {
|
|
2089
|
-
nextNetwork = this.smoother.level;
|
|
2090
|
-
} else {
|
|
2091
|
-
nextNetwork = candidate;
|
|
2092
|
-
}
|
|
2182
|
+
this.committedBackend = classifyBackendHealth(httpSamples);
|
|
2093
2183
|
if (this.isHardOffline()) {
|
|
2094
2184
|
this.smoother.forceOffline();
|
|
2095
|
-
nextNetwork = "offline";
|
|
2096
2185
|
} else if (this.smoother.level === "offline") {
|
|
2097
2186
|
this.smoother.releaseOffline();
|
|
2098
|
-
nextNetwork = this.smoother.level;
|
|
2099
2187
|
}
|
|
2100
|
-
this.committedNetwork = nextNetwork;
|
|
2101
|
-
this.committedBackend = backend;
|
|
2102
2188
|
this.emitIfChanged(this.snapshot());
|
|
2103
2189
|
}
|
|
2104
2190
|
// Emits network:change when any *displayed* field of the snapshot moves. This
|
|
@@ -2159,10 +2245,12 @@ var NetworkQualityMonitor = class {
|
|
|
2159
2245
|
snapshot() {
|
|
2160
2246
|
const samples = this.allHttpAndMqttSamples();
|
|
2161
2247
|
const metrics = this.deriveMetricsFrom(samples);
|
|
2248
|
+
const rtt = Math.round(metrics.emaForLevel);
|
|
2249
|
+
const network = this.smoother.level === "offline" ? "offline" : classifyNetworkLevel(rtt);
|
|
2162
2250
|
return {
|
|
2163
|
-
network
|
|
2251
|
+
network,
|
|
2164
2252
|
backend: this.committedBackend,
|
|
2165
|
-
rtt
|
|
2253
|
+
rtt,
|
|
2166
2254
|
jitter: Math.round(metrics.jitter),
|
|
2167
2255
|
lossRate: metrics.lossRate,
|
|
2168
2256
|
mqttConnected: this.mqttConnected,
|
|
@@ -2983,5 +3071,6 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
|
|
|
2983
3071
|
isTerminal,
|
|
2984
3072
|
isWin,
|
|
2985
3073
|
normalCDF,
|
|
2986
|
-
normalPDF
|
|
3074
|
+
normalPDF,
|
|
3075
|
+
pairIdFromBidResultTopic
|
|
2987
3076
|
});
|
package/dist/index.d.mts
CHANGED
|
@@ -694,6 +694,7 @@ type MqttLifecycleHook = (event: MqttLifecycleEvent) => void;
|
|
|
694
694
|
type MqttCandleHandler = (topic: string, payload: MqttWirePayload) => void;
|
|
695
695
|
type MqttAgencyPairStatsHandler = (topic: string, payload: MqttWirePayload) => void;
|
|
696
696
|
type MqttWalletMessageHandler = (topic: string, payload: MqttWirePayload) => void;
|
|
697
|
+
type MqttUserBidsMessageHandler = (topic: string, payload: MqttWirePayload) => void;
|
|
697
698
|
interface MqttTransport {
|
|
698
699
|
/**
|
|
699
700
|
* Subscribe to a game's MQTT topics (config, ideal_config, bid_result, balance)
|
|
@@ -737,6 +738,16 @@ interface MqttTransport {
|
|
|
737
738
|
subscribeWallet(userId: string, onMessage: MqttWalletMessageHandler, onError: MqttErrorHandler): void;
|
|
738
739
|
/** Tear down the wallet subscription for a `userId`. No-op if not subscribed. */
|
|
739
740
|
unsubscribeWallet(userId: string): void;
|
|
741
|
+
/**
|
|
742
|
+
* Subscribe to the user's bid-result stream across ALL pairs via the wildcard
|
|
743
|
+
* topic `game/+/user/{userId}/bid_result`. Independent of any game
|
|
744
|
+
* subscription — NOT torn down by `unsubscribeAll`, survives pair switches.
|
|
745
|
+
* Idempotent per `userId`. The handler receives the CONCRETE delivered topic
|
|
746
|
+
* (with the real gameId), so the caller can parse the pairId from it.
|
|
747
|
+
*/
|
|
748
|
+
subscribeUserBids(userId: string, onMessage: MqttUserBidsMessageHandler, onError: MqttErrorHandler): void;
|
|
749
|
+
/** Tear down the cross-pair user-bids subscription for a `userId`. No-op if absent. */
|
|
750
|
+
unsubscribeUserBids(userId: string): void;
|
|
740
751
|
close(): void;
|
|
741
752
|
}
|
|
742
753
|
|
|
@@ -791,9 +802,19 @@ interface MqttBidWonEvent {
|
|
|
791
802
|
payout: string;
|
|
792
803
|
userId: string;
|
|
793
804
|
balance?: string;
|
|
805
|
+
/**
|
|
806
|
+
* Pair the won bid belongs to, e.g. "grid-ETH-USD". Derived from the result
|
|
807
|
+
* topic (`game/{agencyId}:{pairId}/user/.../bid_result`) since the won payload
|
|
808
|
+
* carries no pair. Lets consumers attribute a result to its pair — required by
|
|
809
|
+
* the cross-pair subscription (a win for a non-active pair). Undefined only
|
|
810
|
+
* when the topic shape is unrecognised.
|
|
811
|
+
*/
|
|
812
|
+
pairId?: string;
|
|
794
813
|
}
|
|
795
814
|
interface MqttBidLostEvent {
|
|
796
815
|
bidId: string;
|
|
816
|
+
/** Pair the lost bid belongs to (see {@link MqttBidWonEvent.pairId}). */
|
|
817
|
+
pairId?: string;
|
|
797
818
|
}
|
|
798
819
|
interface MqttBidCancelledEvent {
|
|
799
820
|
bidId: string;
|
|
@@ -878,6 +899,24 @@ declare class GameChannel extends EventEmitter<GameChannelEvents> {
|
|
|
878
899
|
constructor(gameId: string);
|
|
879
900
|
}
|
|
880
901
|
|
|
902
|
+
interface UserBidsChannelEvents {
|
|
903
|
+
bidWon: [MqttBidWonEvent];
|
|
904
|
+
bidLost: [MqttBidLostEvent];
|
|
905
|
+
error: [Error];
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* User-scoped, cross-pair bid-result channel. Emits `bidWon` / `bidLost` for the
|
|
909
|
+
* user's settlements on ANY pair, via the wildcard subscription
|
|
910
|
+
* `game/+/user/{userId}/bid_result`. Independent of which pair is currently
|
|
911
|
+
* displayed — keyed by `userId`, not `gameId`, so it survives pair switches
|
|
912
|
+
* (mirror of {@link WalletChannel}). Each emitted event carries the bid's
|
|
913
|
+
* `pairId` (parsed from the result topic) so consumers can attribute it.
|
|
914
|
+
*/
|
|
915
|
+
declare class UserBidsChannel extends EventEmitter<UserBidsChannelEvents> {
|
|
916
|
+
readonly userId: string;
|
|
917
|
+
constructor(userId: string);
|
|
918
|
+
}
|
|
919
|
+
|
|
881
920
|
interface WalletChannelEvents {
|
|
882
921
|
walletBalanceUpdate: [MqttWalletBalanceEvent];
|
|
883
922
|
error: [Error];
|
|
@@ -946,6 +985,24 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
|
|
|
946
985
|
* No-op if there is no active wallet subscription for `userId`.
|
|
947
986
|
*/
|
|
948
987
|
unsubscribeWallet(userId: string): void;
|
|
988
|
+
/**
|
|
989
|
+
* Subscribe to the user's bid-result stream across ALL pairs, via the wildcard
|
|
990
|
+
* topic `game/+/user/{userId}/bid_result`. Returns a `UserBidsChannel` that
|
|
991
|
+
* emits `bidWon` / `bidLost` for settlements on any pair the user holds a bid
|
|
992
|
+
* on — independent of which pair is currently displayed (mirrors
|
|
993
|
+
* `subscribeWallet`). Each emitted event carries the bid's `pairId`, parsed
|
|
994
|
+
* from the concrete result topic, so consumers can attribute and label it.
|
|
995
|
+
*
|
|
996
|
+
* Reference-counted by `userId`. The active pair's bid_result is ALSO delivered
|
|
997
|
+
* on the per-pair `subscribe` channel — that is intentional: the per-pair
|
|
998
|
+
* channel drives state + sound, this cross-pair channel drives notifications.
|
|
999
|
+
*/
|
|
1000
|
+
subscribeUserBids(userId: string): UserBidsChannel;
|
|
1001
|
+
/**
|
|
1002
|
+
* Decrement the cross-pair user-bids subscription refcount for `userId`. Tears
|
|
1003
|
+
* down the wildcard topic and removes the channel only at zero. No-op if absent.
|
|
1004
|
+
*/
|
|
1005
|
+
unsubscribeUserBids(userId: string): void;
|
|
949
1006
|
/**
|
|
950
1007
|
* Subscribe to public market candle data for a pair, keyed by pairId
|
|
951
1008
|
* (the #2 game_pairs.id, e.g. "grid-ETH-USD"). Independent of game/agency —
|
|
@@ -1042,7 +1099,6 @@ declare class NetworkQualityMonitor {
|
|
|
1042
1099
|
private readonly smoother;
|
|
1043
1100
|
private mqttConnected;
|
|
1044
1101
|
private mqttDisconnectedAt;
|
|
1045
|
-
private committedNetwork;
|
|
1046
1102
|
private committedBackend;
|
|
1047
1103
|
private lastEmitted;
|
|
1048
1104
|
private tickHandle;
|
|
@@ -1175,6 +1231,20 @@ declare class TaphubSlippageError extends TaphubValidationError {
|
|
|
1175
1231
|
});
|
|
1176
1232
|
}
|
|
1177
1233
|
|
|
1234
|
+
/**
|
|
1235
|
+
* Extract the slash-free pairId from a bid-result topic.
|
|
1236
|
+
*
|
|
1237
|
+
* Accepts the concrete topic delivered by the broker, e.g.
|
|
1238
|
+
* `game/acme-corp:grid-ETH-USD/user/{uid}/bid_result` → `grid-ETH-USD`.
|
|
1239
|
+
*
|
|
1240
|
+
* Returns `undefined` when the topic does not match the expected shape (so the
|
|
1241
|
+
* caller can fall back gracefully rather than throw). The gameId segment is the
|
|
1242
|
+
* composite `{agencyId}:{pairId}`; the pairId is everything after the FIRST `:`
|
|
1243
|
+
* (pairId itself is slash-free and colon-free by the bid-260602-v2 id rules, but
|
|
1244
|
+
* we split on the first `:` to be safe if an agencyId ever contained one).
|
|
1245
|
+
*/
|
|
1246
|
+
declare function pairIdFromBidResultTopic(topic: string): string | undefined;
|
|
1247
|
+
|
|
1178
1248
|
declare function errorFunction(x: number): number;
|
|
1179
1249
|
declare function normalCDF(x: number): number;
|
|
1180
1250
|
declare function normalPDF(x: number): number;
|
|
@@ -1182,4 +1252,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
|
|
|
1182
1252
|
declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
|
|
1183
1253
|
declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
|
|
1184
1254
|
|
|
1185
|
-
export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, GameChannel, type GameConfig, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttAuthConfig, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type MyRank, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type Pair, type PairInfo, PairModule, type PlaceBidInput, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type Sample, type SampleReason, type SignalSource, type SortDir, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, type WalletBalanceReason, WalletChannel, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF };
|
|
1255
|
+
export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, GameChannel, type GameConfig, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttAuthConfig, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type MyRank, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type Pair, type PairInfo, PairModule, type PlaceBidInput, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type Sample, type SampleReason, type SignalSource, type SortDir, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserBidsChannel, type UserBidsChannelEvents, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, type WalletBalanceReason, WalletChannel, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, pairIdFromBidResultTopic };
|
package/dist/index.d.ts
CHANGED
|
@@ -694,6 +694,7 @@ type MqttLifecycleHook = (event: MqttLifecycleEvent) => void;
|
|
|
694
694
|
type MqttCandleHandler = (topic: string, payload: MqttWirePayload) => void;
|
|
695
695
|
type MqttAgencyPairStatsHandler = (topic: string, payload: MqttWirePayload) => void;
|
|
696
696
|
type MqttWalletMessageHandler = (topic: string, payload: MqttWirePayload) => void;
|
|
697
|
+
type MqttUserBidsMessageHandler = (topic: string, payload: MqttWirePayload) => void;
|
|
697
698
|
interface MqttTransport {
|
|
698
699
|
/**
|
|
699
700
|
* Subscribe to a game's MQTT topics (config, ideal_config, bid_result, balance)
|
|
@@ -737,6 +738,16 @@ interface MqttTransport {
|
|
|
737
738
|
subscribeWallet(userId: string, onMessage: MqttWalletMessageHandler, onError: MqttErrorHandler): void;
|
|
738
739
|
/** Tear down the wallet subscription for a `userId`. No-op if not subscribed. */
|
|
739
740
|
unsubscribeWallet(userId: string): void;
|
|
741
|
+
/**
|
|
742
|
+
* Subscribe to the user's bid-result stream across ALL pairs via the wildcard
|
|
743
|
+
* topic `game/+/user/{userId}/bid_result`. Independent of any game
|
|
744
|
+
* subscription — NOT torn down by `unsubscribeAll`, survives pair switches.
|
|
745
|
+
* Idempotent per `userId`. The handler receives the CONCRETE delivered topic
|
|
746
|
+
* (with the real gameId), so the caller can parse the pairId from it.
|
|
747
|
+
*/
|
|
748
|
+
subscribeUserBids(userId: string, onMessage: MqttUserBidsMessageHandler, onError: MqttErrorHandler): void;
|
|
749
|
+
/** Tear down the cross-pair user-bids subscription for a `userId`. No-op if absent. */
|
|
750
|
+
unsubscribeUserBids(userId: string): void;
|
|
740
751
|
close(): void;
|
|
741
752
|
}
|
|
742
753
|
|
|
@@ -791,9 +802,19 @@ interface MqttBidWonEvent {
|
|
|
791
802
|
payout: string;
|
|
792
803
|
userId: string;
|
|
793
804
|
balance?: string;
|
|
805
|
+
/**
|
|
806
|
+
* Pair the won bid belongs to, e.g. "grid-ETH-USD". Derived from the result
|
|
807
|
+
* topic (`game/{agencyId}:{pairId}/user/.../bid_result`) since the won payload
|
|
808
|
+
* carries no pair. Lets consumers attribute a result to its pair — required by
|
|
809
|
+
* the cross-pair subscription (a win for a non-active pair). Undefined only
|
|
810
|
+
* when the topic shape is unrecognised.
|
|
811
|
+
*/
|
|
812
|
+
pairId?: string;
|
|
794
813
|
}
|
|
795
814
|
interface MqttBidLostEvent {
|
|
796
815
|
bidId: string;
|
|
816
|
+
/** Pair the lost bid belongs to (see {@link MqttBidWonEvent.pairId}). */
|
|
817
|
+
pairId?: string;
|
|
797
818
|
}
|
|
798
819
|
interface MqttBidCancelledEvent {
|
|
799
820
|
bidId: string;
|
|
@@ -878,6 +899,24 @@ declare class GameChannel extends EventEmitter<GameChannelEvents> {
|
|
|
878
899
|
constructor(gameId: string);
|
|
879
900
|
}
|
|
880
901
|
|
|
902
|
+
interface UserBidsChannelEvents {
|
|
903
|
+
bidWon: [MqttBidWonEvent];
|
|
904
|
+
bidLost: [MqttBidLostEvent];
|
|
905
|
+
error: [Error];
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* User-scoped, cross-pair bid-result channel. Emits `bidWon` / `bidLost` for the
|
|
909
|
+
* user's settlements on ANY pair, via the wildcard subscription
|
|
910
|
+
* `game/+/user/{userId}/bid_result`. Independent of which pair is currently
|
|
911
|
+
* displayed — keyed by `userId`, not `gameId`, so it survives pair switches
|
|
912
|
+
* (mirror of {@link WalletChannel}). Each emitted event carries the bid's
|
|
913
|
+
* `pairId` (parsed from the result topic) so consumers can attribute it.
|
|
914
|
+
*/
|
|
915
|
+
declare class UserBidsChannel extends EventEmitter<UserBidsChannelEvents> {
|
|
916
|
+
readonly userId: string;
|
|
917
|
+
constructor(userId: string);
|
|
918
|
+
}
|
|
919
|
+
|
|
881
920
|
interface WalletChannelEvents {
|
|
882
921
|
walletBalanceUpdate: [MqttWalletBalanceEvent];
|
|
883
922
|
error: [Error];
|
|
@@ -946,6 +985,24 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
|
|
|
946
985
|
* No-op if there is no active wallet subscription for `userId`.
|
|
947
986
|
*/
|
|
948
987
|
unsubscribeWallet(userId: string): void;
|
|
988
|
+
/**
|
|
989
|
+
* Subscribe to the user's bid-result stream across ALL pairs, via the wildcard
|
|
990
|
+
* topic `game/+/user/{userId}/bid_result`. Returns a `UserBidsChannel` that
|
|
991
|
+
* emits `bidWon` / `bidLost` for settlements on any pair the user holds a bid
|
|
992
|
+
* on — independent of which pair is currently displayed (mirrors
|
|
993
|
+
* `subscribeWallet`). Each emitted event carries the bid's `pairId`, parsed
|
|
994
|
+
* from the concrete result topic, so consumers can attribute and label it.
|
|
995
|
+
*
|
|
996
|
+
* Reference-counted by `userId`. The active pair's bid_result is ALSO delivered
|
|
997
|
+
* on the per-pair `subscribe` channel — that is intentional: the per-pair
|
|
998
|
+
* channel drives state + sound, this cross-pair channel drives notifications.
|
|
999
|
+
*/
|
|
1000
|
+
subscribeUserBids(userId: string): UserBidsChannel;
|
|
1001
|
+
/**
|
|
1002
|
+
* Decrement the cross-pair user-bids subscription refcount for `userId`. Tears
|
|
1003
|
+
* down the wildcard topic and removes the channel only at zero. No-op if absent.
|
|
1004
|
+
*/
|
|
1005
|
+
unsubscribeUserBids(userId: string): void;
|
|
949
1006
|
/**
|
|
950
1007
|
* Subscribe to public market candle data for a pair, keyed by pairId
|
|
951
1008
|
* (the #2 game_pairs.id, e.g. "grid-ETH-USD"). Independent of game/agency —
|
|
@@ -1042,7 +1099,6 @@ declare class NetworkQualityMonitor {
|
|
|
1042
1099
|
private readonly smoother;
|
|
1043
1100
|
private mqttConnected;
|
|
1044
1101
|
private mqttDisconnectedAt;
|
|
1045
|
-
private committedNetwork;
|
|
1046
1102
|
private committedBackend;
|
|
1047
1103
|
private lastEmitted;
|
|
1048
1104
|
private tickHandle;
|
|
@@ -1175,6 +1231,20 @@ declare class TaphubSlippageError extends TaphubValidationError {
|
|
|
1175
1231
|
});
|
|
1176
1232
|
}
|
|
1177
1233
|
|
|
1234
|
+
/**
|
|
1235
|
+
* Extract the slash-free pairId from a bid-result topic.
|
|
1236
|
+
*
|
|
1237
|
+
* Accepts the concrete topic delivered by the broker, e.g.
|
|
1238
|
+
* `game/acme-corp:grid-ETH-USD/user/{uid}/bid_result` → `grid-ETH-USD`.
|
|
1239
|
+
*
|
|
1240
|
+
* Returns `undefined` when the topic does not match the expected shape (so the
|
|
1241
|
+
* caller can fall back gracefully rather than throw). The gameId segment is the
|
|
1242
|
+
* composite `{agencyId}:{pairId}`; the pairId is everything after the FIRST `:`
|
|
1243
|
+
* (pairId itself is slash-free and colon-free by the bid-260602-v2 id rules, but
|
|
1244
|
+
* we split on the first `:` to be safe if an agencyId ever contained one).
|
|
1245
|
+
*/
|
|
1246
|
+
declare function pairIdFromBidResultTopic(topic: string): string | undefined;
|
|
1247
|
+
|
|
1178
1248
|
declare function errorFunction(x: number): number;
|
|
1179
1249
|
declare function normalCDF(x: number): number;
|
|
1180
1250
|
declare function normalPDF(x: number): number;
|
|
@@ -1182,4 +1252,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
|
|
|
1182
1252
|
declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
|
|
1183
1253
|
declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
|
|
1184
1254
|
|
|
1185
|
-
export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, GameChannel, type GameConfig, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttAuthConfig, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type MyRank, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type Pair, type PairInfo, PairModule, type PlaceBidInput, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type Sample, type SampleReason, type SignalSource, type SortDir, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, type WalletBalanceReason, WalletChannel, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF };
|
|
1255
|
+
export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, GameChannel, type GameConfig, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttAuthConfig, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type MyRank, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type Pair, type PairInfo, PairModule, type PlaceBidInput, type RankBoard, type RankEntry, type RankPeriod, type RankSort, RealtimeModule, type Sample, type SampleReason, type SignalSource, type SortDir, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserBidsChannel, type UserBidsChannelEvents, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, type WalletBalanceReason, WalletChannel, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF, pairIdFromBidResultTopic };
|
package/dist/index.js
CHANGED
|
@@ -978,6 +978,19 @@ function walletBalanceTopic(userId) {
|
|
|
978
978
|
function agencyPairStatsTopic(aid, pairId) {
|
|
979
979
|
return `public/agency/${aid}/pair/${pairId}/stats`;
|
|
980
980
|
}
|
|
981
|
+
function userBidsWildcardTopic(userId) {
|
|
982
|
+
return `${TOPIC_PREFIX}/+/user/${userId}/bid_result`;
|
|
983
|
+
}
|
|
984
|
+
function topicMatchesWildcard(pattern, topic) {
|
|
985
|
+
const pp = pattern.split("/");
|
|
986
|
+
const tp = topic.split("/");
|
|
987
|
+
if (pp.length !== tp.length) return false;
|
|
988
|
+
for (let i = 0; i < pp.length; i++) {
|
|
989
|
+
if (pp[i] === "+") continue;
|
|
990
|
+
if (pp[i] !== tp[i]) return false;
|
|
991
|
+
}
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
981
994
|
function topicFor(gameId, suffix, userId) {
|
|
982
995
|
if (USER_SCOPED_SUFFIXES.includes(suffix)) {
|
|
983
996
|
return `${TOPIC_PREFIX}/${gameId}/user/${userId}/${suffix}`;
|
|
@@ -996,6 +1009,7 @@ function createMqttTransport(endpoint, opts = {}) {
|
|
|
996
1009
|
const candleSubscriptions = /* @__PURE__ */ new Map();
|
|
997
1010
|
const statsSubscriptions = /* @__PURE__ */ new Map();
|
|
998
1011
|
const walletSubscriptions = /* @__PURE__ */ new Map();
|
|
1012
|
+
const userBidsSubscriptions = /* @__PURE__ */ new Map();
|
|
999
1013
|
const { onLifecycle, auth } = opts;
|
|
1000
1014
|
let connectStartedAt = 0;
|
|
1001
1015
|
function fireLifecycle(event) {
|
|
@@ -1046,6 +1060,9 @@ function createMqttTransport(endpoint, opts = {}) {
|
|
|
1046
1060
|
for (const entry of walletSubscriptions.values()) {
|
|
1047
1061
|
c.subscribe(entry.topic, { qos: 1 });
|
|
1048
1062
|
}
|
|
1063
|
+
for (const entry of userBidsSubscriptions.values()) {
|
|
1064
|
+
c.subscribe(entry.pattern, { qos: 1 });
|
|
1065
|
+
}
|
|
1049
1066
|
fireLifecycle({ kind: "connect", rttMs: Date.now() - connectStartedAt });
|
|
1050
1067
|
});
|
|
1051
1068
|
client.on("reconnect", () => {
|
|
@@ -1094,6 +1111,19 @@ function createMqttTransport(endpoint, opts = {}) {
|
|
|
1094
1111
|
walletSub.onMessage(receivedTopic, payload2);
|
|
1095
1112
|
return;
|
|
1096
1113
|
}
|
|
1114
|
+
let userBidsPayload;
|
|
1115
|
+
for (const sub of userBidsSubscriptions.values()) {
|
|
1116
|
+
if (!topicMatchesWildcard(sub.pattern, receivedTopic)) continue;
|
|
1117
|
+
if (userBidsPayload === void 0) {
|
|
1118
|
+
try {
|
|
1119
|
+
userBidsPayload = JSON.parse(message.toString());
|
|
1120
|
+
} catch {
|
|
1121
|
+
userBidsPayload = null;
|
|
1122
|
+
sub.onError(new Error(`Invalid JSON on topic ${receivedTopic}`));
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if (userBidsPayload != null) sub.onMessage(receivedTopic, userBidsPayload);
|
|
1126
|
+
}
|
|
1097
1127
|
let matched;
|
|
1098
1128
|
for (const sub of subscriptions.values()) {
|
|
1099
1129
|
if (sub.topics.includes(receivedTopic)) {
|
|
@@ -1181,6 +1211,19 @@ function createMqttTransport(endpoint, opts = {}) {
|
|
|
1181
1211
|
if (client) client.unsubscribe(entry.topic);
|
|
1182
1212
|
walletSubscriptions.delete(userId);
|
|
1183
1213
|
},
|
|
1214
|
+
subscribeUserBids(userId, onMessage, onError) {
|
|
1215
|
+
if (userBidsSubscriptions.has(userId)) return;
|
|
1216
|
+
const pattern = userBidsWildcardTopic(userId);
|
|
1217
|
+
const mqttClient = ensureConnected();
|
|
1218
|
+
userBidsSubscriptions.set(userId, { pattern, onMessage, onError });
|
|
1219
|
+
mqttClient.subscribe(pattern, { qos: 1 });
|
|
1220
|
+
},
|
|
1221
|
+
unsubscribeUserBids(userId) {
|
|
1222
|
+
const entry = userBidsSubscriptions.get(userId);
|
|
1223
|
+
if (!entry) return;
|
|
1224
|
+
if (client) client.unsubscribe(entry.pattern);
|
|
1225
|
+
userBidsSubscriptions.delete(userId);
|
|
1226
|
+
},
|
|
1184
1227
|
unsubscribeAll(gameId, userId) {
|
|
1185
1228
|
const matches = entriesForGame(gameId);
|
|
1186
1229
|
if (matches.length === 0) return;
|
|
@@ -1221,11 +1264,15 @@ function createMqttTransport(endpoint, opts = {}) {
|
|
|
1221
1264
|
for (const entry of walletSubscriptions.values()) {
|
|
1222
1265
|
client.unsubscribe(entry.topic);
|
|
1223
1266
|
}
|
|
1267
|
+
for (const entry of userBidsSubscriptions.values()) {
|
|
1268
|
+
client.unsubscribe(entry.pattern);
|
|
1269
|
+
}
|
|
1224
1270
|
}
|
|
1225
1271
|
subscriptions.clear();
|
|
1226
1272
|
candleSubscriptions.clear();
|
|
1227
1273
|
statsSubscriptions.clear();
|
|
1228
1274
|
walletSubscriptions.clear();
|
|
1275
|
+
userBidsSubscriptions.clear();
|
|
1229
1276
|
if (client) {
|
|
1230
1277
|
client.end(true);
|
|
1231
1278
|
client = null;
|
|
@@ -1235,7 +1282,7 @@ function createMqttTransport(endpoint, opts = {}) {
|
|
|
1235
1282
|
}
|
|
1236
1283
|
|
|
1237
1284
|
// src/modules/realtime/index.ts
|
|
1238
|
-
import
|
|
1285
|
+
import EventEmitter4 from "eventemitter3";
|
|
1239
1286
|
|
|
1240
1287
|
// src/modules/realtime/GameChannel.ts
|
|
1241
1288
|
import EventEmitter from "eventemitter3";
|
|
@@ -1247,9 +1294,19 @@ var GameChannel = class extends EventEmitter {
|
|
|
1247
1294
|
}
|
|
1248
1295
|
};
|
|
1249
1296
|
|
|
1250
|
-
// src/modules/realtime/
|
|
1297
|
+
// src/modules/realtime/UserBidsChannel.ts
|
|
1251
1298
|
import EventEmitter2 from "eventemitter3";
|
|
1252
|
-
var
|
|
1299
|
+
var UserBidsChannel = class extends EventEmitter2 {
|
|
1300
|
+
userId;
|
|
1301
|
+
constructor(userId) {
|
|
1302
|
+
super();
|
|
1303
|
+
this.userId = userId;
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
// src/modules/realtime/WalletChannel.ts
|
|
1308
|
+
import EventEmitter3 from "eventemitter3";
|
|
1309
|
+
var WalletChannel = class extends EventEmitter3 {
|
|
1253
1310
|
userId;
|
|
1254
1311
|
constructor(userId) {
|
|
1255
1312
|
super();
|
|
@@ -1257,6 +1314,19 @@ var WalletChannel = class extends EventEmitter2 {
|
|
|
1257
1314
|
}
|
|
1258
1315
|
};
|
|
1259
1316
|
|
|
1317
|
+
// src/modules/realtime/topic.ts
|
|
1318
|
+
var BID_RESULT_SUFFIX = "bid_result";
|
|
1319
|
+
function pairIdFromBidResultTopic(topic) {
|
|
1320
|
+
if (!topic.endsWith(`/${BID_RESULT_SUFFIX}`)) return void 0;
|
|
1321
|
+
const parts = topic.split("/");
|
|
1322
|
+
if (parts.length !== 5 || parts[0] !== "game" || parts[2] !== "user") return void 0;
|
|
1323
|
+
const gameId = parts[1];
|
|
1324
|
+
const sep = gameId.indexOf(":");
|
|
1325
|
+
if (sep < 0) return void 0;
|
|
1326
|
+
const pairId = gameId.slice(sep + 1);
|
|
1327
|
+
return pairId === "" ? void 0 : pairId;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1260
1330
|
// src/modules/realtime/index.ts
|
|
1261
1331
|
var TOPIC_SUFFIX_CANDLE = "candle";
|
|
1262
1332
|
var TOPIC_SUFFIX_BID_RESULT = "bid_result";
|
|
@@ -1279,7 +1349,7 @@ function mapWireCandle(raw) {
|
|
|
1279
1349
|
if (c.coefMults !== void 0) result.coefMults = c.coefMults;
|
|
1280
1350
|
return result;
|
|
1281
1351
|
}
|
|
1282
|
-
function mapWireBidResult(raw) {
|
|
1352
|
+
function mapWireBidResult(raw, topicPairId) {
|
|
1283
1353
|
const p = raw;
|
|
1284
1354
|
switch (p.type) {
|
|
1285
1355
|
case "accepted": {
|
|
@@ -1296,13 +1366,13 @@ function mapWireBidResult(raw) {
|
|
|
1296
1366
|
userId: p.user_id
|
|
1297
1367
|
};
|
|
1298
1368
|
if (typeof p.balance === "string") data.balance = p.balance;
|
|
1369
|
+
if (topicPairId !== void 0) data.pairId = topicPairId;
|
|
1299
1370
|
return { event: "bidWon", data };
|
|
1300
1371
|
}
|
|
1301
1372
|
case "lost": {
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
};
|
|
1373
|
+
const data = { bidId: p.bidId };
|
|
1374
|
+
if (topicPairId !== void 0) data.pairId = topicPairId;
|
|
1375
|
+
return { event: "bidLost", data };
|
|
1306
1376
|
}
|
|
1307
1377
|
default:
|
|
1308
1378
|
console.warn(`Unknown bid_result type "${String(p.type)}"`);
|
|
@@ -1350,7 +1420,7 @@ function mapWireToEvent(topic, payload) {
|
|
|
1350
1420
|
return { event: "candle", data: mapWireCandle(payload) };
|
|
1351
1421
|
}
|
|
1352
1422
|
if (topic.endsWith(`/${TOPIC_SUFFIX_BID_RESULT}`)) {
|
|
1353
|
-
return mapWireBidResult(payload);
|
|
1423
|
+
return mapWireBidResult(payload, pairIdFromBidResultTopic(topic));
|
|
1354
1424
|
}
|
|
1355
1425
|
if (topic.endsWith(`/${TOPIC_SUFFIX_BALANCE_UPDATE}`)) {
|
|
1356
1426
|
return { event: "balanceUpdate", data: mapWireBalanceUpdate(payload) };
|
|
@@ -1366,11 +1436,13 @@ function mapWireToEvent(topic, payload) {
|
|
|
1366
1436
|
function normaliseUserId2(userId) {
|
|
1367
1437
|
return userId && userId !== "" ? userId : null;
|
|
1368
1438
|
}
|
|
1369
|
-
var RealtimeModule = class extends
|
|
1439
|
+
var RealtimeModule = class extends EventEmitter4 {
|
|
1370
1440
|
#transport;
|
|
1371
1441
|
#entries = /* @__PURE__ */ new Map();
|
|
1372
1442
|
#walletEntries = /* @__PURE__ */ new Map();
|
|
1373
1443
|
// keyed by userId
|
|
1444
|
+
#userBidsEntries = /* @__PURE__ */ new Map();
|
|
1445
|
+
// keyed by userId
|
|
1374
1446
|
#agencyId;
|
|
1375
1447
|
constructor(mqttEndpointOrOptions) {
|
|
1376
1448
|
super();
|
|
@@ -1501,6 +1573,64 @@ var RealtimeModule = class extends EventEmitter3 {
|
|
|
1501
1573
|
entry.channel.removeAllListeners();
|
|
1502
1574
|
this.#transport.unsubscribeWallet(cleanUserId);
|
|
1503
1575
|
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Subscribe to the user's bid-result stream across ALL pairs, via the wildcard
|
|
1578
|
+
* topic `game/+/user/{userId}/bid_result`. Returns a `UserBidsChannel` that
|
|
1579
|
+
* emits `bidWon` / `bidLost` for settlements on any pair the user holds a bid
|
|
1580
|
+
* on — independent of which pair is currently displayed (mirrors
|
|
1581
|
+
* `subscribeWallet`). Each emitted event carries the bid's `pairId`, parsed
|
|
1582
|
+
* from the concrete result topic, so consumers can attribute and label it.
|
|
1583
|
+
*
|
|
1584
|
+
* Reference-counted by `userId`. The active pair's bid_result is ALSO delivered
|
|
1585
|
+
* on the per-pair `subscribe` channel — that is intentional: the per-pair
|
|
1586
|
+
* channel drives state + sound, this cross-pair channel drives notifications.
|
|
1587
|
+
*/
|
|
1588
|
+
subscribeUserBids(userId) {
|
|
1589
|
+
const cleanUserId = normaliseUserId2(userId);
|
|
1590
|
+
if (!cleanUserId) {
|
|
1591
|
+
throw new TaphubError("userId is required to subscribe to user bids", {
|
|
1592
|
+
code: "UserIdRequired"
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
if (/[/+#]/.test(cleanUserId)) {
|
|
1596
|
+
throw new TaphubError("userId must not contain MQTT topic separators (/ + #)", {
|
|
1597
|
+
code: "InvalidUserId"
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
const existing = this.#userBidsEntries.get(cleanUserId);
|
|
1601
|
+
if (existing) {
|
|
1602
|
+
existing.refcount += 1;
|
|
1603
|
+
return existing.channel;
|
|
1604
|
+
}
|
|
1605
|
+
const channel = new UserBidsChannel(cleanUserId);
|
|
1606
|
+
const onMessage = (topic, payload) => {
|
|
1607
|
+
const mapped = mapWireBidResult(payload, pairIdFromBidResultTopic(topic));
|
|
1608
|
+
if (!mapped) return;
|
|
1609
|
+
if (mapped.event === "bidWon") channel.emit("bidWon", mapped.data);
|
|
1610
|
+
else if (mapped.event === "bidLost") channel.emit("bidLost", mapped.data);
|
|
1611
|
+
};
|
|
1612
|
+
const onError = (err) => {
|
|
1613
|
+
channel.emit("error", err);
|
|
1614
|
+
};
|
|
1615
|
+
this.#userBidsEntries.set(cleanUserId, { userId: cleanUserId, channel, refcount: 1 });
|
|
1616
|
+
this.#transport.subscribeUserBids(cleanUserId, onMessage, onError);
|
|
1617
|
+
return channel;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Decrement the cross-pair user-bids subscription refcount for `userId`. Tears
|
|
1621
|
+
* down the wildcard topic and removes the channel only at zero. No-op if absent.
|
|
1622
|
+
*/
|
|
1623
|
+
unsubscribeUserBids(userId) {
|
|
1624
|
+
const cleanUserId = normaliseUserId2(userId);
|
|
1625
|
+
if (!cleanUserId) return;
|
|
1626
|
+
const entry = this.#userBidsEntries.get(cleanUserId);
|
|
1627
|
+
if (!entry) return;
|
|
1628
|
+
entry.refcount -= 1;
|
|
1629
|
+
if (entry.refcount > 0) return;
|
|
1630
|
+
this.#userBidsEntries.delete(cleanUserId);
|
|
1631
|
+
entry.channel.removeAllListeners();
|
|
1632
|
+
this.#transport.unsubscribeUserBids(cleanUserId);
|
|
1633
|
+
}
|
|
1504
1634
|
/**
|
|
1505
1635
|
* Subscribe to public market candle data for a pair, keyed by pairId
|
|
1506
1636
|
* (the #2 game_pairs.id, e.g. "grid-ETH-USD"). Independent of game/agency —
|
|
@@ -1552,6 +1682,10 @@ var RealtimeModule = class extends EventEmitter3 {
|
|
|
1552
1682
|
entry.channel.removeAllListeners();
|
|
1553
1683
|
}
|
|
1554
1684
|
this.#walletEntries.clear();
|
|
1685
|
+
for (const entry of this.#userBidsEntries.values()) {
|
|
1686
|
+
entry.channel.removeAllListeners();
|
|
1687
|
+
}
|
|
1688
|
+
this.#userBidsEntries.clear();
|
|
1555
1689
|
this.#transport.close();
|
|
1556
1690
|
}
|
|
1557
1691
|
};
|
|
@@ -1783,20 +1917,9 @@ function classifyNetworkLevel(rtt) {
|
|
|
1783
1917
|
// src/network/RttSmoother.ts
|
|
1784
1918
|
var EMA_ALPHA = 0.25;
|
|
1785
1919
|
var OUTLIER_MULTIPLIER = 3;
|
|
1786
|
-
var DEBOUNCE_DEGRADE = 2;
|
|
1787
|
-
var DEBOUNCE_IMPROVE = 5;
|
|
1788
|
-
var LEVEL_RANK = {
|
|
1789
|
-
good: 0,
|
|
1790
|
-
fair: 1,
|
|
1791
|
-
poor: 2,
|
|
1792
|
-
offline: 3
|
|
1793
|
-
};
|
|
1794
1920
|
var RttSmoother = class {
|
|
1795
1921
|
ema = 0;
|
|
1796
1922
|
level = "good";
|
|
1797
|
-
committed = "good";
|
|
1798
|
-
candidate = "good";
|
|
1799
|
-
candidateCount = 0;
|
|
1800
1923
|
overrideOffline = false;
|
|
1801
1924
|
consecutiveOutliers = 0;
|
|
1802
1925
|
add(rtt) {
|
|
@@ -1808,23 +1931,7 @@ var RttSmoother = class {
|
|
|
1808
1931
|
}
|
|
1809
1932
|
const coldStart = this.ema === 0;
|
|
1810
1933
|
this.ema = coldStart ? rtt : EMA_ALPHA * rtt + (1 - EMA_ALPHA) * this.ema;
|
|
1811
|
-
|
|
1812
|
-
if (coldStart) {
|
|
1813
|
-
this.committed = next;
|
|
1814
|
-
this.candidate = next;
|
|
1815
|
-
this.candidateCount = 1;
|
|
1816
|
-
} else {
|
|
1817
|
-
if (next === this.candidate) {
|
|
1818
|
-
this.candidateCount += 1;
|
|
1819
|
-
} else {
|
|
1820
|
-
this.candidate = next;
|
|
1821
|
-
this.candidateCount = 1;
|
|
1822
|
-
}
|
|
1823
|
-
if (this.candidateCount >= this.requiredSamplesFor(next)) {
|
|
1824
|
-
this.committed = next;
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
this.level = this.overrideOffline ? "offline" : this.committed;
|
|
1934
|
+
this.level = this.overrideOffline ? "offline" : classifyNetworkLevel(this.ema);
|
|
1828
1935
|
}
|
|
1829
1936
|
forceOffline() {
|
|
1830
1937
|
this.overrideOffline = true;
|
|
@@ -1832,20 +1939,14 @@ var RttSmoother = class {
|
|
|
1832
1939
|
}
|
|
1833
1940
|
releaseOffline() {
|
|
1834
1941
|
this.overrideOffline = false;
|
|
1835
|
-
this.level = this.
|
|
1942
|
+
this.level = classifyNetworkLevel(this.ema);
|
|
1836
1943
|
}
|
|
1837
1944
|
reset() {
|
|
1838
1945
|
this.ema = 0;
|
|
1839
|
-
this.committed = "good";
|
|
1840
|
-
this.candidate = "good";
|
|
1841
|
-
this.candidateCount = 0;
|
|
1842
1946
|
this.consecutiveOutliers = 0;
|
|
1843
1947
|
this.overrideOffline = false;
|
|
1844
1948
|
this.level = "good";
|
|
1845
1949
|
}
|
|
1846
|
-
requiredSamplesFor(next) {
|
|
1847
|
-
return LEVEL_RANK[next] > LEVEL_RANK[this.committed] ? DEBOUNCE_DEGRADE : DEBOUNCE_IMPROVE;
|
|
1848
|
-
}
|
|
1849
1950
|
};
|
|
1850
1951
|
|
|
1851
1952
|
// src/network/NetworkQualityMonitor.ts
|
|
@@ -1872,7 +1973,6 @@ var NetworkQualityMonitor = class {
|
|
|
1872
1973
|
smoother = new RttSmoother();
|
|
1873
1974
|
mqttConnected = false;
|
|
1874
1975
|
mqttDisconnectedAt = null;
|
|
1875
|
-
committedNetwork = "good";
|
|
1876
1976
|
committedBackend = "ok";
|
|
1877
1977
|
// The last snapshot we emitted, used to decide whether the next recompute is
|
|
1878
1978
|
// worth emitting. Seeded with the initial default so the first real change
|
|
@@ -1950,7 +2050,6 @@ var NetworkQualityMonitor = class {
|
|
|
1950
2050
|
this.smoother.reset();
|
|
1951
2051
|
this.mqttConnected = false;
|
|
1952
2052
|
this.mqttDisconnectedAt = null;
|
|
1953
|
-
this.committedNetwork = "good";
|
|
1954
2053
|
this.committedBackend = "ok";
|
|
1955
2054
|
this.emitIfChanged(this.snapshot());
|
|
1956
2055
|
}
|
|
@@ -2014,26 +2113,12 @@ var NetworkQualityMonitor = class {
|
|
|
2014
2113
|
const useConnection = realSamples.length < COLD_START_REAL_SAMPLE_THRESHOLD;
|
|
2015
2114
|
const effectiveSamples = useConnection ? allSamples : realSamples;
|
|
2016
2115
|
const httpSamples = effectiveSamples.filter((s) => isHttpSource(s.source));
|
|
2017
|
-
|
|
2018
|
-
const metrics = this.deriveMetricsFrom(effectiveSamples);
|
|
2019
|
-
const candidate = classifyNetworkLevel(metrics.emaForLevel);
|
|
2020
|
-
let nextNetwork = this.smoother.level;
|
|
2021
|
-
if (effectiveSamples.length === 0) {
|
|
2022
|
-
nextNetwork = this.committedNetwork;
|
|
2023
|
-
} else if (this.smoother.ema > 0) {
|
|
2024
|
-
nextNetwork = this.smoother.level;
|
|
2025
|
-
} else {
|
|
2026
|
-
nextNetwork = candidate;
|
|
2027
|
-
}
|
|
2116
|
+
this.committedBackend = classifyBackendHealth(httpSamples);
|
|
2028
2117
|
if (this.isHardOffline()) {
|
|
2029
2118
|
this.smoother.forceOffline();
|
|
2030
|
-
nextNetwork = "offline";
|
|
2031
2119
|
} else if (this.smoother.level === "offline") {
|
|
2032
2120
|
this.smoother.releaseOffline();
|
|
2033
|
-
nextNetwork = this.smoother.level;
|
|
2034
2121
|
}
|
|
2035
|
-
this.committedNetwork = nextNetwork;
|
|
2036
|
-
this.committedBackend = backend;
|
|
2037
2122
|
this.emitIfChanged(this.snapshot());
|
|
2038
2123
|
}
|
|
2039
2124
|
// Emits network:change when any *displayed* field of the snapshot moves. This
|
|
@@ -2094,10 +2179,12 @@ var NetworkQualityMonitor = class {
|
|
|
2094
2179
|
snapshot() {
|
|
2095
2180
|
const samples = this.allHttpAndMqttSamples();
|
|
2096
2181
|
const metrics = this.deriveMetricsFrom(samples);
|
|
2182
|
+
const rtt = Math.round(metrics.emaForLevel);
|
|
2183
|
+
const network = this.smoother.level === "offline" ? "offline" : classifyNetworkLevel(rtt);
|
|
2097
2184
|
return {
|
|
2098
|
-
network
|
|
2185
|
+
network,
|
|
2099
2186
|
backend: this.committedBackend,
|
|
2100
|
-
rtt
|
|
2187
|
+
rtt,
|
|
2101
2188
|
jitter: Math.round(metrics.jitter),
|
|
2102
2189
|
lossRate: metrics.lossRate,
|
|
2103
2190
|
mqttConnected: this.mqttConnected,
|
|
@@ -2917,5 +3004,6 @@ export {
|
|
|
2917
3004
|
isTerminal,
|
|
2918
3005
|
isWin,
|
|
2919
3006
|
normalCDF,
|
|
2920
|
-
normalPDF
|
|
3007
|
+
normalPDF,
|
|
3008
|
+
pairIdFromBidResultTopic
|
|
2921
3009
|
};
|