Wire new matrix placements in ~60s, not once a day

The live tail wired a new member into its parent's matrix slots with a
guess-based fill and silently gave up on any fetchMember hiccup, deferring
to the once-daily full snapshot — so a placement could take up to 24h to
appear in the tree (observed: #119 qualified #38 on-chain but the dashboard
card still showed 1/2 with an open slot).

Now placement reads the parent's two slots straight from the contract
(getMatrixChildren) — authoritative and order-independent — and any member
that can't wire on its first pass is queued and retried every tick via
drainPendingWire(). Trees reflect real placements within one ~60s poll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-17 15:17:39 -05:00
parent 436306f4d0
commit 1695f712c6
+40 -5
View File
@@ -220,6 +220,39 @@ function genBetween(fromId, toId) {
return null;
}
// Pull a parent's two matrix slots straight from contract storage — authoritative
// and order-independent, so a new placement shows in the tree within one poll (~60s)
// instead of waiting for the once-a-day full snapshot. Returns true on success.
async function refreshChildren(pid) {
if (!pid || !state.members[pid]) return false;
try {
const r = await ethCall(SEL.getMatrixChildren + encU(pid));
state.members[pid].l = wInt(r, 0) || 0;
state.members[pid].r = wInt(r, 1) || 0;
return true;
} catch (e) { return false; }
}
// Queue a just-registered member whose placement couldn't be wired this pass
// (e.g. a transient fetchMember failure) so the next tick retries it — never
// silently deferring to the daily snapshot again.
function enqueueWire(id) {
if (!state.pendingWire) state.pendingWire = [];
if (!state.pendingWire.includes(id)) state.pendingWire.push(id);
}
// Retry all queued placements; drop each one that successfully wires.
async function drainPendingWire() {
if (!state.pendingWire || !state.pendingWire.length) return;
const still = [];
for (const id of state.pendingWire) {
try {
const m = await fetchMember(id);
if (m) { Object.assign(state.members[id], { uplineId: m.uplineId, level: m.level }); await refreshChildren(m.uplineId); }
else still.push(id);
} catch (e) { still.push(id); }
}
state.pendingWire = still;
}
async function processRange(fromBlock, toBlock) {
const logs = await rpc('eth_getLogs', [{
address: CONTRACT, fromBlock: '0x' + fromBlock.toString(16), toBlock: '0x' + toBlock.toString(16),
@@ -287,11 +320,12 @@ async function processRange(fromBlock, toBlock) {
const m = await fetchMember(id);
if (m) {
Object.assign(state.members[id], { uplineId: m.uplineId, level: m.level });
// wire the new member into the parent's matrix slots so trees update live
const par = state.members[m.uplineId];
if (par) { if (!par.l) par.l = id; else if (!par.r && par.l !== id) par.r = id; }
}
} catch (e) { /* picked up by next snapshot */ }
// wire the new member in by reading the parent's slots straight from the
// contract — authoritative and order-independent (fixes both l and r even
// if an earlier sibling's wiring was missed)
await refreshChildren(m.uplineId);
} else { enqueueWire(id); }
} catch (e) { enqueueWire(id); } // retry next tick, don't wait for the daily snapshot
}
state.payouts.sort((a, b) => (a.ts || 0) - (b.ts || 0));
if (state.payouts.length > KEEP_PAYOUTS) state.payouts = state.payouts.slice(-KEEP_PAYOUTS);
@@ -320,6 +354,7 @@ async function tick() {
from = to + 1;
if (from <= latest) await new Promise(r => setTimeout(r, 150));
}
await drainPendingWire(); // reconcile any placements that couldn't wire on their first pass
state.updatedAt = new Date().toISOString();
saveState();
} catch (e) {