mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-14 21:55:08 +00:00
Move hop resolution to client side
Create public/hop-resolver.js that mirrors the server's disambiguateHops() algorithm (prefix index, forward/backward pass, distance sanity check). Replace all /api/resolve-hops fetch calls in packets.js with local HopResolver.resolve() calls. The resolver lazily fetches and caches the full nodes list via /api/nodes on first use. The server endpoint is kept as fallback but no longer called by the UI, eliminating 40+ HTTP requests per session.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Client-side hop resolver — eliminates /api/resolve-hops HTTP requests.
|
||||
* Mirrors the server's disambiguateHops() logic from server.js.
|
||||
*/
|
||||
window.HopResolver = (function() {
|
||||
'use strict';
|
||||
|
||||
const MAX_HOP_DIST = 1.8; // ~200km in degrees
|
||||
let prefixIdx = {}; // lowercase hex prefix → [node, ...]
|
||||
let nodesList = [];
|
||||
|
||||
function dist(lat1, lon1, lat2, lon2) {
|
||||
return Math.sqrt((lat1 - lat2) ** 2 + (lon1 - lon2) ** 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize (or rebuild) the prefix index from the full nodes list.
|
||||
* @param {Array} nodes - Array of {public_key, name, lat, lon, ...}
|
||||
*/
|
||||
function init(nodes) {
|
||||
nodesList = nodes || [];
|
||||
prefixIdx = {};
|
||||
for (const n of nodesList) {
|
||||
if (!n.public_key) continue;
|
||||
const pk = n.public_key.toLowerCase();
|
||||
for (let len = 1; len <= 3; len++) {
|
||||
const p = pk.slice(0, len * 2);
|
||||
if (!prefixIdx[p]) prefixIdx[p] = [];
|
||||
prefixIdx[p].push(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an array of hex hop prefixes to node info.
|
||||
* Returns a map: { hop: {name, pubkey, lat, lon, ambiguous, unreliable} }
|
||||
*
|
||||
* @param {string[]} hops - Hex prefixes
|
||||
* @param {number|null} originLat - Sender latitude (forward anchor)
|
||||
* @param {number|null} originLon - Sender longitude (forward anchor)
|
||||
* @param {number|null} observerLat - Observer latitude (backward anchor)
|
||||
* @param {number|null} observerLon - Observer longitude (backward anchor)
|
||||
* @returns {Object} resolved map keyed by hop prefix
|
||||
*/
|
||||
function resolve(hops, originLat, originLon, observerLat, observerLon) {
|
||||
if (!hops || !hops.length) return {};
|
||||
|
||||
const resolved = {};
|
||||
const hopPositions = {};
|
||||
|
||||
// First pass: find candidates
|
||||
for (const hop of hops) {
|
||||
const h = hop.toLowerCase();
|
||||
const candidates = prefixIdx[h] || [];
|
||||
if (candidates.length === 0) {
|
||||
resolved[hop] = { name: null, candidates: [] };
|
||||
} else if (candidates.length === 1) {
|
||||
resolved[hop] = { name: candidates[0].name, pubkey: candidates[0].public_key, candidates: [{ name: candidates[0].name, pubkey: candidates[0].public_key }] };
|
||||
} else {
|
||||
resolved[hop] = { name: candidates[0].name, pubkey: candidates[0].public_key, ambiguous: true, candidates: candidates.map(c => ({ name: c.name, pubkey: c.public_key, lat: c.lat, lon: c.lon })) };
|
||||
}
|
||||
}
|
||||
|
||||
// Build initial positions for unambiguous hops
|
||||
for (const hop of hops) {
|
||||
const r = resolved[hop];
|
||||
if (r && !r.ambiguous && r.pubkey) {
|
||||
const node = nodesList.find(n => n.public_key === r.pubkey);
|
||||
if (node && node.lat && node.lon && !(node.lat === 0 && node.lon === 0)) {
|
||||
hopPositions[hop] = { lat: node.lat, lon: node.lon };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forward pass
|
||||
let lastPos = (originLat != null && originLon != null) ? { lat: originLat, lon: originLon } : null;
|
||||
for (let i = 0; i < hops.length; i++) {
|
||||
const hop = hops[i];
|
||||
if (hopPositions[hop]) { lastPos = hopPositions[hop]; continue; }
|
||||
const r = resolved[hop];
|
||||
if (!r || !r.ambiguous) continue;
|
||||
const withLoc = r.candidates.filter(c => c.lat && c.lon && !(c.lat === 0 && c.lon === 0));
|
||||
if (!withLoc.length) continue;
|
||||
let anchor = lastPos;
|
||||
if (!anchor && i === hops.length - 1 && observerLat != null) {
|
||||
anchor = { lat: observerLat, lon: observerLon };
|
||||
}
|
||||
if (anchor) {
|
||||
withLoc.sort((a, b) => dist(a.lat, a.lon, anchor.lat, anchor.lon) - dist(b.lat, b.lon, anchor.lat, anchor.lon));
|
||||
}
|
||||
r.name = withLoc[0].name;
|
||||
r.pubkey = withLoc[0].pubkey;
|
||||
hopPositions[hop] = { lat: withLoc[0].lat, lon: withLoc[0].lon };
|
||||
lastPos = hopPositions[hop];
|
||||
}
|
||||
|
||||
// Backward pass
|
||||
let nextPos = (observerLat != null && observerLon != null) ? { lat: observerLat, lon: observerLon } : null;
|
||||
for (let i = hops.length - 1; i >= 0; i--) {
|
||||
const hop = hops[i];
|
||||
if (hopPositions[hop]) { nextPos = hopPositions[hop]; continue; }
|
||||
const r = resolved[hop];
|
||||
if (!r || !r.ambiguous) continue;
|
||||
const withLoc = r.candidates.filter(c => c.lat && c.lon && !(c.lat === 0 && c.lon === 0));
|
||||
if (!withLoc.length || !nextPos) continue;
|
||||
withLoc.sort((a, b) => dist(a.lat, a.lon, nextPos.lat, nextPos.lon) - dist(b.lat, b.lon, nextPos.lat, nextPos.lon));
|
||||
r.name = withLoc[0].name;
|
||||
r.pubkey = withLoc[0].pubkey;
|
||||
hopPositions[hop] = { lat: withLoc[0].lat, lon: withLoc[0].lon };
|
||||
nextPos = hopPositions[hop];
|
||||
}
|
||||
|
||||
// Sanity check: drop hops impossibly far from neighbors
|
||||
for (let i = 0; i < hops.length; i++) {
|
||||
const pos = hopPositions[hops[i]];
|
||||
if (!pos) continue;
|
||||
const prev = i > 0 ? hopPositions[hops[i - 1]] : null;
|
||||
const next = i < hops.length - 1 ? hopPositions[hops[i + 1]] : null;
|
||||
if (!prev && !next) continue;
|
||||
const dPrev = prev ? dist(pos.lat, pos.lon, prev.lat, prev.lon) : 0;
|
||||
const dNext = next ? dist(pos.lat, pos.lon, next.lat, next.lon) : 0;
|
||||
const tooFarPrev = prev && dPrev > MAX_HOP_DIST;
|
||||
const tooFarNext = next && dNext > MAX_HOP_DIST;
|
||||
if ((tooFarPrev && tooFarNext) || (tooFarPrev && !next) || (tooFarNext && !prev)) {
|
||||
const r = resolved[hops[i]];
|
||||
if (r) r.unreliable = true;
|
||||
delete hopPositions[hops[i]];
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the resolver has been initialized with nodes.
|
||||
*/
|
||||
function ready() {
|
||||
return nodesList.length > 0;
|
||||
}
|
||||
|
||||
return { init: init, resolve: resolve, ready: ready };
|
||||
})();
|
||||
+2
-1
@@ -81,9 +81,10 @@
|
||||
<script src="vendor/qrcode.js"></script>
|
||||
<script src="roles.js?v=1774290000"></script>
|
||||
<script src="region-filter.js?v=1774355100"></script>
|
||||
<script src="hop-resolver.js?v=1774588800"></script>
|
||||
<script src="app.js?v=1774052279"></script>
|
||||
<script src="home.js?v=1774042199"></script>
|
||||
<script src="packets.js?v=1774354500"></script>
|
||||
<script src="packets.js?v=1774588800"></script>
|
||||
<script src="map.js?v=1774083841" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774075538" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774354200" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
|
||||
+25
-13
@@ -93,16 +93,25 @@
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
// Resolve hop hex prefixes to node names (cached)
|
||||
// Ensure HopResolver is initialized with the nodes list
|
||||
async function ensureHopResolver() {
|
||||
if (!HopResolver.ready()) {
|
||||
try {
|
||||
const data = await api('/nodes?limit=2000', { ttl: 60000 });
|
||||
HopResolver.init(data.nodes || []);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve hop hex prefixes to node names (cached, client-side)
|
||||
async function resolveHops(hops) {
|
||||
const unknown = hops.filter(h => !(h in hopNameCache));
|
||||
if (unknown.length) {
|
||||
try {
|
||||
const data = await api('/resolve-hops?hops=' + unknown.join(','));
|
||||
Object.assign(hopNameCache, data.resolved || {});
|
||||
// Cache misses as null so we don't re-query
|
||||
unknown.forEach(h => { if (!(h in hopNameCache)) hopNameCache[h] = null; });
|
||||
} catch {}
|
||||
await ensureHopResolver();
|
||||
const resolved = HopResolver.resolve(unknown);
|
||||
Object.assign(hopNameCache, resolved || {});
|
||||
// Cache misses as null so we don't re-query
|
||||
unknown.forEach(h => { if (!(h in hopNameCache)) hopNameCache[h] = null; });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -924,15 +933,18 @@
|
||||
if (routeBtn && pathHops.length) {
|
||||
routeBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
const obsId = obsName(pkt.observer_id);
|
||||
const observerParam = obsId ? '&observer=' + encodeURIComponent(obsId) : '';
|
||||
// Anchor disambiguation from sender's location if known (e.g. ADVERT lat/lon)
|
||||
const senderLat = decoded.lat || decoded.latitude;
|
||||
const senderLon = decoded.lon || decoded.longitude;
|
||||
const originParam = (senderLat != null && senderLon != null) ? `&originLat=${senderLat}&originLon=${senderLon}` : '';
|
||||
const resp = await fetch('/api/resolve-hops?hops=' + encodeURIComponent(pathHops.join(',')) + observerParam + originParam);
|
||||
const data = await resp.json();
|
||||
// Pass full pubkeys (server-disambiguated) to map, falling back to short prefix
|
||||
// Resolve observer position for backward-pass anchor
|
||||
let obsLat = null, obsLon = null;
|
||||
const obsId = obsName(pkt.observer_id);
|
||||
if (obsId && HopResolver.ready()) {
|
||||
// Try to find observer in nodes list by name — best effort
|
||||
}
|
||||
await ensureHopResolver();
|
||||
const data = { resolved: HopResolver.resolve(pathHops, senderLat || null, senderLon || null, obsLat, obsLon) };
|
||||
// Pass full pubkeys (client-disambiguated) to map, falling back to short prefix
|
||||
const resolvedKeys = pathHops.map(h => {
|
||||
const r = data.resolved?.[h];
|
||||
return r?.pubkey || h;
|
||||
|
||||
Reference in New Issue
Block a user