From 1695f712c60174240e3c5a7f84a12358574498c7 Mon Sep 17 00:00:00 2001 From: martbost Date: Mon, 17 Aug 2026 15:17:39 -0500 Subject: [PATCH] Wire new matrix placements in ~60s, not once a day MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- chain.js | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/chain.js b/chain.js index f572f09..c03e284 100644 --- a/chain.js +++ b/chain.js @@ -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) {