pub mod config;
pub mod handlers;
pub use config::ZenohConfig;
use std::sync::Arc;
use tokio::sync::watch;
use tracing::{debug, error, info, warn};
use zenoh::Session;
use crate::handlers::state::MultiUserMemoryManager;
use crate::streaming::StreamMessage;
use config::{AutoTopic, PayloadMode};
pub struct ZenohTransportHandle {
shutdown_tx: watch::Sender<bool>,
session: Session,
}
impl ZenohTransportHandle {
pub async fn shutdown(self) {
let _ = self.shutdown_tx.send(true);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
if let Err(e) = self.session.close().await {
error!("Error closing Zenoh session: {}", e);
}
info!("Zenoh transport shut down");
}
}
pub async fn start(
manager: Arc<MultiUserMemoryManager>,
config: ZenohConfig,
) -> anyhow::Result<ZenohTransportHandle> {
config.validate();
let mut zenoh_config = zenoh::Config::default();
match config.mode {
config::ZenohMode::Peer => {
zenoh_config
.insert_json5("mode", r#""peer""#)
.map_err(|e| anyhow::anyhow!("Failed to set Zenoh mode: {e}"))?;
}
config::ZenohMode::Client => {
zenoh_config
.insert_json5("mode", r#""client""#)
.map_err(|e| anyhow::anyhow!("Failed to set Zenoh mode: {e}"))?;
}
config::ZenohMode::Router => {
zenoh_config
.insert_json5("mode", r#""router""#)
.map_err(|e| anyhow::anyhow!("Failed to set Zenoh mode: {e}"))?;
}
}
if !config.connect.is_empty() {
let endpoints_json = serde_json::to_string(&config.connect)?;
zenoh_config
.insert_json5("connect/endpoints", &endpoints_json)
.map_err(|e| anyhow::anyhow!("Failed to set connect endpoints: {e}"))?;
}
if !config.listen.is_empty() {
let endpoints_json = serde_json::to_string(&config.listen)?;
zenoh_config
.insert_json5("listen/endpoints", &endpoints_json)
.map_err(|e| anyhow::anyhow!("Failed to set listen endpoints: {e}"))?;
}
let session = zenoh::open(zenoh_config)
.await
.map_err(|e| anyhow::anyhow!("Failed to open Zenoh session: {e}"))?;
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let prefix = &config.prefix;
let api_key = &config.api_key;
register_remember_subscriber(
&session,
prefix,
&manager,
api_key.clone(),
shutdown_rx.clone(),
)
.await?;
register_recall_queryable(
&session,
prefix,
&manager,
api_key.clone(),
shutdown_rx.clone(),
)
.await?;
register_forget_subscriber(
&session,
prefix,
&manager,
api_key.clone(),
shutdown_rx.clone(),
)
.await?;
register_stream_subscribers(
&session,
prefix,
&manager,
api_key.clone(),
shutdown_rx.clone(),
)
.await?;
register_health_queryable(&session, prefix, shutdown_rx.clone()).await?;
register_mission_subscribers(
&session,
prefix,
&manager,
api_key.clone(),
shutdown_rx.clone(),
)
.await?;
register_fleet_discovery(&session, prefix, shutdown_rx.clone()).await?;
for topic in &config.auto_topics {
if topic.key_expr.is_empty() || topic.user_id.is_empty() {
warn!(
key_expr = %topic.key_expr,
user_id = %topic.user_id,
"Skipping auto-topic with empty key_expr or user_id"
);
continue;
}
register_auto_topic(&session, topic, &manager, shutdown_rx.clone()).await?;
}
let auto_count = config
.auto_topics
.iter()
.filter(|t| !t.key_expr.is_empty() && !t.user_id.is_empty())
.count();
info!(
mode = %config.mode.as_str(),
prefix = %prefix,
auto_topics = auto_count,
"Zenoh transport started"
);
Ok(ZenohTransportHandle {
shutdown_tx,
session,
})
}
async fn register_remember_subscriber(
session: &Session,
prefix: &str,
manager: &Arc<MultiUserMemoryManager>,
api_key: Option<String>,
mut shutdown_rx: watch::Receiver<bool>,
) -> anyhow::Result<()> {
let key_expr = format!("{}/*/remember", prefix);
let subscriber = session
.declare_subscriber(&key_expr)
.await
.map_err(|e| anyhow::anyhow!("Failed to declare remember subscriber: {e}"))?;
let mgr = Arc::clone(manager);
tokio::spawn(async move {
loop {
tokio::select! {
sample = subscriber.recv_async() => {
match sample {
Ok(sample) => {
if !handlers::authenticate_payload(sample.payload(), api_key.as_deref()) {
continue;
}
let mgr = Arc::clone(&mgr);
tokio::spawn(async move {
handlers::handle_remember(sample, mgr).await;
});
}
Err(e) => {
error!("Remember subscriber channel closed: {}", e);
break;
}
}
}
_ = shutdown_rx.changed() => {
debug!("Remember subscriber shutting down");
break;
}
}
}
});
debug!(key_expr = %key_expr, "Remember subscriber registered");
Ok(())
}
async fn register_forget_subscriber(
session: &Session,
prefix: &str,
manager: &Arc<MultiUserMemoryManager>,
api_key: Option<String>,
mut shutdown_rx: watch::Receiver<bool>,
) -> anyhow::Result<()> {
let key_expr = format!("{}/*/forget", prefix);
let subscriber = session
.declare_subscriber(&key_expr)
.await
.map_err(|e| anyhow::anyhow!("Failed to declare forget subscriber: {e}"))?;
let mgr = Arc::clone(manager);
let prefix_owned = prefix.to_string();
tokio::spawn(async move {
loop {
tokio::select! {
sample = subscriber.recv_async() => {
match sample {
Ok(sample) => {
if !handlers::authenticate_payload(sample.payload(), api_key.as_deref()) {
continue;
}
let mgr = Arc::clone(&mgr);
let pfx = prefix_owned.clone();
tokio::spawn(async move {
handlers::handle_forget(sample, mgr, &pfx).await;
});
}
Err(e) => {
error!("Forget subscriber channel closed: {}", e);
break;
}
}
}
_ = shutdown_rx.changed() => {
debug!("Forget subscriber shutting down");
break;
}
}
}
});
debug!(key_expr = %key_expr, "Forget subscriber registered");
Ok(())
}
async fn register_stream_subscribers(
session: &Session,
prefix: &str,
manager: &Arc<MultiUserMemoryManager>,
api_key: Option<String>,
mut shutdown_rx: watch::Receiver<bool>,
) -> anyhow::Result<()> {
let key_expr = format!("{}/*/stream/**", prefix);
let subscriber = session
.declare_subscriber(&key_expr)
.await
.map_err(|e| anyhow::anyhow!("Failed to declare stream subscriber: {e}"))?;
let mgr = Arc::clone(manager);
let pfx = prefix.to_string();
let sessions: Arc<tokio::sync::RwLock<std::collections::HashMap<String, String>>> =
Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new()));
tokio::spawn(async move {
loop {
tokio::select! {
sample = subscriber.recv_async() => {
match sample {
Ok(sample) => {
if !handlers::authenticate_payload(sample.payload(), api_key.as_deref()) {
continue;
}
let key = sample.key_expr().as_str();
let user_id = match handlers::extract_user_id(key, &pfx) {
Some(uid) => uid.to_string(),
None => {
warn!(key = %key, "Cannot extract user_id from stream key");
continue;
}
};
let mode = if key.contains("/stream/sensor") {
crate::streaming::StreamMode::Sensor
} else if key.contains("/stream/event") {
crate::streaming::StreamMode::Event
} else {
crate::streaming::StreamMode::Conversation
};
let session_id = {
let sessions_read = sessions.read().await;
sessions_read.get(&user_id).cloned()
};
let session_id = match session_id {
Some(id) => id,
None => {
match handlers::create_stream_session(
&user_id,
mode,
crate::streaming::ExtractionConfig::default(),
Vec::new(),
&mgr,
)
.await
{
Ok(id) => {
sessions.write().await.insert(user_id.clone(), id.clone());
debug!(user_id = %user_id, session_id = %id, "Created stream session for Zenoh subscriber");
id
}
Err(e) => {
error!(user_id = %user_id, "Failed to create stream session: {}", e);
continue;
}
}
}
};
let payload_str = match sample.payload().try_to_string() {
Ok(s) => s.into_owned(),
Err(e) => {
warn!("Invalid UTF-8 in stream payload: {}", e);
continue;
}
};
let msg: StreamMessage = match serde_json::from_str(&payload_str) {
Ok(m) => m,
Err(_) => {
handlers::wrap_passthrough(&payload_str, &[])
}
};
let memory = match mgr.get_user_memory(&user_id) {
Ok(m) => m,
Err(e) => {
error!(user_id = %user_id, "Failed to get user memory: {}", e);
continue;
}
};
let mgr_clone = Arc::clone(&mgr);
let sid = session_id.clone();
tokio::spawn(async move {
handlers::handle_stream_message(msg, &sid, memory, &mgr_clone).await;
});
}
Err(e) => {
error!("Stream subscriber channel closed: {}", e);
break;
}
}
}
_ = shutdown_rx.changed() => {
debug!("Stream subscriber shutting down");
let sessions = sessions.read().await;
for (user_id, session_id) in sessions.iter() {
if let Some(total) = mgr.streaming_extractor().close_session(session_id).await {
debug!(user_id = %user_id, total_memories = total, "Closed stream session");
}
}
break;
}
}
}
});
debug!(key_expr = %key_expr, "Stream subscriber registered");
Ok(())
}
async fn register_recall_queryable(
session: &Session,
prefix: &str,
manager: &Arc<MultiUserMemoryManager>,
api_key: Option<String>,
mut shutdown_rx: watch::Receiver<bool>,
) -> anyhow::Result<()> {
let key_expr = format!("{}/*/recall", prefix);
let queryable = session
.declare_queryable(&key_expr)
.await
.map_err(|e| anyhow::anyhow!("Failed to declare recall queryable: {e}"))?;
let mgr = Arc::clone(manager);
tokio::spawn(async move {
loop {
tokio::select! {
query = queryable.recv_async() => {
match query {
Ok(query) => {
match query.payload() {
Some(payload) => {
if !handlers::authenticate_payload(payload, api_key.as_deref()) {
let err = serde_json::json!({"error": "Unauthorized", "success": false});
if let Ok(bytes) = serde_json::to_vec(&err) {
let _ = query.reply(query.key_expr(), bytes).await;
}
continue;
}
}
None if api_key.is_some() => {
let err = serde_json::json!({"error": "Unauthorized: payload required when API key is configured", "success": false});
if let Ok(bytes) = serde_json::to_vec(&err) {
let _ = query.reply(query.key_expr(), bytes).await;
}
continue;
}
None => {} }
let mgr = Arc::clone(&mgr);
tokio::spawn(async move {
handlers::handle_recall(query, mgr).await;
});
}
Err(e) => {
error!("Recall queryable channel closed: {}", e);
break;
}
}
}
_ = shutdown_rx.changed() => {
debug!("Recall queryable shutting down");
break;
}
}
}
});
debug!(key_expr = %key_expr, "Recall queryable registered");
Ok(())
}
async fn register_health_queryable(
session: &Session,
prefix: &str,
mut shutdown_rx: watch::Receiver<bool>,
) -> anyhow::Result<()> {
let key_expr = format!("{}/health", prefix);
let queryable = session
.declare_queryable(&key_expr)
.await
.map_err(|e| anyhow::anyhow!("Failed to declare health queryable: {e}"))?;
let ke = key_expr.clone();
tokio::spawn(async move {
loop {
tokio::select! {
query = queryable.recv_async() => {
match query {
Ok(query) => {
let response = handlers::build_health_response();
if let Ok(bytes) = serde_json::to_vec(&response) {
if let Err(e) = query.reply(&ke, bytes).await {
error!("Failed to reply to health query: {}", e);
}
}
}
Err(e) => {
error!("Health queryable channel closed: {}", e);
break;
}
}
}
_ = shutdown_rx.changed() => {
debug!("Health queryable shutting down");
break;
}
}
}
});
debug!(key_expr = %key_expr, "Health queryable registered");
Ok(())
}
async fn register_mission_subscribers(
session: &Session,
prefix: &str,
manager: &Arc<MultiUserMemoryManager>,
api_key: Option<String>,
mut shutdown_rx: watch::Receiver<bool>,
) -> anyhow::Result<()> {
let start_key = format!("{}/*/mission/start", prefix);
let start_sub = session
.declare_subscriber(&start_key)
.await
.map_err(|e| anyhow::anyhow!("Failed to declare mission start subscriber: {e}"))?;
let mgr_start = Arc::clone(manager);
let mut shutdown_start = shutdown_rx.clone();
let api_key_start = api_key.clone();
tokio::spawn(async move {
loop {
tokio::select! {
sample = start_sub.recv_async() => {
match sample {
Ok(sample) => {
if !handlers::authenticate_payload(sample.payload(), api_key_start.as_deref()) {
continue;
}
let mgr = Arc::clone(&mgr_start);
tokio::spawn(async move {
handlers::handle_mission_start(sample, mgr).await;
});
}
Err(e) => {
error!("Mission start subscriber channel closed: {}", e);
break;
}
}
}
_ = shutdown_start.changed() => {
debug!("Mission start subscriber shutting down");
break;
}
}
}
});
debug!(key_expr = %start_key, "Mission start subscriber registered");
let end_key = format!("{}/*/mission/end", prefix);
let end_sub = session
.declare_subscriber(&end_key)
.await
.map_err(|e| anyhow::anyhow!("Failed to declare mission end subscriber: {e}"))?;
let mgr_end = Arc::clone(manager);
tokio::spawn(async move {
loop {
tokio::select! {
sample = end_sub.recv_async() => {
match sample {
Ok(sample) => {
if !handlers::authenticate_payload(sample.payload(), api_key.as_deref()) {
continue;
}
let mgr = Arc::clone(&mgr_end);
tokio::spawn(async move {
handlers::handle_mission_end(sample, mgr).await;
});
}
Err(e) => {
error!("Mission end subscriber channel closed: {}", e);
break;
}
}
}
_ = shutdown_rx.changed() => {
debug!("Mission end subscriber shutting down");
break;
}
}
}
});
debug!(key_expr = %end_key, "Mission end subscriber registered");
Ok(())
}
async fn register_fleet_discovery(
session: &Session,
prefix: &str,
shutdown_rx: watch::Receiver<bool>,
) -> anyhow::Result<()> {
let liveliness_key = format!("{}/fleet/shodh-server", prefix);
let _token = session
.liveliness()
.declare_token(&liveliness_key)
.await
.map_err(|e| anyhow::anyhow!("Failed to declare liveliness token: {e}"))?;
info!(key_expr = %liveliness_key, "Fleet liveliness token declared");
let fleet_key = format!("{}/fleet/**", prefix);
let fleet_sub = session
.liveliness()
.declare_subscriber(&fleet_key)
.await
.map_err(|e| anyhow::anyhow!("Failed to declare fleet liveliness subscriber: {e}"))?;
let peers: Arc<tokio::sync::RwLock<std::collections::HashMap<String, handlers::FleetPeer>>> =
Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new()));
let peers_for_task = peers.clone();
let pfx = prefix.to_string();
let mut shutdown_fleet_sub = shutdown_rx.clone();
let mut shutdown_fleet_query = shutdown_rx;
tokio::spawn(async move {
loop {
tokio::select! {
sample = fleet_sub.recv_async() => {
match sample {
Ok(sample) => {
let key = sample.key_expr().as_str().to_string();
let robot_id = key
.strip_prefix(&pfx)
.and_then(|rest| rest.strip_prefix("/fleet/"))
.unwrap_or(&key)
.to_string();
match sample.kind() {
zenoh::sample::SampleKind::Put => {
info!(robot_id = %robot_id, "Fleet peer joined");
peers_for_task.write().await.insert(
robot_id,
handlers::FleetPeer {
key_expr: key,
joined_at: chrono::Utc::now(),
},
);
}
zenoh::sample::SampleKind::Delete => {
info!(robot_id = %robot_id, "Fleet peer left");
peers_for_task.write().await.remove(&robot_id);
}
}
}
Err(e) => {
error!("Fleet liveliness subscriber channel closed: {}", e);
break;
}
}
}
_ = shutdown_fleet_sub.changed() => {
debug!("Fleet liveliness subscriber shutting down");
break;
}
}
}
});
let fleet_query_key = format!("{}/fleet", prefix);
let fleet_queryable = session
.declare_queryable(&fleet_query_key)
.await
.map_err(|e| anyhow::anyhow!("Failed to declare fleet queryable: {e}"))?;
let fleet_ke = fleet_query_key.clone();
let peers_for_query = peers;
tokio::spawn(async move {
loop {
tokio::select! {
query = fleet_queryable.recv_async() => {
match query {
Ok(query) => {
let roster = peers_for_query.read().await;
let response = handlers::build_fleet_response(&roster);
if let Ok(bytes) = serde_json::to_vec(&response) {
if let Err(e) = query.reply(&fleet_ke, bytes).await {
error!("Failed to reply to fleet query: {}", e);
}
}
}
Err(e) => {
error!("Fleet queryable channel closed: {}", e);
break;
}
}
}
_ = shutdown_fleet_query.changed() => {
debug!("Fleet queryable shutting down");
break;
}
}
}
});
debug!(key_expr = %fleet_query_key, "Fleet discovery registered");
Ok(())
}
async fn register_auto_topic(
session: &Session,
topic: &AutoTopic,
manager: &Arc<MultiUserMemoryManager>,
mut shutdown_rx: watch::Receiver<bool>,
) -> anyhow::Result<()> {
let subscriber = session
.declare_subscriber(&topic.key_expr)
.await
.map_err(|e| {
anyhow::anyhow!(
"Failed to declare auto-topic subscriber for '{}': {e}",
topic.key_expr
)
})?;
let mut extraction_config = topic.extraction_config.clone();
extraction_config.validate_and_clamp();
let session_id = handlers::create_stream_session(
&topic.user_id,
topic.mode,
extraction_config,
topic.tags.clone(),
manager,
)
.await
.map_err(|e| anyhow::anyhow!("Failed to create stream session for auto-topic: {e}"))?;
let memory = manager
.get_user_memory(&topic.user_id)
.map_err(|e| anyhow::anyhow!("Failed to get user memory for auto-topic: {e}"))?;
let mgr = Arc::clone(manager);
let user_id = topic.user_id.clone();
let key_expr = topic.key_expr.clone();
let payload_mode = topic.payload_mode;
let tags = topic.tags.clone();
let sid = session_id.clone();
tokio::spawn(async move {
loop {
tokio::select! {
sample = subscriber.recv_async() => {
match sample {
Ok(sample) => {
let payload_str = match sample.payload().try_to_string() {
Ok(s) => s.into_owned(),
Err(e) => {
warn!(key_expr = %key_expr, "Invalid UTF-8 in auto-topic payload: {}", e);
continue;
}
};
let msg = match payload_mode {
PayloadMode::Passthrough => {
handlers::wrap_passthrough(&payload_str, &tags)
}
PayloadMode::Structured => {
match serde_json::from_str::<StreamMessage>(&payload_str) {
Ok(m) => m,
Err(e) => {
warn!(
key_expr = %key_expr,
"Failed to parse structured payload, falling back to passthrough: {}",
e
);
handlers::wrap_passthrough(&payload_str, &tags)
}
}
}
};
let mem = memory.clone();
let mgr_clone = Arc::clone(&mgr);
let sid_clone = sid.clone();
tokio::spawn(async move {
handlers::handle_stream_message(msg, &sid_clone, mem, &mgr_clone).await;
});
}
Err(e) => {
error!(
key_expr = %key_expr,
"Auto-topic subscriber channel closed: {}", e
);
break;
}
}
}
_ = shutdown_rx.changed() => {
debug!(key_expr = %key_expr, "Auto-topic subscriber shutting down");
if let Some(total) = mgr.streaming_extractor().close_session(&sid).await {
info!(
key_expr = %key_expr,
user_id = %user_id,
total_memories = total,
"Auto-topic session closed"
);
}
break;
}
}
}
});
info!(
key_expr = %topic.key_expr,
user_id = %topic.user_id,
mode = ?topic.mode,
payload_mode = ?topic.payload_mode,
"Auto-topic subscriber registered"
);
Ok(())
}