use std::sync::Arc;
use iroh::Watcher;
use mcpmesh_local_api::{RelayInfo, SelfNetwork};
use crate::util::epoch_now_i64;
use super::MeshState;
pub(crate) fn project(
relays: impl IntoIterator<Item = (String, bool)>,
direct_addrs: Vec<String>,
last_change_epoch: Option<i64>,
) -> SelfNetwork {
let relays: Vec<RelayInfo> = relays
.into_iter()
.map(|(url, connected)| RelayInfo { url, connected })
.collect();
let online = relays.iter().any(|r| r.connected);
let home_relay = relays.iter().find(|r| r.connected).map(|r| r.url.clone());
SelfNetwork {
online,
home_relay,
relays,
direct_addrs,
last_change_epoch,
}
}
pub(crate) fn read_current(mesh: &MeshState, last_change_epoch: Option<i64>) -> SelfNetwork {
let relays = mesh
.endpoint
.home_relay_status()
.get()
.into_iter()
.map(|s| (super::reach::sanitize_relay_url(s.url()), s.is_connected()));
let direct_addrs = mesh
.endpoint
.addr()
.addrs
.iter()
.filter_map(|a| match a {
iroh::TransportAddr::Ip(sock) => Some(sock.to_string()),
_ => None,
})
.collect();
project(relays, direct_addrs, last_change_epoch)
}
pub fn spawn_self_net_watch(mesh: Arc<MeshState>) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let mut watcher = mesh.endpoint.home_relay_status();
let mut previous = project(std::iter::empty(), Vec::new(), None);
loop {
let current = read_current(&mesh, None);
if signature(¤t) != signature(&previous) {
let stamp = epoch_now_i64();
*mesh
.self_net_change
.lock()
.expect("self_net_change lock not poisoned") = Some(stamp);
let frame = SelfNetwork {
last_change_epoch: Some(stamp),
..current.clone()
};
let _ = mesh.self_net_bcast.send(frame);
previous = current;
}
if watcher.updated().await.is_err() {
return;
}
}
})
}
fn signature(net: &SelfNetwork) -> (bool, Option<&str>, Vec<(&str, bool)>) {
(
net.online,
net.home_relay.as_deref(),
net.relays
.iter()
.map(|r| (r.url.as_str(), r.connected))
.collect(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn projection_selects_the_first_connected_relay_as_home() {
let net = project(
[
("https://a.example:443".to_string(), false),
("https://b.example:443".to_string(), true),
("https://c.example:443".to_string(), true),
],
vec!["192.168.1.2:4444".into()],
None,
);
assert!(net.online);
assert_eq!(net.home_relay.as_deref(), Some("https://b.example:443"));
assert_eq!(net.relays.len(), 3);
let net = project(
[("https://a.example:443".to_string(), false)],
Vec::new(),
None,
);
assert!(!net.online, "a known-but-disconnected relay is not online");
assert_eq!(net.home_relay, None);
let net = project(std::iter::empty::<(String, bool)>(), Vec::new(), None);
assert!(!net.online, "no relays configured (relay_mode=disabled)");
assert!(net.relays.is_empty());
}
#[test]
fn only_online_home_relay_or_relay_state_count_as_a_transition() {
let base = project(
[("https://a.example:443".to_string(), true)],
vec!["10.0.0.1:1".into()],
None,
);
let addr_churn = project(
[("https://a.example:443".to_string(), true)],
vec!["10.0.0.2:2".into()],
None,
);
assert_eq!(
signature(&base),
signature(&addr_churn),
"address churn alone must not emit"
);
let relay_down = project(
[("https://a.example:443".to_string(), false)],
vec!["10.0.0.1:1".into()],
None,
);
assert_ne!(
signature(&base),
signature(&relay_down),
"a relay losing its connection is a transition"
);
let two_up = project(
[
("https://a.example:443".to_string(), true),
("https://b.example:443".to_string(), true),
],
vec!["10.0.0.1:1".into()],
None,
);
let secondary_down = project(
[
("https://a.example:443".to_string(), true),
("https://b.example:443".to_string(), false),
],
vec!["10.0.0.1:1".into()],
None,
);
assert_ne!(
signature(&two_up),
signature(&secondary_down),
"a secondary relay's connection state is a transition even while the home stays up — losing a fallback relay is exactly the pre-outage warning #90 exists to give"
);
let stamp_only = SelfNetwork {
last_change_epoch: Some(42),
..base.clone()
};
assert_eq!(
signature(&base),
signature(&stamp_only),
"the stamp itself must not count, or every emission differs from its successor"
);
}
}