#![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 {
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");
}
}