@taphubhq/sdk-core 0.23.3 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +151 -12
- package/dist/index.d.mts +78 -1
- package/dist/index.d.ts +78 -1
- package/dist/index.js +149 -11
- 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,9 @@ 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 {
|
|
1318
1366
|
userId;
|
|
1319
1367
|
constructor(userId) {
|
|
1320
1368
|
super();
|
|
@@ -1322,6 +1370,29 @@ var WalletChannel = class extends import_eventemitter32.default {
|
|
|
1322
1370
|
}
|
|
1323
1371
|
};
|
|
1324
1372
|
|
|
1373
|
+
// src/modules/realtime/WalletChannel.ts
|
|
1374
|
+
var import_eventemitter33 = __toESM(require("eventemitter3"));
|
|
1375
|
+
var WalletChannel = class extends import_eventemitter33.default {
|
|
1376
|
+
userId;
|
|
1377
|
+
constructor(userId) {
|
|
1378
|
+
super();
|
|
1379
|
+
this.userId = userId;
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
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)}"`);
|
|
@@ -1402,6 +1473,9 @@ function mapWireConfig(raw) {
|
|
|
1402
1473
|
function mapWireIdealConfig(raw) {
|
|
1403
1474
|
const p = raw;
|
|
1404
1475
|
return {
|
|
1476
|
+
// bid-260612 guard ①: carry the producer's pairId so consumers can reject cross-pair
|
|
1477
|
+
// suggestions. Undefined when talking to a producer that predates the field.
|
|
1478
|
+
pairId: p.pairId,
|
|
1405
1479
|
cellSizeValue: p.cellSizeValue,
|
|
1406
1480
|
currentPrice: p.currentPrice,
|
|
1407
1481
|
reason: p.reason ?? "volatility_shift"
|
|
@@ -1412,7 +1486,7 @@ function mapWireToEvent(topic, payload) {
|
|
|
1412
1486
|
return { event: "candle", data: mapWireCandle(payload) };
|
|
1413
1487
|
}
|
|
1414
1488
|
if (topic.endsWith(`/${TOPIC_SUFFIX_BID_RESULT}`)) {
|
|
1415
|
-
return mapWireBidResult(payload);
|
|
1489
|
+
return mapWireBidResult(payload, pairIdFromBidResultTopic(topic));
|
|
1416
1490
|
}
|
|
1417
1491
|
if (topic.endsWith(`/${TOPIC_SUFFIX_BALANCE_UPDATE}`)) {
|
|
1418
1492
|
return { event: "balanceUpdate", data: mapWireBalanceUpdate(payload) };
|
|
@@ -1428,11 +1502,13 @@ function mapWireToEvent(topic, payload) {
|
|
|
1428
1502
|
function normaliseUserId2(userId) {
|
|
1429
1503
|
return userId && userId !== "" ? userId : null;
|
|
1430
1504
|
}
|
|
1431
|
-
var RealtimeModule = class extends
|
|
1505
|
+
var RealtimeModule = class extends import_eventemitter34.default {
|
|
1432
1506
|
#transport;
|
|
1433
1507
|
#entries = /* @__PURE__ */ new Map();
|
|
1434
1508
|
#walletEntries = /* @__PURE__ */ new Map();
|
|
1435
1509
|
// keyed by userId
|
|
1510
|
+
#userBidsEntries = /* @__PURE__ */ new Map();
|
|
1511
|
+
// keyed by userId
|
|
1436
1512
|
#agencyId;
|
|
1437
1513
|
constructor(mqttEndpointOrOptions) {
|
|
1438
1514
|
super();
|
|
@@ -1563,6 +1639,64 @@ var RealtimeModule = class extends import_eventemitter33.default {
|
|
|
1563
1639
|
entry.channel.removeAllListeners();
|
|
1564
1640
|
this.#transport.unsubscribeWallet(cleanUserId);
|
|
1565
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
|
+
}
|
|
1566
1700
|
/**
|
|
1567
1701
|
* Subscribe to public market candle data for a pair, keyed by pairId
|
|
1568
1702
|
* (the #2 game_pairs.id, e.g. "grid-ETH-USD"). Independent of game/agency —
|
|
@@ -1614,6 +1748,10 @@ var RealtimeModule = class extends import_eventemitter33.default {
|
|
|
1614
1748
|
entry.channel.removeAllListeners();
|
|
1615
1749
|
}
|
|
1616
1750
|
this.#walletEntries.clear();
|
|
1751
|
+
for (const entry of this.#userBidsEntries.values()) {
|
|
1752
|
+
entry.channel.removeAllListeners();
|
|
1753
|
+
}
|
|
1754
|
+
this.#userBidsEntries.clear();
|
|
1617
1755
|
this.#transport.close();
|
|
1618
1756
|
}
|
|
1619
1757
|
};
|
|
@@ -2980,5 +3118,6 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
|
|
|
2980
3118
|
isTerminal,
|
|
2981
3119
|
isWin,
|
|
2982
3120
|
normalCDF,
|
|
2983
|
-
normalPDF
|
|
3121
|
+
normalPDF,
|
|
3122
|
+
pairIdFromBidResultTopic
|
|
2984
3123
|
});
|
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;
|
|
@@ -838,6 +859,12 @@ interface MqttConfigEvent {
|
|
|
838
859
|
acceptableBids: number[];
|
|
839
860
|
}
|
|
840
861
|
interface MqttIdealConfigEvent {
|
|
862
|
+
/**
|
|
863
|
+
* game_pairs.id (e.g. "grid-ETH-USD") the suggestion was computed for. Optional for
|
|
864
|
+
* backward compat with producers predating bid-260612; when present, a consumer drops
|
|
865
|
+
* suggestions whose pairId does not match the pair it is currently viewing (guard ①).
|
|
866
|
+
*/
|
|
867
|
+
pairId?: string;
|
|
841
868
|
cellSizeValue: number;
|
|
842
869
|
currentPrice: number;
|
|
843
870
|
reason: string;
|
|
@@ -872,6 +899,24 @@ declare class GameChannel extends EventEmitter<GameChannelEvents> {
|
|
|
872
899
|
constructor(gameId: string);
|
|
873
900
|
}
|
|
874
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
|
+
|
|
875
920
|
interface WalletChannelEvents {
|
|
876
921
|
walletBalanceUpdate: [MqttWalletBalanceEvent];
|
|
877
922
|
error: [Error];
|
|
@@ -940,6 +985,24 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
|
|
|
940
985
|
* No-op if there is no active wallet subscription for `userId`.
|
|
941
986
|
*/
|
|
942
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;
|
|
943
1006
|
/**
|
|
944
1007
|
* Subscribe to public market candle data for a pair, keyed by pairId
|
|
945
1008
|
* (the #2 game_pairs.id, e.g. "grid-ETH-USD"). Independent of game/agency —
|
|
@@ -1169,6 +1232,20 @@ declare class TaphubSlippageError extends TaphubValidationError {
|
|
|
1169
1232
|
});
|
|
1170
1233
|
}
|
|
1171
1234
|
|
|
1235
|
+
/**
|
|
1236
|
+
* Extract the slash-free pairId from a bid-result topic.
|
|
1237
|
+
*
|
|
1238
|
+
* Accepts the concrete topic delivered by the broker, e.g.
|
|
1239
|
+
* `game/acme-corp:grid-ETH-USD/user/{uid}/bid_result` → `grid-ETH-USD`.
|
|
1240
|
+
*
|
|
1241
|
+
* Returns `undefined` when the topic does not match the expected shape (so the
|
|
1242
|
+
* caller can fall back gracefully rather than throw). The gameId segment is the
|
|
1243
|
+
* composite `{agencyId}:{pairId}`; the pairId is everything after the FIRST `:`
|
|
1244
|
+
* (pairId itself is slash-free and colon-free by the bid-260602-v2 id rules, but
|
|
1245
|
+
* we split on the first `:` to be safe if an agencyId ever contained one).
|
|
1246
|
+
*/
|
|
1247
|
+
declare function pairIdFromBidResultTopic(topic: string): string | undefined;
|
|
1248
|
+
|
|
1172
1249
|
declare function errorFunction(x: number): number;
|
|
1173
1250
|
declare function normalCDF(x: number): number;
|
|
1174
1251
|
declare function normalPDF(x: number): number;
|
|
@@ -1176,4 +1253,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
|
|
|
1176
1253
|
declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
|
|
1177
1254
|
declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
|
|
1178
1255
|
|
|
1179
|
-
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 };
|
|
1256
|
+
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;
|
|
@@ -838,6 +859,12 @@ interface MqttConfigEvent {
|
|
|
838
859
|
acceptableBids: number[];
|
|
839
860
|
}
|
|
840
861
|
interface MqttIdealConfigEvent {
|
|
862
|
+
/**
|
|
863
|
+
* game_pairs.id (e.g. "grid-ETH-USD") the suggestion was computed for. Optional for
|
|
864
|
+
* backward compat with producers predating bid-260612; when present, a consumer drops
|
|
865
|
+
* suggestions whose pairId does not match the pair it is currently viewing (guard ①).
|
|
866
|
+
*/
|
|
867
|
+
pairId?: string;
|
|
841
868
|
cellSizeValue: number;
|
|
842
869
|
currentPrice: number;
|
|
843
870
|
reason: string;
|
|
@@ -872,6 +899,24 @@ declare class GameChannel extends EventEmitter<GameChannelEvents> {
|
|
|
872
899
|
constructor(gameId: string);
|
|
873
900
|
}
|
|
874
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
|
+
|
|
875
920
|
interface WalletChannelEvents {
|
|
876
921
|
walletBalanceUpdate: [MqttWalletBalanceEvent];
|
|
877
922
|
error: [Error];
|
|
@@ -940,6 +985,24 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
|
|
|
940
985
|
* No-op if there is no active wallet subscription for `userId`.
|
|
941
986
|
*/
|
|
942
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;
|
|
943
1006
|
/**
|
|
944
1007
|
* Subscribe to public market candle data for a pair, keyed by pairId
|
|
945
1008
|
* (the #2 game_pairs.id, e.g. "grid-ETH-USD"). Independent of game/agency —
|
|
@@ -1169,6 +1232,20 @@ declare class TaphubSlippageError extends TaphubValidationError {
|
|
|
1169
1232
|
});
|
|
1170
1233
|
}
|
|
1171
1234
|
|
|
1235
|
+
/**
|
|
1236
|
+
* Extract the slash-free pairId from a bid-result topic.
|
|
1237
|
+
*
|
|
1238
|
+
* Accepts the concrete topic delivered by the broker, e.g.
|
|
1239
|
+
* `game/acme-corp:grid-ETH-USD/user/{uid}/bid_result` → `grid-ETH-USD`.
|
|
1240
|
+
*
|
|
1241
|
+
* Returns `undefined` when the topic does not match the expected shape (so the
|
|
1242
|
+
* caller can fall back gracefully rather than throw). The gameId segment is the
|
|
1243
|
+
* composite `{agencyId}:{pairId}`; the pairId is everything after the FIRST `:`
|
|
1244
|
+
* (pairId itself is slash-free and colon-free by the bid-260602-v2 id rules, but
|
|
1245
|
+
* we split on the first `:` to be safe if an agencyId ever contained one).
|
|
1246
|
+
*/
|
|
1247
|
+
declare function pairIdFromBidResultTopic(topic: string): string | undefined;
|
|
1248
|
+
|
|
1172
1249
|
declare function errorFunction(x: number): number;
|
|
1173
1250
|
declare function normalCDF(x: number): number;
|
|
1174
1251
|
declare function normalPDF(x: number): number;
|
|
@@ -1176,4 +1253,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
|
|
|
1176
1253
|
declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
|
|
1177
1254
|
declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
|
|
1178
1255
|
|
|
1179
|
-
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 };
|
|
1256
|
+
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,9 @@ 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 {
|
|
1253
1300
|
userId;
|
|
1254
1301
|
constructor(userId) {
|
|
1255
1302
|
super();
|
|
@@ -1257,6 +1304,29 @@ var WalletChannel = class extends EventEmitter2 {
|
|
|
1257
1304
|
}
|
|
1258
1305
|
};
|
|
1259
1306
|
|
|
1307
|
+
// src/modules/realtime/WalletChannel.ts
|
|
1308
|
+
import EventEmitter3 from "eventemitter3";
|
|
1309
|
+
var WalletChannel = class extends EventEmitter3 {
|
|
1310
|
+
userId;
|
|
1311
|
+
constructor(userId) {
|
|
1312
|
+
super();
|
|
1313
|
+
this.userId = userId;
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
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)}"`);
|
|
@@ -1337,6 +1407,9 @@ function mapWireConfig(raw) {
|
|
|
1337
1407
|
function mapWireIdealConfig(raw) {
|
|
1338
1408
|
const p = raw;
|
|
1339
1409
|
return {
|
|
1410
|
+
// bid-260612 guard ①: carry the producer's pairId so consumers can reject cross-pair
|
|
1411
|
+
// suggestions. Undefined when talking to a producer that predates the field.
|
|
1412
|
+
pairId: p.pairId,
|
|
1340
1413
|
cellSizeValue: p.cellSizeValue,
|
|
1341
1414
|
currentPrice: p.currentPrice,
|
|
1342
1415
|
reason: p.reason ?? "volatility_shift"
|
|
@@ -1347,7 +1420,7 @@ function mapWireToEvent(topic, payload) {
|
|
|
1347
1420
|
return { event: "candle", data: mapWireCandle(payload) };
|
|
1348
1421
|
}
|
|
1349
1422
|
if (topic.endsWith(`/${TOPIC_SUFFIX_BID_RESULT}`)) {
|
|
1350
|
-
return mapWireBidResult(payload);
|
|
1423
|
+
return mapWireBidResult(payload, pairIdFromBidResultTopic(topic));
|
|
1351
1424
|
}
|
|
1352
1425
|
if (topic.endsWith(`/${TOPIC_SUFFIX_BALANCE_UPDATE}`)) {
|
|
1353
1426
|
return { event: "balanceUpdate", data: mapWireBalanceUpdate(payload) };
|
|
@@ -1363,11 +1436,13 @@ function mapWireToEvent(topic, payload) {
|
|
|
1363
1436
|
function normaliseUserId2(userId) {
|
|
1364
1437
|
return userId && userId !== "" ? userId : null;
|
|
1365
1438
|
}
|
|
1366
|
-
var RealtimeModule = class extends
|
|
1439
|
+
var RealtimeModule = class extends EventEmitter4 {
|
|
1367
1440
|
#transport;
|
|
1368
1441
|
#entries = /* @__PURE__ */ new Map();
|
|
1369
1442
|
#walletEntries = /* @__PURE__ */ new Map();
|
|
1370
1443
|
// keyed by userId
|
|
1444
|
+
#userBidsEntries = /* @__PURE__ */ new Map();
|
|
1445
|
+
// keyed by userId
|
|
1371
1446
|
#agencyId;
|
|
1372
1447
|
constructor(mqttEndpointOrOptions) {
|
|
1373
1448
|
super();
|
|
@@ -1498,6 +1573,64 @@ var RealtimeModule = class extends EventEmitter3 {
|
|
|
1498
1573
|
entry.channel.removeAllListeners();
|
|
1499
1574
|
this.#transport.unsubscribeWallet(cleanUserId);
|
|
1500
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
|
+
}
|
|
1501
1634
|
/**
|
|
1502
1635
|
* Subscribe to public market candle data for a pair, keyed by pairId
|
|
1503
1636
|
* (the #2 game_pairs.id, e.g. "grid-ETH-USD"). Independent of game/agency —
|
|
@@ -1549,6 +1682,10 @@ var RealtimeModule = class extends EventEmitter3 {
|
|
|
1549
1682
|
entry.channel.removeAllListeners();
|
|
1550
1683
|
}
|
|
1551
1684
|
this.#walletEntries.clear();
|
|
1685
|
+
for (const entry of this.#userBidsEntries.values()) {
|
|
1686
|
+
entry.channel.removeAllListeners();
|
|
1687
|
+
}
|
|
1688
|
+
this.#userBidsEntries.clear();
|
|
1552
1689
|
this.#transport.close();
|
|
1553
1690
|
}
|
|
1554
1691
|
};
|
|
@@ -2914,5 +3051,6 @@ export {
|
|
|
2914
3051
|
isTerminal,
|
|
2915
3052
|
isWin,
|
|
2916
3053
|
normalCDF,
|
|
2917
|
-
normalPDF
|
|
3054
|
+
normalPDF,
|
|
3055
|
+
pairIdFromBidResultTopic
|
|
2918
3056
|
};
|