openrtc 0.2.0

OpenRTC: a Rust-first P2P runtime for device discovery, signaling, and iroh/QUIC networking.
Documentation
#![cfg(not(target_arch = "wasm32"))]

use crate::client::{Client, EndpointHandle};
use crate::protocol_registry::{
    ProtocolPlugin, ProtocolPluginDescriptor, ProtocolPluginKind, ProtocolRegistry, RuntimeContext,
};
use anyhow::Result;
use iroh::{protocol::RouterBuilder, Endpoint};
use std::sync::Arc;

pub struct RuntimeManager {
    client: Arc<Client>,
    protocol_registry: Arc<ProtocolRegistry>,
}

impl RuntimeManager {
    /// RuntimeManager is the native-only extension seam for advanced protocol
    /// composition.
    ///
    /// Allowed:
    /// - own protocol plugin registration
    /// - add ALPNs before endpoint bind
    /// - notify plugins when the runtime endpoint is ready or adopted
    ///
    /// Not allowed:
    /// - become a second peer-session lifecycle engine
    /// - define app payload semantics
    ///
    /// Source of truth:
    /// - `Client` owns peer sessions, discovery, signaling, and settled readiness
    /// - `ProtocolRegistry` owns plugin declarations and runtime-ready callbacks
    pub fn new(client: Arc<Client>) -> Self {
        Self {
            client,
            protocol_registry: Arc::new(ProtocolRegistry::new()),
        }
    }

    pub fn with_registry(client: Arc<Client>, protocol_registry: Arc<ProtocolRegistry>) -> Self {
        Self {
            client,
            protocol_registry,
        }
    }

    pub fn protocol_registry(&self) -> Arc<ProtocolRegistry> {
        self.protocol_registry.clone()
    }

    pub fn client(&self) -> Arc<Client> {
        self.client.clone()
    }

    pub async fn register_protocol(&self, plugin: Arc<dyn ProtocolPlugin>) -> Result<()> {
        self.protocol_registry.register_checked(plugin).await?;
        if let Some(context) = self.current_context().await? {
            self.protocol_registry.notify_runtime_ready(context).await?;
        }
        Ok(())
    }

    pub async fn register_session_plugin(&self, plugin: Arc<dyn ProtocolPlugin>) -> Result<()> {
        if plugin.kind() != ProtocolPluginKind::SessionAdjacent {
            return Err(anyhow::anyhow!(
                "register_session_plugin requires a session-adjacent protocol plugin"
            ));
        }
        self.register_protocol(plugin).await
    }

    pub async fn register_alpn_protocol(&self, plugin: Arc<dyn ProtocolPlugin>) -> Result<()> {
        if plugin.kind() != ProtocolPluginKind::Alpn {
            return Err(anyhow::anyhow!(
                "register_alpn_protocol requires an ALPN protocol plugin"
            ));
        }
        self.register_protocol(plugin).await
    }

    pub async fn unregister_protocol(&self, name: &str) {
        self.protocol_registry.unregister(name).await;
    }

    pub async fn protocol_descriptors(&self) -> Vec<ProtocolPluginDescriptor> {
        self.protocol_registry.descriptors().await
    }

    pub async fn apply_registered_protocols(
        &self,
        builder: RouterBuilder,
    ) -> Result<RouterBuilder> {
        self.protocol_registry.apply_router_handlers(builder).await
    }

    pub async fn init_runtime(&self, secret_key: Option<Vec<u8>>) -> Result<String> {
        let extra_alpns = self.protocol_registry.extra_alpns().await;
        let node_id = self.client.init_iroh(secret_key, extra_alpns).await?;
        if let Some(context) = self.current_context().await? {
            self.protocol_registry.notify_runtime_ready(context).await?;
        }
        Ok(node_id)
    }

    pub async fn adopt_endpoint(&self, endpoint: Endpoint) -> Result<String> {
        let node_id = endpoint.id().to_string();
        self.client.adopt_endpoint(endpoint).await;
        if let Some(context) = self.current_context().await? {
            self.protocol_registry.notify_runtime_ready(context).await?;
        }
        Ok(node_id)
    }

    pub async fn current_node_id(&self) -> Option<String> {
        self.client.current_node_id().await
    }

    pub async fn get_endpoint(&self) -> Result<Endpoint> {
        self.client.get_endpoint().await
    }

    pub async fn export_endpoint_handle(&self) -> Result<EndpointHandle> {
        self.client.export_endpoint_handle().await
    }

    async fn current_context(&self) -> Result<Option<RuntimeContext>> {
        let endpoint = match self.client.get_endpoint().await {
            Ok(endpoint) => endpoint,
            Err(_) => return Ok(None),
        };

        let node_id = match self.client.current_node_id().await {
            Some(node_id) => node_id,
            None => return Ok(None),
        };

        Ok(Some(RuntimeContext { endpoint, node_id }))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::protocol_registry::ProtocolPlugin;
    use async_trait::async_trait;
    use std::sync::atomic::{AtomicUsize, Ordering};

    struct HookPlugin {
        ready_calls: Arc<AtomicUsize>,
    }

    #[async_trait]
    impl ProtocolPlugin for HookPlugin {
        fn name(&self) -> &'static str {
            "hook"
        }

        fn alpn(&self) -> Option<Vec<u8>> {
            Some(b"pluto/hook/1".to_vec())
        }

        async fn on_runtime_ready(&self, _context: RuntimeContext) -> Result<()> {
            self.ready_calls.fetch_add(1, Ordering::SeqCst);
            Ok(())
        }
    }

    #[tokio::test]
    async fn runtime_manager_notifies_protocols_on_endpoint_adoption() {
        let client = Arc::new(Client::new(
            crate::test_constants::TEST_PROJECT_ID.to_string(),
            crate::test_constants::TEST_API_KEY.to_string(),
            Box::new(|| None),
        ));
        let manager = RuntimeManager::new(client);

        let ready_calls = Arc::new(AtomicUsize::new(0));
        manager
            .register_protocol(Arc::new(HookPlugin {
                ready_calls: ready_calls.clone(),
            }))
            .await
            .expect("register protocol");

        let endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0)
            .bind()
            .await
            .expect("bind endpoint");
        manager
            .adopt_endpoint(endpoint)
            .await
            .expect("adopt endpoint");

        assert_eq!(ready_calls.load(Ordering::SeqCst), 1);
        assert!(manager.current_node_id().await.is_some());
    }

    struct SessionOnlyPlugin;

    #[async_trait]
    impl ProtocolPlugin for SessionOnlyPlugin {
        fn kind(&self) -> ProtocolPluginKind {
            ProtocolPluginKind::SessionAdjacent
        }

        fn name(&self) -> &'static str {
            "session-only"
        }

        fn alpn(&self) -> Option<Vec<u8>> {
            None
        }

        async fn on_runtime_ready(&self, _context: RuntimeContext) -> Result<()> {
            Ok(())
        }
    }

    #[tokio::test]
    async fn runtime_manager_rejects_wrong_plugin_category_for_session_registration() {
        let client = Arc::new(Client::new(
            crate::test_constants::TEST_PROJECT_ID.to_string(),
            crate::test_constants::TEST_API_KEY.to_string(),
            Box::new(|| None),
        ));
        let manager = RuntimeManager::new(client);

        let result = manager
            .register_session_plugin(Arc::new(HookPlugin {
                ready_calls: Arc::new(AtomicUsize::new(0)),
            }))
            .await;
        assert!(result.is_err());

        manager
            .register_session_plugin(Arc::new(SessionOnlyPlugin))
            .await
            .expect("register session plugin");
    }
}