pub mod reconnect;
#[cfg(test)]
mod tests;
use std::collections::{HashMap, HashSet};
use std::net::IpAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{broadcast, mpsc, RwLock};
use self::reconnect::ReconnectBackoff;
use crate::network::{NetworkPeer, NetworkPeerEvent, NetworkProvider, PeerAddr};
use crate::transport::websocket::{WebSocketTransport, WsFramedStream};
use crate::transport::{FramedStream, StreamTransport};
#[derive(Debug, Clone)]
pub struct PeerState {
pub id: String,
pub name: String,
pub ip: IpAddr,
pub online: bool,
pub ws_connected: bool,
pub connection_type: String,
pub os: Option<String>,
pub last_seen: Option<String>,
}
#[derive(Debug, Clone)]
pub enum PeerEvent {
Joined(PeerState),
Left(String),
Updated(PeerState),
WsConnected(String),
WsDisconnected(String),
AuthRequired { url: String },
}
#[derive(Debug, Clone)]
pub struct IncomingMessage {
pub from: String,
pub data: Vec<u8>,
pub received_at: Instant,
}
#[derive(Debug, thiserror::Error)]
pub enum SessionError {
#[error("unknown peer: {0}")]
UnknownPeer(String),
#[error("peer offline: {0}")]
PeerOffline(String),
#[error("connect failed: {0}")]
ConnectFailed(String),
#[error("send failed: {0}")]
SendFailed(String),
#[error("reconnect backoff: retry after {retry_after:?}")]
ReconnectBackoff {
retry_after: Duration,
},
#[error("transport error: {0}")]
Transport(#[from] crate::transport::TransportError),
}
struct WsConnectionHandle {
send_tx: mpsc::Sender<Vec<u8>>,
close_tx: mpsc::Sender<()>,
#[allow(dead_code)]
peer_id: String,
#[allow(dead_code)]
connected_at: Instant,
}
pub struct PeerRegistry<N: NetworkProvider + 'static> {
network: Arc<N>,
ws_transport: Arc<WebSocketTransport<N>>,
peers: Arc<RwLock<HashMap<String, PeerState>>>,
ws_connections: Arc<RwLock<HashMap<String, WsConnectionHandle>>>,
peer_backoffs: Arc<RwLock<HashMap<String, ReconnectBackoff>>>,
connecting: Arc<RwLock<HashSet<String>>>,
event_tx: broadcast::Sender<PeerEvent>,
incoming_tx: broadcast::Sender<IncomingMessage>,
}
impl<N: NetworkProvider + 'static> PeerRegistry<N> {
pub fn new(network: Arc<N>, ws_transport: Arc<WebSocketTransport<N>>) -> Self {
let (event_tx, _) = broadcast::channel(256);
let (incoming_tx, _) = broadcast::channel(1024);
Self {
network,
ws_transport,
peers: Arc::new(RwLock::new(HashMap::new())),
ws_connections: Arc::new(RwLock::new(HashMap::new())),
peer_backoffs: Arc::new(RwLock::new(HashMap::new())),
connecting: Arc::new(RwLock::new(HashSet::new())),
event_tx,
incoming_tx,
}
}
pub async fn start(&self) {
self.spawn_peer_event_loop();
self.spawn_accept_loop().await;
}
fn spawn_peer_event_loop(&self) {
let mut events = self.network.peer_events();
let peers = self.peers.clone();
let ws_connections = self.ws_connections.clone();
let event_tx = self.event_tx.clone();
tokio::spawn(async move {
loop {
match events.recv().await {
Ok(NetworkPeerEvent::Joined(network_peer)) => {
let state = network_peer_to_state(&network_peer);
let peer_event = PeerEvent::Joined(state.clone());
{
let mut map = peers.write().await;
map.insert(network_peer.id.clone(), state);
}
let _ = event_tx.send(peer_event);
tracing::info!(
peer_id = %network_peer.id,
peer_name = %network_peer.hostname,
"session: peer joined"
);
}
Ok(NetworkPeerEvent::Left(peer_id)) => {
let handle = {
let mut conns = ws_connections.write().await;
conns.remove(&peer_id)
};
if let Some(handle) = handle {
let _ = handle.close_tx.send(()).await;
let _ = event_tx.send(PeerEvent::WsDisconnected(peer_id.clone()));
tracing::info!(
peer_id = %peer_id,
"session: closed WS connection for departing peer"
);
}
{
let mut map = peers.write().await;
map.remove(&peer_id);
}
let _ = event_tx.send(PeerEvent::Left(peer_id.clone()));
tracing::info!(peer_id = %peer_id, "session: peer left");
}
Ok(NetworkPeerEvent::Updated(network_peer)) => {
let mut state = network_peer_to_state(&network_peer);
{
let mut map = peers.write().await;
if let Some(existing) = map.get(&network_peer.id) {
state.ws_connected = existing.ws_connected;
}
map.insert(network_peer.id.clone(), state.clone());
}
let _ = event_tx.send(PeerEvent::Updated(state));
tracing::debug!(
peer_id = %network_peer.id,
"session: peer updated"
);
}
Ok(NetworkPeerEvent::AuthRequired { url }) => {
let _ = event_tx.send(PeerEvent::AuthRequired { url });
}
Err(broadcast::error::RecvError::Lagged(n)) => {
tracing::warn!(
missed = n,
"session: peer event receiver lagged, missed {n} events"
);
}
Err(broadcast::error::RecvError::Closed) => {
tracing::debug!("session: peer event channel closed");
break;
}
}
}
});
}
async fn spawn_accept_loop(&self) {
let ws_transport = self.ws_transport.clone();
let ws_connections = self.ws_connections.clone();
let peers = self.peers.clone();
let event_tx = self.event_tx.clone();
let incoming_tx = self.incoming_tx.clone();
let mut listener = match ws_transport.listen().await {
Ok(l) => l,
Err(e) => {
tracing::error!("session: failed to start WS listener: {e}");
return;
}
};
tokio::spawn(async move {
loop {
match listener.accept().await {
Some(stream) => {
let peer_id = stream.remote_peer_id().to_string();
tracing::info!(
peer_id = %peer_id,
"session: accepted incoming WS connection"
);
let handle = spawn_connection_task(
stream,
peer_id.clone(),
ws_connections.clone(),
peers.clone(),
event_tx.clone(),
incoming_tx.clone(),
);
{
let mut conns = ws_connections.write().await;
conns.insert(peer_id.clone(), handle);
}
{
let mut map = peers.write().await;
if let Some(state) = map.get_mut(&peer_id) {
state.ws_connected = true;
}
}
let _ = event_tx.send(PeerEvent::WsConnected(peer_id));
}
None => {
tracing::debug!("session: WS listener closed");
break;
}
}
}
});
}
pub async fn peers(&self) -> Vec<PeerState> {
let map = self.peers.read().await;
map.values().cloned().collect()
}
pub fn on_peer_change(&self) -> broadcast::Receiver<PeerEvent> {
self.event_tx.subscribe()
}
pub async fn send(&self, peer_id: &str, data: &[u8]) -> Result<(), SessionError> {
let peer_id = {
let map = self.peers.read().await;
if map.contains_key(peer_id) {
peer_id.to_string()
} else {
map.values()
.find(|p| p.name == peer_id)
.map(|p| p.id.clone())
.ok_or_else(|| SessionError::UnknownPeer(peer_id.to_string()))?
}
};
let peer_id = peer_id.as_str();
let peer_addr = {
let map = self.peers.read().await;
let state = map
.get(peer_id)
.ok_or_else(|| SessionError::UnknownPeer(peer_id.to_string()))?;
if !state.online {
return Err(SessionError::PeerOffline(peer_id.to_string()));
}
PeerAddr {
ip: Some(state.ip),
hostname: state.name.clone(),
dns_name: None,
}
};
{
let conns = self.ws_connections.read().await;
if let Some(handle) = conns.get(peer_id) {
return handle
.send_tx
.send(data.to_vec())
.await
.map_err(|_| SessionError::SendFailed("connection task closed".to_string()));
}
}
{
let backoffs = self.peer_backoffs.read().await;
if let Some(backoff) = backoffs.get(peer_id) {
if backoff.should_retry().is_none() {
let retry_after = backoff.retry_after();
return Err(SessionError::ReconnectBackoff { retry_after });
}
}
}
{
let mut connecting = self.connecting.write().await;
if connecting.contains(peer_id) {
return Err(SessionError::ConnectFailed(
"connection already in progress".to_string(),
));
}
connecting.insert(peer_id.to_string());
}
tracing::info!(peer_id = %peer_id, "session: lazy connecting WS");
let connect_result = self.ws_transport.connect(&peer_addr).await;
{
let mut connecting = self.connecting.write().await;
connecting.remove(peer_id);
}
let ws_stream = match connect_result {
Ok(stream) => {
let mut backoffs = self.peer_backoffs.write().await;
backoffs
.entry(peer_id.to_string())
.or_insert_with(ReconnectBackoff::new)
.success();
stream
}
Err(e) => {
let mut backoffs = self.peer_backoffs.write().await;
backoffs
.entry(peer_id.to_string())
.or_insert_with(ReconnectBackoff::new)
.failure();
return Err(SessionError::ConnectFailed(e.to_string()));
}
};
let handle = spawn_connection_task(
ws_stream,
peer_id.to_string(),
self.ws_connections.clone(),
self.peers.clone(),
self.event_tx.clone(),
self.incoming_tx.clone(),
);
let send_result = handle
.send_tx
.send(data.to_vec())
.await
.map_err(|_| SessionError::SendFailed("connection task closed".to_string()));
{
let mut conns = self.ws_connections.write().await;
conns.insert(peer_id.to_string(), handle);
}
{
let mut map = self.peers.write().await;
if let Some(state) = map.get_mut(peer_id) {
state.ws_connected = true;
}
}
let _ = self
.event_tx
.send(PeerEvent::WsConnected(peer_id.to_string()));
send_result
}
pub async fn broadcast(&self, data: &[u8]) {
let conns = self.ws_connections.read().await;
for (peer_id, handle) in conns.iter() {
if let Err(_) = handle.send_tx.send(data.to_vec()).await {
tracing::warn!(
peer_id = %peer_id,
"session: broadcast send failed (connection task closed)"
);
}
}
}
pub fn subscribe(&self) -> broadcast::Receiver<IncomingMessage> {
self.incoming_tx.subscribe()
}
pub async fn disconnect(&self, peer_id: &str) {
let handle = {
let mut conns = self.ws_connections.write().await;
conns.remove(peer_id)
};
if let Some(handle) = handle {
let _ = handle.close_tx.send(()).await;
}
{
let mut map = self.peers.write().await;
if let Some(state) = map.get_mut(peer_id) {
state.ws_connected = false;
}
}
let _ = self
.event_tx
.send(PeerEvent::WsDisconnected(peer_id.to_string()));
}
}
fn spawn_connection_task(
stream: WsFramedStream,
peer_id: String,
ws_connections: Arc<RwLock<HashMap<String, WsConnectionHandle>>>,
peers: Arc<RwLock<HashMap<String, PeerState>>>,
event_tx: broadcast::Sender<PeerEvent>,
incoming_tx: broadcast::Sender<IncomingMessage>,
) -> WsConnectionHandle {
let (send_tx, mut send_rx) = mpsc::channel::<Vec<u8>>(256);
let (close_tx, mut close_rx) = mpsc::channel::<()>(1);
let handle = WsConnectionHandle {
send_tx: send_tx.clone(),
close_tx: close_tx.clone(),
peer_id: peer_id.clone(),
connected_at: Instant::now(),
};
tokio::spawn(async move {
let mut stream = stream;
let mut closed = false;
loop {
tokio::select! {
Some(data) = send_rx.recv() => {
if let Err(e) = stream.send(&data).await {
tracing::warn!(
peer_id = %peer_id,
error = %e,
"session: WS send error"
);
break;
}
}
result = stream.recv() => {
match result {
Ok(Some(data)) => {
let msg = IncomingMessage {
from: peer_id.clone(),
data,
received_at: Instant::now(),
};
let _ = incoming_tx.send(msg);
}
Ok(None) => {
tracing::info!(
peer_id = %peer_id,
"session: WS stream closed"
);
break;
}
Err(e) => {
tracing::warn!(
peer_id = %peer_id,
error = %e,
"session: WS recv error"
);
break;
}
}
}
_ = close_rx.recv() => {
tracing::info!(
peer_id = %peer_id,
"session: connection close requested"
);
closed = true;
let _ = stream.close().await;
break;
}
}
}
if !closed {
{
let mut conns = ws_connections.write().await;
conns.remove(&peer_id);
}
{
let mut map = peers.write().await;
if let Some(state) = map.get_mut(&peer_id) {
state.ws_connected = false;
}
}
let _ = event_tx.send(PeerEvent::WsDisconnected(peer_id));
}
});
handle
}
fn network_peer_to_state(peer: &NetworkPeer) -> PeerState {
let connection_type = if let Some(ref relay) = peer.relay {
format!("relay:{relay}")
} else if peer.cur_addr.is_some() {
"direct".to_string()
} else {
"unknown".to_string()
};
PeerState {
id: peer.id.clone(),
name: peer.hostname.clone(),
ip: peer.ip,
online: peer.online,
ws_connected: false,
connection_type,
os: peer.os.clone(),
last_seen: peer.last_seen.clone(),
}
}