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) {