From 16ead248d0a579bf3b9940cf665e763f3f5a7cdf Mon Sep 17 00:00:00 2001 From: Arjen <18398758+Origami74@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:58:45 +0200 Subject: [PATCH] feat(ble): public peer-view read API for embedders running run_rx_loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose ControlReadHandle (was pub(crate)) + make Node::control_read_handle() public, and add ControlReadHandle::peer_views() -> Vec { node_addr_hex, npub, connected }, read lock-free from the tick-published stats snapshot (peer_meta). This lets an embedder run run_rx_loop on a background task (which exclusively borrows &mut Node) and still poll live peer state from a cloned handle — no &Node access needed. Used by the Myco app's developer UI. --- src/control/read_handle.rs | 36 +++++++++++++++++++++++++++++++++++- src/node/mod.rs | 6 ++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/control/read_handle.rs b/src/control/read_handle.rs index 22660f1..27b5fd4 100644 --- a/src/control/read_handle.rs +++ b/src/control/read_handle.rs @@ -41,7 +41,7 @@ use super::snapshot::{EntitySnapshot, RoutingSnapshot, StatsSnapshot}; /// starting R1 as `show_*` queries cut over to off-loop rendering; until then /// they are wired but unread. #[derive(Clone)] -pub(crate) struct ControlReadHandle { +pub struct ControlReadHandle { /// Effectively-immutable node context (config, identity, limits). context: Arc, /// Metrics registry (counters / gauges) for `show_stats_*`. @@ -108,6 +108,40 @@ impl ControlReadHandle { } } +/// A minimal, public view of one peer for embedders (e.g. an app UI), read +/// lock-free from the tick-published snapshot. See [`ControlReadHandle::peer_views`]. +#[derive(Debug, Clone)] +pub struct PeerView { + /// The peer's `node_addr`, hex-encoded. + pub node_addr_hex: String, + /// Resolved npub (or the `node_addr` hex when not yet resolved to a peer). + pub npub: String, + /// Whether the peer is currently in the live authenticated-peer table. + pub connected: bool, +} + +impl ControlReadHandle { + /// A lock-free snapshot of known peers (node_addr / npub / connected), + /// read from the tick-published stats snapshot. + /// + /// Intended for embedders that run [`crate::Node::run_rx_loop`] on a + /// background task (so the node is exclusively borrowed there) and poll peer + /// state from a clone of this handle — the read touches only an `ArcSwap` + /// load, never the `Node`. See the Myco app for the reference embedding. + pub fn peer_views(&self) -> Vec { + self.stats + .load() + .peer_meta + .iter() + .map(|(addr, meta)| PeerView { + node_addr_hex: addr.to_string(), + npub: meta.npub.clone(), + connected: meta.is_active, + }) + .collect() + } +} + /// Attempt to serve a request entirely from the read handle, off the rx_loop. /// /// Returns `Some(response)` when the command is a pure-snapshot query that has diff --git a/src/node/mod.rs b/src/node/mod.rs index bc1a84f..3098cd9 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -1476,8 +1476,10 @@ impl Node { /// over this node's already-shared `NodeContext` and `MetricsRegistry`. /// /// Used at control-socket spawn time so pure-snapshot `show_*` queries - /// render off the rx_loop. Cloneable; cheap (all `Arc` clones). - pub(crate) fn control_read_handle(&self) -> crate::control::read_handle::ControlReadHandle { + /// render off the rx_loop. Cloneable; cheap (all `Arc` clones). Also the + /// public seam for embedders that run [`Self::run_rx_loop`] on a background + /// task and poll peer state via [`crate::control::read_handle::ControlReadHandle::peer_views`]. + pub fn control_read_handle(&self) -> crate::control::read_handle::ControlReadHandle { crate::control::read_handle::ControlReadHandle::new( self.context.clone(), self.metrics.clone(),