enlace 0.2.5

Encrypted mailbox and latest-value slot fan-out.
Documentation
use std::sync::Arc;

use crate::config::Config;
use crate::coordinator::{Coordinator, TransportEndpoint};
use crate::error::OpenError;
use crate::kdf::NameError;
use crate::mailbox::Mailbox;
use crate::slot::Slot;
#[cfg(feature = "dht")]
use crate::transports::DhtTransport;
#[cfg(feature = "http")]
use crate::transports::HttpTransport;
#[cfg(feature = "iroh")]
use crate::transports::IrohTransport;
#[cfg(feature = "pkarr")]
use crate::transports::PkarrTransport;
use crate::transports::{HealthReport, HealthTracker};

#[derive(Clone)]
pub struct Namespace {
    pub(crate) inner: Arc<NamespaceInner>,
}

pub(crate) struct NamespaceInner {
    pub(crate) coordinator: Arc<Coordinator>,
    #[cfg(feature = "http")]
    pub(crate) http: Option<Arc<HttpTransport>>,
    #[cfg(feature = "pkarr")]
    pub(crate) pkarr: Option<Arc<PkarrTransport>>,
    #[cfg(feature = "dht")]
    pub(crate) dht: Option<Arc<DhtTransport>>,
    #[cfg(feature = "iroh")]
    pub(crate) iroh: Option<Arc<IrohTransport>>,
}

impl Namespace {
    #[allow(clippy::unused_async)]
    pub async fn open(seed: &[u8; 32], config: Config) -> Result<Self, OpenError> {
        validate_seed(seed)?;
        validate_config(&config)?;

        if config.signing.is_some() && config.trusted.is_empty() {
            tracing::warn!(
                "signing key configured without trusted keys; incoming messages will not be verified"
            );
        }

        let state = config.state.store();
        let mut transports = Vec::new();
        #[cfg(feature = "http")]
        let http = if let Some(http_config) = config.http.clone() {
            let http = Arc::new(HttpTransport::new(http_config).map_err(|err| {
                OpenError::TransportInit(crate::TransportKind::Http, Box::new(err))
            })?);
            let transport: Arc<dyn crate::transports::Transport> = http.clone();
            transports.push(TransportEndpoint {
                kind: crate::TransportKind::Http,
                transport,
            });
            Some(http)
        } else {
            None
        };
        #[cfg(feature = "pkarr")]
        let pkarr = if let Some(pkarr_config) = &config.pkarr {
            let pkarr = Arc::new(PkarrTransport::new(seed, pkarr_config).map_err(|err| {
                OpenError::TransportInit(crate::TransportKind::Pkarr, Box::new(err))
            })?);
            let transport: Arc<dyn crate::transports::Transport> = pkarr.clone();
            transports.push(TransportEndpoint {
                kind: crate::TransportKind::Pkarr,
                transport,
            });
            Some(pkarr)
        } else {
            None
        };
        #[cfg(feature = "dht")]
        let dht = if let Some(dht_config) = &config.dht {
            let dht = Arc::new(DhtTransport::new(seed, dht_config).map_err(|err| {
                OpenError::TransportInit(crate::TransportKind::Dht, Box::new(err))
            })?);
            let transport: Arc<dyn crate::transports::Transport> = dht.clone();
            transports.push(TransportEndpoint {
                kind: crate::TransportKind::Dht,
                transport,
            });
            Some(dht)
        } else {
            None
        };
        #[cfg(feature = "iroh")]
        let iroh = open_iroh_transport(&config, state.as_ref(), &mut transports).await?;
        for configured in &config.transports {
            transports.push(TransportEndpoint {
                kind: configured.kind,
                transport: Arc::clone(&configured.transport),
            });
        }
        let health = Arc::new(HealthTracker::from_config(&config));
        let coordinator = Arc::new(Coordinator::new(seed, transports, &config, state, health));

        Ok(Self {
            inner: Arc::new(NamespaceInner {
                coordinator,
                #[cfg(feature = "http")]
                http,
                #[cfg(feature = "pkarr")]
                pkarr,
                #[cfg(feature = "dht")]
                dht,
                #[cfg(feature = "iroh")]
                iroh,
            }),
        })
    }

    pub fn mailbox(&self, name: &str) -> Result<Mailbox, NameError> {
        Mailbox::new(Arc::clone(&self.inner), name)
    }

    pub fn slot(&self, name: &str) -> Result<Slot, NameError> {
        Slot::new(Arc::clone(&self.inner), name)
    }

    #[must_use]
    #[cfg(feature = "http")]
    pub fn http(&self) -> Option<&HttpTransport> {
        self.inner.http.as_deref()
    }

    #[must_use]
    #[cfg(feature = "pkarr")]
    pub fn pkarr(&self) -> Option<&PkarrTransport> {
        self.inner.pkarr.as_deref()
    }

    #[must_use]
    #[cfg(feature = "dht")]
    pub fn dht(&self) -> Option<&DhtTransport> {
        self.inner.dht.as_deref()
    }

    #[must_use]
    #[cfg(feature = "iroh")]
    pub fn iroh(&self) -> Option<&IrohTransport> {
        self.inner.iroh.as_deref()
    }

    #[must_use]
    pub fn health(&self) -> HealthReport {
        self.inner.coordinator.health()
    }
}

fn validate_seed(seed: &[u8; 32]) -> Result<(), OpenError> {
    if seed.iter().all(|&b| b == 0) {
        return Err(OpenError::InvalidSeed);
    }
    Ok(())
}

fn validate_config(config: &Config) -> Result<(), OpenError> {
    if config.transport_count() == 0 {
        return Err(OpenError::NoTransport);
    }
    Ok(())
}

#[cfg(feature = "iroh")]
async fn open_iroh_transport(
    config: &Config,
    state: &dyn crate::state::StateStore,
    transports: &mut Vec<TransportEndpoint>,
) -> Result<Option<Arc<IrohTransport>>, OpenError> {
    if let Some(iroh_config) = &config.iroh {
        let iroh = Arc::new(IrohTransport::new(iroh_config, state).await.map_err(
            |err| match err {
                crate::transports::IrohInitError::State(err) => OpenError::State(err),
                crate::transports::IrohInitError::Transport(err) => {
                    OpenError::TransportInit(crate::TransportKind::Iroh, Box::new(err))
                }
            },
        )?);
        let transport: Arc<dyn crate::transports::Transport> = iroh.clone();
        transports.push(TransportEndpoint {
            kind: crate::TransportKind::Iroh,
            transport,
        });
        Ok(Some(iroh))
    } else {
        Ok(None)
    }
}

#[cfg(test)]
#[cfg(feature = "iroh")]
mod tests {
    use super::*;

    use crate::config::IrohConfig;

    #[tokio::test]
    async fn iroh_config_exposes_transport_handle() {
        let namespace = Namespace::open(
            &[1; 32],
            Config {
                iroh: Some(IrohConfig::default()),
                ..Config::default()
            },
        )
        .await
        .unwrap();

        assert!(namespace.iroh().is_some());
    }
}