perf: resolve message metadata in one batched request

Every incoming echo triggers a sweep of the whole rendered message list, and
refreshMessagesMeta() awaited one /api/messages/<id>/meta per message inside
the loop. Messages that never gain a route (nothing heard them) never stop
qualifying for the sweep, so the same ~180 messages were re-fetched every few
seconds: 7,500 requests in nine minutes on a single tab, each opening its own
SQLite connections. The single-threaded werkzeug server — the same one
production runs — had no room left for anything else, so the UI hung on
"Loading messages..." / "Connecting..." while the device was in fact connected.

Add GET /api/messages/meta?ids=... resolving the whole sweep with a handful of
queries, batching the row and echo lookups, and have the client collect ids
first and fetch them in chunks. The per-message endpoint stays for forced
single refreshes; both now share _build_message_meta() and the existing
_build_channel_secrets / _get_row_pkt_payload helpers, so the payload is
unchanged (verified byte-identical against the old response).

Measured on the local container, 500 messages rendered / 181 needing meta:
one 273 ms request in place of 181 sequential ones at ~82 ms each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-07-31 07:44:48 +02:00
parent ebd2e95fe1
commit 0e524ddbc1
4 changed files with 133 additions and 70 deletions
+27 -8
View File
@@ -1218,9 +1218,17 @@ function appendMessageFromSocket(data) {
markChannelAsRead(currentChannelIdx, msg.timestamp);
}
// Cap on ids per /api/messages/meta request, to keep the query string short.
const META_BATCH_SIZE = 200;
/**
* Refresh metadata (SNR, hops, route, analyzer) for messages missing it.
* Fetches /api/messages/<id>/meta for each incomplete message, updates DOM in-place.
*
* Every echo triggers a sweep of the whole rendered list, and messages that
* never gain a route (no repeaters heard them) never stop qualifying so this
* must stay cheap. Ids are collected first and resolved with batched
* /api/messages/meta calls; one request per message used to flood the
* single-threaded server and hang the UI.
*/
async function refreshMessagesMeta(forceIds = []) {
const container = document.getElementById('messagesList');
@@ -1228,7 +1236,8 @@ async function refreshMessagesMeta(forceIds = []) {
const forced = new Set((forceIds || []).map(String));
// Find message wrappers that don't have full metadata yet
// Collect message wrappers that don't have full metadata yet
const pending = new Map(); // msgId -> wrapper
const wrappers = container.querySelectorAll('.message-wrapper[data-msg-id]');
for (const wrapper of wrappers) {
const msgId = wrapper.dataset.msgId;
@@ -1245,14 +1254,24 @@ async function refreshMessagesMeta(forceIds = []) {
if (hasRoute && hasAnalyzer) continue;
}
try {
const resp = await fetch(`/api/messages/${msgId}/meta`);
const meta = await resp.json();
if (!meta.success) continue;
pending.set(msgId, wrapper);
}
if (pending.size === 0) return;
updateMessageMetaDOM(wrapper, meta);
const ids = Array.from(pending.keys());
for (let i = 0; i < ids.length; i += META_BATCH_SIZE) {
const chunk = ids.slice(i, i + META_BATCH_SIZE);
try {
const resp = await fetch(`/api/messages/meta?ids=${chunk.join(',')}`);
const data = await resp.json();
if (!data.success || !data.metas) continue;
for (const [msgId, meta] of Object.entries(data.metas)) {
const wrapper = pending.get(msgId);
if (wrapper && meta.success) updateMessageMetaDOM(wrapper, meta);
}
} catch (e) {
console.error(`Error fetching meta for msg #${msgId}:`, e);
console.error('Error fetching message meta batch:', e);
}
}
}