fix(meshcore): node list collapsing to a single node with multiple sources#3539
Conversation
…urces With multiple MeshCore sources, one source's node list could show only the local node while the DB held all its contacts. Two causes: - refreshContacts() cleared the in-memory contact map on ANY successful get_contacts response, including a transient empty read on a busy companion — wiping the known list until adverts refilled it. - getAllNodes() served only that volatile in-memory map, so a momentarily empty map collapsed the served list to just the local node. Fix: - getAllNodes() now merges durable per-source meshcore_nodes rows with the live in-memory contacts (in-memory wins for freshness; DB-only fields like battery/uptime/radio preserved) and lists the live local node first. - refreshContacts() only clears+replaces when get_contacts returns entries. - Add per-source repo read getNodesBySource(sourceId). - Callers of the now-async getAllNodes() updated to await (meshcore/source/unified routes, server map aggregation). Adds regression tests for the empty-response and DB-merge paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVGxkuD4Fwa2JGVim8ZeVj
|
Claude finished @Yeraze's task —— View job PR Review: fix(meshcore): node list collapsing to a single node with multiple sourcesTasks:
SummaryThis is a well-diagnosed and clearly written fix. The root cause analysis is accurate, the solution is appropriately layered (DB as durable base + in-memory overlay), and the test coverage directly targets the two failure modes. A few observations follow. Core Logic (
|
Summary
With multiple MeshCore sources, one source's node list could intermittently collapse to just the local node (e.g. one source showing 95 nodes, another showing 1) even though the device had — and the database had persisted — dozens of contacts. This was a read-path bug, not a data/processing bug: both sources'
meshcore_nodesrows were present and fresh in the DB.Diagnosed against a live two-MeshCore-source instance: the DB held 94 and 95 nodes for the two sources respectively, both freshly written, yet the UI served 1 for one of them.
Root cause
Two independent issues combined:
refreshContacts()wiped the in-memory contact map on a transient empty read. It calledthis.contacts.clear()on anyresponse.success && Array.isArray(response.data)— including a successful-but-emptyget_contacts, which a busy companion can return right after a reconnect or mid path-refresh (these refreshes are debounced off everyPathUpdatedpush). The list then stayed empty until adverts slowly refilled it.getAllNodes()served only the volatile in-memory map (this.localNode+this.contacts), never the durable per-source DB rows. So a momentarily-empty map collapsed the served list to a single node.Changes
MeshCoreManager.getAllNodes()is now async and merges the durable per-sourcemeshcore_nodesrows with the live in-memory contacts: DB rows are the base (so the list survives in-memory churn), in-memory contacts overlay them for freshness (rssi/snr/position), and DB-only fields (battery/uptime/radio) are preserved through the merge. The live local node is listed first and de-duplicated against its persisted row.refreshContacts()now only clears+replaces whenget_contactsreturns at least one entry — an empty response no longer wipes the known list.MeshCoreRepository.getNodesBySource(sourceId)(the existinggetAllNodes()was global/unscoped).getAllNodes()to await:meshcoreRoutes(/nodes,/status),sourceRoutes(node count + map),unifiedRoutes(aggregate counts), and the server-side map aggregation inserver.ts.Issues Resolved
None filed — reported directly. Relates to the multi-source MeshCore work.
Documentation Updates
Testing
src/server/meshcoreManager.getAllNodes.test.ts(6 tests): DB-backed list when in-memory empty; in-memory overlay without duplication preserving DB-only fields; live local node first + de-duped; DB-failure fallback to in-memory; emptyget_contactsdoes not wipe; non-emptyget_contactsstill replaces.meshcore_nodescount and no longer drops to 1 after a reconnect.🤖 Generated with Claude Code