Skip to content

Commit 6756b95

Browse files
markets/: stop the in-place updater clobbering its own hooks
The fix that kept Cash out focusable destroyed the class hooks it needed to work. Assigning d.className = 'pnl up' dropped the v-pnl marker, so the NEXT tick threw a TypeError, aborted renderDetail mid-loop, and left every P&L figure and the cost/value footer frozen while the first leg kept moving -- the UI read -158.10 against a server value of -166.31, and the stale shape survived navigation so only a full page load healed it. Two bets ten seconds apart were enough to trigger it, and the raw TypeError was painted into the ticket beside the receipt. Worse, the early return also skipped quote(), so an armed confirmation slip never re-priced: it advertised 'Returns 95.41 at 4.77' with a live Confirm and a running clock while the fill came back 27.91. maxCost does not guard that -- the cost is what you typed; it is the return that collapses. Classes are now toggled rather than assigned, the in-place path is null-guarded and falls back to a full rebuild instead of throwing, and the whole render pass continues so the oracle and dispute cards re-evaluate and the ticket re-prices. Verified: two bets in a row leave the hooks intact with no errors and the P&L matching the server to the cent, and a third party's trade re-prices an armed slip from 72.86 to 33.08 instead of holding a stale promise. Also: the stale position shape is reset on route change, a websocket render failure can no longer reach the ticket as a raw string, and the previous market's wording is cleared from the hidden slip. 75 plugin tests green.
1 parent 116cf78 commit 6756b95

1 file changed

Lines changed: 35 additions & 15 deletions

File tree

markets/ui.js

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,24 +1044,39 @@ export function renderUi(prefix) {
10441044
// time anyone else traded.
10451045
const legs = pos.shares.map((s, i) => (s > 0 ? i : -1)).filter((i) => i >= 0);
10461046
const shape = m.id + ':' + legs.join(',');
1047+
// Toggle the up/down class, never ASSIGN className — assigning it
1048+
// dropped the v-pnl/v-total hooks this very function needs, so the
1049+
// next tick threw, froze every P&L figure, and (via an early
1050+
// return) skipped quote(), letting an armed slip advertise a
1051+
// payout 3.4x what it filled at.
1052+
const mark = (el, val) => {
1053+
el.classList.toggle('up', val >= 0);
1054+
el.classList.toggle('down', val < 0);
1055+
};
1056+
let inPlace = false;
10471057
if (pd.dataset.shape === shape) {
1048-
legs.forEach((i) => {
1049-
const row = pd.querySelector('tr[data-i="' + i + '"]');
1050-
if (!row) return;
1051-
row.querySelector('.v-now').textContent = cr(pos.value[i]);
1052-
const d = row.querySelector('.v-pnl');
1053-
d.textContent = (pos.value[i] >= pos.cost[i] ? '+' : '') + cr(pos.value[i] - pos.cost[i]);
1054-
d.className = 'pnl ' + (pos.value[i] >= pos.cost[i] ? 'up' : 'down');
1055-
});
1056-
const tot = pd.querySelector('.v-total');
1057-
if (tot) {
1058+
try {
1059+
legs.forEach((i) => {
1060+
const row = pd.querySelector('tr[data-i="' + i + '"]');
1061+
const now = row && row.querySelector('.v-now');
1062+
const d = row && row.querySelector('.v-pnl');
1063+
if (!now || !d) throw new Error('position hooks missing');
1064+
now.textContent = cr(pos.value[i]);
1065+
d.textContent = (pos.value[i] >= pos.cost[i] ? '+' : '') + cr(pos.value[i] - pos.cost[i]);
1066+
mark(d, pos.value[i] - pos.cost[i]);
1067+
});
1068+
const tot = pd.querySelector('.v-total');
1069+
if (!tot) throw new Error('position total missing');
10581070
tot.textContent = (pos.unrealizedPnl >= 0 ? '+' : '') + cr(pos.unrealizedPnl);
1059-
tot.className = 'pnl ' + (pos.unrealizedPnl >= 0 ? 'up' : 'down');
1071+
mark(tot, pos.unrealizedPnl);
1072+
const cost = pd.querySelector('.v-cost');
1073+
if (cost) cost.textContent = 'cost ' + cr(pos.totalCost) + ' · value ' + cr(pos.totalValue);
1074+
inPlace = true;
1075+
} catch {
1076+
inPlace = false; // fall through to a full rebuild rather than throw
10601077
}
1061-
const cost = pd.querySelector('.v-cost');
1062-
if (cost) cost.textContent = 'cost ' + cr(pos.totalCost) + ' · value ' + cr(pos.totalValue);
1063-
return;
10641078
}
1079+
if (!inPlace) {
10651080
pd.dataset.shape = shape;
10661081
pd.innerHTML = '<h2>Your position</h2>'
10671082
+ '<table><thead><tr><th>Bet</th><th class="num">Sell now for</th>'
@@ -1083,6 +1098,7 @@ export function renderUi(prefix) {
10831098
pd.querySelectorAll('.sell-one').forEach((b) => {
10841099
b.onclick = () => cashOut(m.id, Number(b.dataset.i));
10851100
});
1101+
}
10861102
} else { pd.classList.add('hidden'); pd.dataset.shape = ''; }
10871103
10881104
$('oracle-card').classList.toggle('hidden', !(isYou && m.canResolve));
@@ -1196,6 +1212,7 @@ export function renderUi(prefix) {
11961212
$('t-buy').classList.remove('hidden');
11971213
if (slipTimer) { clearInterval(slipTimer); slipTimer = null; }
11981214
$('t-countdown').innerHTML = ''; // so the next slip rebuilds it
1215+
$('t-slip-copy').innerHTML = ''; // don't keep the last market's wording
11991216
// Never strand focus on a hidden control — Tab from there restarts
12001217
// at the top of the document, mid-purchase.
12011218
if (hadFocus) {
@@ -1307,6 +1324,7 @@ export function renderUi(prefix) {
13071324
$('list-view').classList.remove('hidden');
13081325
document.title = 'Markets — prediction markets on your pod';
13091326
current = null; cursor = null; paged = false;
1327+
$('d-position').dataset.shape = '';
13101328
cancelSlip();
13111329
$('t-msg').textContent = ''; $('t-msg').className = 'msg';
13121330
$('t-fill').textContent = '';
@@ -1467,7 +1485,9 @@ export function renderUi(prefix) {
14671485
ws.onmessage = (ev) => {
14681486
let msg; try { msg = JSON.parse(ev.data); } catch { return; }
14691487
if (current) {
1470-
if (msg.market && msg.market.id === current.id) schedule(() => renderDetail(current.id, true));
1488+
if (msg.market && msg.market.id === current.id) {
1489+
schedule(() => renderDetail(current.id, true).catch((e) => console.error('render', e)));
1490+
}
14711491
} else if (!paged) schedule(() => renderList());
14721492
if (msg.type === 'settle' && me) refreshMe();
14731493
};

0 commit comments

Comments
 (0)