From 5f50e8093181bd170e9bf88f236413ea4f5e4982 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Wed, 1 Apr 2026 08:27:06 -0700 Subject: [PATCH] perf: replace server round-trip with client-side filter for My Nodes toggle (#401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #381 — The "My Nodes" filter in `packets.js` was making a **server API call inside `renderTableRows()`** on every render cycle. With WebSocket updates arriving every few seconds while the toggle was active, this created continuous unnecessary server load. ## What Changed **`public/packets.js`** — Replaced the `api('/packets?nodes=...')` server call with a pure client-side filter: ```js // Before: server round-trip on every render const myData = await api('/packets?nodes=' + allKeys.join(',') + '&limit=500'); displayPackets = myData.packets || []; // After: filter already-loaded packets client-side displayPackets = displayPackets.filter(p => { const dj = p.decoded_json || ''; return allKeys.some(k => dj.includes(k)); }); ``` This uses the exact same matching logic as the server's `QueryMultiNodePackets()` — a string contains check on `decoded_json` for each pubkey — but without the network round-trip. **`test-frontend-helpers.js`** — Added 5 unit tests for the filter logic: - Single and multiple pubkey matching - No matches / empty keys edge case - Null/empty `decoded_json` handled gracefully **`public/index.html`** — Cache busters bumped. ## Test Results - Frontend helpers: **232 passed, 0 failed** (including 5 new tests) - Packet filter: **62 passed, 0 failed** - Aging: **29 passed, 0 failed** Co-authored-by: you --- public/index.html | 56 ++++++++++++++++++++-------------------- public/packets.js | 10 +++---- test-frontend-helpers.js | 50 +++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 33 deletions(-) diff --git a/public/index.html b/public/index.html index ff334314..d7ee9f02 100644 --- a/public/index.html +++ b/public/index.html @@ -22,9 +22,9 @@ - - - + + + @@ -85,30 +85,30 @@
- - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/packets.js b/public/packets.js index f44f5700..27d79531 100644 --- a/public/packets.js +++ b/public/packets.js @@ -997,7 +997,7 @@ const groupBtn = document.getElementById('fGroup'); if (groupBtn) groupBtn.classList.toggle('active', groupByHash); - // Filter to claimed/favorited nodes if toggle is on — use server-side multi-node lookup + // Filter to claimed/favorited nodes — pure client-side filter (no server round-trip) let displayPackets = packets; if (filters.myNodes) { const myNodes = JSON.parse(localStorage.getItem('meshcore-my-nodes') || '[]'); @@ -1005,10 +1005,10 @@ const favs = getFavorites(); const allKeys = [...new Set([...myKeys, ...favs])]; if (allKeys.length > 0) { - try { - const myData = await api('/packets?nodes=' + allKeys.join(',') + '&limit=500'); - displayPackets = myData.packets || []; - } catch { displayPackets = []; } + displayPackets = displayPackets.filter(p => { + const dj = p.decoded_json || ''; + return allKeys.some(k => dj.includes(k)); + }); } else { displayPackets = []; } diff --git a/test-frontend-helpers.js b/test-frontend-helpers.js index 65f18106..eb908dcf 100644 --- a/test-frontend-helpers.js +++ b/test-frontend-helpers.js @@ -2642,6 +2642,56 @@ console.log('\n=== packets.js: savedTimeWindowMin defaults ==='); assert.ok(deltaMin > 10 && deltaMin < 25, `expected capped ~15m window, got ${deltaMin.toFixed(2)}m`); }); } +// ===== My Nodes client-side filter (issue #381) ===== +{ + console.log('\n--- My Nodes client-side filter ---'); + + // Simulate the client-side filter logic from packets.js renderTableRows() + function filterMyNodes(packets, allKeys) { + if (!allKeys.length) return []; + return packets.filter(p => { + const dj = p.decoded_json || ''; + return allKeys.some(k => dj.includes(k)); + }); + } + + const testPackets = [ + { decoded_json: '{"pubKey":"abc123","name":"Node1"}' }, + { decoded_json: '{"pubKey":"def456","name":"Node2"}' }, + { decoded_json: '{"pubKey":"ghi789","name":"Node3","hops":["abc123"]}' }, + { decoded_json: '' }, + { decoded_json: null }, + ]; + + test('filters packets matching a single pubkey', () => { + const result = filterMyNodes(testPackets, ['abc123']); + assert.strictEqual(result.length, 2, 'should match sender + hop'); + assert.ok(result[0].decoded_json.includes('abc123')); + assert.ok(result[1].decoded_json.includes('abc123')); + }); + + test('filters packets matching multiple pubkeys', () => { + const result = filterMyNodes(testPackets, ['abc123', 'def456']); + assert.strictEqual(result.length, 3); + }); + + test('returns empty array for no matching keys', () => { + const result = filterMyNodes(testPackets, ['zzz999']); + assert.strictEqual(result.length, 0); + }); + + test('returns empty array when allKeys is empty', () => { + const result = filterMyNodes(testPackets, []); + assert.strictEqual(result.length, 0); + }); + + test('handles null/empty decoded_json gracefully', () => { + const result = filterMyNodes(testPackets, ['abc123']); + // Should not throw, null decoded_json packets are skipped + assert.strictEqual(result.length, 2); + }); +} + // ===== SUMMARY ===== Promise.allSettled(pendingTests).then(() => { console.log(`\n${'═'.repeat(40)}`);