diff --git a/public/analytics.js b/public/analytics.js
index 23df9649..822ecc3e 100644
--- a/public/analytics.js
+++ b/public/analytics.js
@@ -101,10 +101,10 @@
try {
_analyticsData = {};
const [hashData, rfData, topoData, chanData] = await Promise.all([
- api('/analytics/hash-sizes'),
- api('/analytics/rf'),
- api('/analytics/topology'),
- api('/analytics/channels'),
+ api('/analytics/hash-sizes', { ttl: 60000 }),
+ api('/analytics/rf', { ttl: 60000 }),
+ api('/analytics/topology', { ttl: 60000 }),
+ api('/analytics/channels', { ttl: 60000 }),
]);
_analyticsData = { hashData, rfData, topoData, chanData };
renderTab('overview');
@@ -747,7 +747,7 @@
`;
let allNodes = [];
- try { const nd = await api('/nodes?limit=2000'); allNodes = nd.nodes || []; } catch {}
+ try { const nd = await api('/nodes?limit=2000', { ttl: 10000 }); allNodes = nd.nodes || []; } catch {}
renderHashMatrix(data.topHops, allNodes);
renderCollisions(data.topHops, allNodes);
}
@@ -938,10 +938,10 @@
el.innerHTML = '
Analyzing route patterns…
';
try {
const [d2, d3, d4, d5] = await Promise.all([
- api('/analytics/subpaths?minLen=2&maxLen=2&limit=50'),
- api('/analytics/subpaths?minLen=3&maxLen=3&limit=30'),
- api('/analytics/subpaths?minLen=4&maxLen=4&limit=20'),
- api('/analytics/subpaths?minLen=5&maxLen=8&limit=15')
+ api('/analytics/subpaths?minLen=2&maxLen=2&limit=50', { ttl: 60000 }),
+ api('/analytics/subpaths?minLen=3&maxLen=3&limit=30', { ttl: 60000 }),
+ api('/analytics/subpaths?minLen=4&maxLen=4&limit=20', { ttl: 60000 }),
+ api('/analytics/subpaths?minLen=5&maxLen=8&limit=15', { ttl: 60000 })
]);
function renderTable(data, title) {
@@ -1032,7 +1032,7 @@
panel.classList.remove('collapsed');
panel.innerHTML = 'Loading…
';
try {
- const data = await api('/analytics/subpath-detail?hops=' + encodeURIComponent(hopsStr));
+ const data = await api('/analytics/subpath-detail?hops=' + encodeURIComponent(hopsStr), { ttl: 60000 });
renderSubpathDetail(panel, data);
} catch (e) {
panel.innerHTML = `Error: ${e.message}
`;
@@ -1141,9 +1141,9 @@
el.innerHTML = 'Loading node analytics…
';
try {
const [nodesResp, bulkHealth, netStatus] = await Promise.all([
- api('/nodes?limit=200&sortBy=lastSeen'),
- api('/nodes/bulk-health?limit=50'),
- api('/nodes/network-status')
+ api('/nodes?limit=200&sortBy=lastSeen', { ttl: 10000 }),
+ api('/nodes/bulk-health?limit=50', { ttl: 60000 }),
+ api('/nodes/network-status', { ttl: 60000 })
]);
const nodes = nodesResp.nodes || nodesResp;
const myNodes = JSON.parse(localStorage.getItem('meshcore-my-nodes') || '[]');
diff --git a/public/app.js b/public/app.js
index 58661372..6bd1c1da 100644
--- a/public/app.js
+++ b/public/app.js
@@ -11,19 +11,45 @@ function payloadTypeName(n) { return PAYLOAD_TYPES[n] || 'UNKNOWN'; }
function payloadTypeColor(n) { return PAYLOAD_COLORS[n] || 'unknown'; }
// --- Utilities ---
-const _apiPerf = { calls: 0, totalMs: 0, log: [] };
-async function api(path) {
+const _apiPerf = { calls: 0, totalMs: 0, log: [], cacheHits: 0 };
+const _apiCache = new Map();
+const _inflight = new Map();
+async function api(path, { ttl = 0, bust = false } = {}) {
const t0 = performance.now();
- const res = await fetch('/api' + path);
- if (!res.ok) throw new Error(`API ${res.status}: ${path}`);
- const data = await res.json();
- const ms = performance.now() - t0;
- _apiPerf.calls++;
- _apiPerf.totalMs += ms;
- _apiPerf.log.push({ path, ms: Math.round(ms), time: Date.now() });
- if (_apiPerf.log.length > 200) _apiPerf.log.shift();
- if (ms > 500) console.warn(`[SLOW API] ${path} took ${Math.round(ms)}ms`);
- return data;
+ if (!bust && ttl > 0) {
+ const cached = _apiCache.get(path);
+ if (cached && Date.now() < cached.expires) {
+ _apiPerf.calls++;
+ _apiPerf.cacheHits++;
+ _apiPerf.log.push({ path, ms: 0, time: Date.now(), cached: true });
+ if (_apiPerf.log.length > 200) _apiPerf.log.shift();
+ return cached.data;
+ }
+ }
+ // Deduplicate in-flight requests
+ if (_inflight.has(path)) return _inflight.get(path);
+ const promise = (async () => {
+ const res = await fetch('/api' + path);
+ if (!res.ok) throw new Error(`API ${res.status}: ${path}`);
+ const data = await res.json();
+ const ms = performance.now() - t0;
+ _apiPerf.calls++;
+ _apiPerf.totalMs += ms;
+ _apiPerf.log.push({ path, ms: Math.round(ms), time: Date.now() });
+ if (_apiPerf.log.length > 200) _apiPerf.log.shift();
+ if (ms > 500) console.warn(`[SLOW API] ${path} took ${Math.round(ms)}ms`);
+ if (ttl > 0) _apiCache.set(path, { data, expires: Date.now() + ttl });
+ return data;
+ })();
+ _inflight.set(path, promise);
+ promise.finally(() => _inflight.delete(path));
+ return promise;
+}
+
+function invalidateApiCache(prefix) {
+ for (const key of _apiCache.keys()) {
+ if (key.startsWith(prefix || '')) _apiCache.delete(key);
+ }
}
// Expose for console debugging: apiPerf()
window.apiPerf = function() {
@@ -39,7 +65,9 @@ window.apiPerf = function() {
totalMs: Math.round(s.totalMs)
})).sort((a, b) => b.totalMs - a.totalMs);
console.table(rows);
- return { calls: _apiPerf.calls, avgMs: Math.round(_apiPerf.totalMs / _apiPerf.calls), endpoints: rows };
+ const hitRate = _apiPerf.calls ? Math.round(_apiPerf.cacheHits / _apiPerf.calls * 100) : 0;
+ console.log(`Cache: ${_apiPerf.cacheHits} hits / ${_apiPerf.calls} calls (${hitRate}% hit rate)`);
+ return { calls: _apiPerf.calls, avgMs: Math.round(_apiPerf.totalMs / (_apiPerf.calls - _apiPerf.cacheHits || 1)), cacheHits: _apiPerf.cacheHits, hitRate: hitRate + '%', endpoints: rows };
};
function timeAgo(iso) {
@@ -165,6 +193,10 @@ function connectWS() {
ws.onmessage = (e) => {
try {
const msg = JSON.parse(e.data);
+ // Invalidate caches when new data arrives
+ invalidateApiCache('/stats');
+ invalidateApiCache('/nodes');
+ invalidateApiCache('/channels');
wsListeners.forEach(fn => fn(msg));
} catch {}
};
@@ -318,7 +350,7 @@ window.addEventListener('DOMContentLoaded', () => {
favDropdown.innerHTML = 'Loading...
';
const items = await Promise.all(favs.map(async (pk) => {
try {
- const h = await api('/nodes/' + pk + '/health');
+ const h = await api('/nodes/' + pk + '/health', { ttl: 30000 });
const age = h.stats.lastHeard ? Date.now() - new Date(h.stats.lastHeard).getTime() : null;
const status = age === null ? '🔴' : age < 3600000 ? '🟢' : age < 86400000 ? '🟡' : '🔴';
return ''
@@ -422,7 +454,7 @@ window.addEventListener('DOMContentLoaded', () => {
// --- Nav Stats ---
async function updateNavStats() {
try {
- const stats = await api('/stats');
+ const stats = await api('/stats', { ttl: 5000 });
const el = document.getElementById('navStats');
if (el) {
el.innerHTML = `${stats.totalPackets} pkts · ${stats.totalNodes} nodes · ${stats.totalObservers} obs`;
diff --git a/public/channels.js b/public/channels.js
index 2970d763..75d6f42a 100644
--- a/public/channels.js
+++ b/public/channels.js
@@ -18,7 +18,7 @@
if (cached && !cached.fetchedAt) return cached; // legacy null entries
}
try {
- const data = await api('/nodes/search?q=' + encodeURIComponent(name));
+ const data = await api('/nodes/search?q=' + encodeURIComponent(name), { ttl: 10000 });
// Try exact match first, then case-insensitive, then contains
const nodes = data.nodes || [];
const match = nodes.find(n => n.name === name)
@@ -110,7 +110,7 @@
}
try {
- const detail = await api('/nodes/' + encodeURIComponent(node.public_key));
+ const detail = await api('/nodes/' + encodeURIComponent(node.public_key), { ttl: 15000 });
const n = detail.node;
const adverts = detail.recentAdverts || [];
const role = n.is_repeater ? '📡 Repeater' : n.is_room ? '🏠 Room' : n.is_sensor ? '🌡 Sensor' : '📻 Companion';
@@ -389,7 +389,7 @@
async function loadChannels(silent) {
try {
- const data = await api('/channels');
+ const data = await api('/channels', { ttl: 15000 });
channels = (data.channels || []).sort((a, b) => (b.lastActivity || '').localeCompare(a.lastActivity || ''));
renderChannelList();
} catch (e) {
@@ -451,7 +451,7 @@
msgEl.innerHTML = 'Loading messages…
';
try {
- const data = await api(`/channels/${hash}/messages?limit=200`);
+ const data = await api(`/channels/${hash}/messages?limit=200`, { ttl: 10000 });
messages = data.messages || [];
renderMessages();
scrollToBottom();
@@ -466,7 +466,7 @@
if (!msgEl) return;
const wasAtBottom = msgEl.scrollHeight - msgEl.scrollTop - msgEl.clientHeight < 60;
try {
- const data = await api(`/channels/${selectedHash}/messages?limit=200`);
+ const data = await api(`/channels/${selectedHash}/messages?limit=200`, { ttl: 10000 });
const newMsgs = data.messages || [];
// #92: Use message ID/hash for change detection instead of count + timestamp
var _getLastId = function (arr) { var m = arr.length ? arr[arr.length - 1] : null; return m ? (m.id || m.packetId || m.timestamp || '') : ''; };
diff --git a/public/home.js b/public/home.js
index 6d9eb386..1c83cf14 100644
--- a/public/home.js
+++ b/public/home.js
@@ -146,7 +146,7 @@
if (!q) { suggest.classList.remove('open'); input.setAttribute('aria-expanded', 'false'); input.setAttribute('aria-activedescendant', ''); return; }
searchTimeout = setTimeout(async () => {
try {
- const data = await api('/nodes/search?q=' + encodeURIComponent(q));
+ const data = await api('/nodes/search?q=' + encodeURIComponent(q), { ttl: 10000 });
const nodes = data.nodes || [];
if (!nodes.length) {
suggest.innerHTML = 'No nodes found
';
@@ -247,7 +247,7 @@
const cards = await Promise.all(myNodes.map(async (mn) => {
try {
- const h = await api('/nodes/' + encodeURIComponent(mn.pubkey) + '/health');
+ const h = await api('/nodes/' + encodeURIComponent(mn.pubkey) + '/health', { ttl: 30000 });
const node = h.node || {};
const stats = h.stats || {};
const obs = h.observers || [];
@@ -369,7 +369,7 @@
// ==================== STATS ====================
async function loadStats() {
try {
- const s = await api('/stats');
+ const s = await api('/stats', { ttl: 5000 });
const el = document.getElementById('homeStats');
if (!el) return;
el.innerHTML = `
@@ -391,7 +391,7 @@
if (journey) journey.classList.remove('visible');
try {
- const h = await api('/nodes/' + encodeURIComponent(pubkey) + '/health');
+ const h = await api('/nodes/' + encodeURIComponent(pubkey) + '/health', { ttl: 30000 });
const node = h.node || {};
const stats = h.stats || {};
const packets = h.recentPackets || [];
diff --git a/public/index.html b/public/index.html
index 4c109c76..61920a01 100644
--- a/public/index.html
+++ b/public/index.html
@@ -76,17 +76,17 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+