use std::{
collections::{BTreeMap, btree_map::Entry},
sync::{Arc, RwLock},
};
use iroh_base::NodeId;
use n0_future::{
boxed::BoxStream,
stream::{self, StreamExt},
time::SystemTime,
};
use super::{Discovery, DiscoveryError, DiscoveryItem, NodeData, NodeInfo};
#[derive(Debug, Default, Clone)]
#[repr(transparent)]
pub struct StaticProvider {
nodes: Arc<RwLock<BTreeMap<NodeId, StoredNodeInfo>>>,
}
#[derive(Debug)]
struct StoredNodeInfo {
data: NodeData,
last_updated: SystemTime,
}
impl StaticProvider {
pub const PROVENANCE: &'static str = "static_discovery";
pub fn new() -> Self {
Self::default()
}
pub fn from_node_info(infos: impl IntoIterator<Item = impl Into<NodeInfo>>) -> Self {
let res = Self::default();
for info in infos {
res.add_node_info(info);
}
res
}
pub fn set_node_info(&self, node_info: impl Into<NodeInfo>) -> Option<NodeData> {
let last_updated = SystemTime::now();
let NodeInfo { node_id, data } = node_info.into();
let mut guard = self.nodes.write().expect("poisoned");
let previous = guard.insert(node_id, StoredNodeInfo { data, last_updated });
previous.map(|x| x.data)
}
pub fn add_node_info(&self, node_info: impl Into<NodeInfo>) {
let last_updated = SystemTime::now();
let NodeInfo { node_id, data } = node_info.into();
let mut guard = self.nodes.write().expect("poisoned");
match guard.entry(node_id) {
Entry::Occupied(mut entry) => {
let existing = entry.get_mut();
existing
.data
.add_direct_addresses(data.direct_addresses().iter().copied());
existing.data.set_relay_url(data.relay_url().cloned());
existing.data.set_user_data(data.user_data().cloned());
existing.last_updated = last_updated;
}
Entry::Vacant(entry) => {
entry.insert(StoredNodeInfo { data, last_updated });
}
}
}
pub fn get_node_info(&self, node_id: NodeId) -> Option<NodeInfo> {
let guard = self.nodes.read().expect("poisoned");
let info = guard.get(&node_id)?;
Some(NodeInfo::from_parts(node_id, info.data.clone()))
}
pub fn remove_node_info(&self, node_id: NodeId) -> Option<NodeInfo> {
let mut guard = self.nodes.write().expect("poisoned");
let info = guard.remove(&node_id)?;
Some(NodeInfo::from_parts(node_id, info.data))
}
}
impl Discovery for StaticProvider {
fn publish(&self, _data: &NodeData) {}
fn resolve(
&self,
node_id: NodeId,
) -> Option<BoxStream<Result<super::DiscoveryItem, DiscoveryError>>> {
let guard = self.nodes.read().expect("poisoned");
let info = guard.get(&node_id);
match info {
Some(node_info) => {
let last_updated = node_info
.last_updated
.duration_since(SystemTime::UNIX_EPOCH)
.expect("time drift")
.as_micros() as u64;
let item = DiscoveryItem::new(
NodeInfo::from_parts(node_id, node_info.data.clone()),
Self::PROVENANCE,
Some(last_updated),
);
Some(stream::iter(Some(Ok(item))).boxed())
}
None => None,
}
}
}
#[cfg(test)]
mod tests {
use iroh_base::{NodeAddr, SecretKey};
use n0_snafu::{Result, ResultExt};
use super::*;
use crate::Endpoint;
#[tokio::test]
async fn test_basic() -> Result {
let discovery = StaticProvider::new();
let _ep = Endpoint::builder()
.add_discovery(discovery.clone())
.bind()
.await?;
let key = SecretKey::from_bytes(&[0u8; 32]);
let addr = NodeAddr {
node_id: key.public(),
relay_url: Some("https://example.com".parse()?),
direct_addresses: Default::default(),
};
let user_data = Some("foobar".parse().unwrap());
let node_info = NodeInfo::from(addr.clone()).with_user_data(user_data.clone());
discovery.add_node_info(node_info.clone());
let back = discovery.get_node_info(key.public()).context("no addr")?;
assert_eq!(back, node_info);
assert_eq!(back.user_data(), user_data.as_ref());
assert_eq!(back.into_node_addr(), addr);
let removed = discovery
.remove_node_info(key.public())
.context("nothing removed")?;
assert_eq!(removed, node_info);
let res = discovery.get_node_info(key.public());
assert!(res.is_none());
Ok(())
}
}