mecha10-core 0.8.0

Mecha10 core runtime and foundational types
Documentation
//! Conditional publishing for declarative message publishing
//!
//! This module provides a fluent API for conditional and throttled publishing.
//!
//! # Examples
//!
//! ## Conditional Publishing
//!
//! ```rust
//! use mecha10_core::prelude::*;
//! use mecha10_core::publish::ContextPublishExt;
//! use mecha10_core::messages::Detection;
//! use mecha10_core::topics::perception;
//!
//! # async fn example(ctx: &Context, detections: Vec<Detection>) -> Result<()> {
//! // Only publish when detections are non-empty
//! ctx.publish_with(perception::DETECTIONS, &detections)
//!     .when(|d| !d.is_empty())
//!     .await?;
//! # Ok(())
//! # }
//! ```
//!
//! ## Throttled Publishing
//!
//! ```rust
//! use mecha10_core::prelude::*;
//! use mecha10_core::publish::ContextPublishExt;
//! use mecha10_core::messages::NodeStatus;
//! use mecha10_core::topics::system;
//!
//! # async fn example(ctx: &Context, status: NodeStatus) -> Result<()> {
//! // Publish at most once per second
//! ctx.publish_with(system::NODE_STATUS, &status)
//!     .throttle(Duration::from_secs(1))
//!     .await?;
//! # Ok(())
//! # }
//! ```

use crate::context::Context;
use crate::error::Result;
use crate::messages::Message;
use crate::topics::Topic;
use serde::Serialize;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;

static THROTTLE_STATE: once_cell::sync::Lazy<Arc<Mutex<HashMap<String, Instant>>>> =
    once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(HashMap::new())));

pub struct PublishBuilder<'a, 'b, T: Message + Serialize> {
    ctx: &'a Context,
    topic: Topic<T>,
    message: &'b T,
    #[allow(clippy::type_complexity)]
    condition: Option<Box<dyn Fn(&T) -> bool + Send + Sync>>,
    throttle_duration: Option<Duration>,
}

impl<'a, 'b, T: Message + Serialize> PublishBuilder<'a, 'b, T> {
    /// Create a new publish builder
    pub fn new(ctx: &'a Context, topic: Topic<T>, message: &'b T) -> Self {
        Self {
            ctx,
            topic,
            message,
            condition: None,
            throttle_duration: None,
        }
    }

    crate::builder_setter!(
        /// Only publish if condition is true
        ///
        /// If the predicate returns `false`, the message will not be published.
        ///
        /// # Example
        ///
        /// ```rust
        /// # use mecha10_core::prelude::*;
        /// # use mecha10_core::publish::ContextPublishExt;
        /// # use mecha10_core::messages::Detection;
        /// # use mecha10_core::topics::perception;
        /// # async fn example(ctx: &Context, detections: Vec<Detection>) -> Result<()> {
        /// ctx.publish_with(perception::DETECTIONS, &detections)
        ///     .when(|d| !d.is_empty())
        ///     .await?;
        /// # Ok(())
        /// # }
        /// ```
        predicate, when, condition, Fn(&T) -> bool + Send + Sync + 'static
    );

    crate::builder_setter!(
        /// Throttle publishing to a maximum rate
        ///
        /// Messages will be rate-limited to at most one per `duration`.
        /// If you try to publish more frequently, the message will be dropped.
        ///
        /// # Example
        ///
        /// ```rust
        /// # use mecha10_core::prelude::*;
        /// # use mecha10_core::publish::ContextPublishExt;
        /// # use mecha10_core::messages::NodeStatus;
        /// # use mecha10_core::topics::system;
        /// # async fn example(ctx: &Context, status: NodeStatus) -> Result<()> {
        /// // Max once per second
        /// ctx.publish_with(system::NODE_STATUS, &status)
        ///     .throttle(Duration::from_secs(1))
        ///     .await?;
        /// # Ok(())
        /// # }
        /// ```
        into_some, throttle, throttle_duration, Duration
    );

    /// Execute the publish with all configured conditions
    ///
    /// This method applies all the configured filters and throttling,
    /// then publishes the message if all conditions are met.
    pub async fn execute(self) -> Result<()> {
        if let Some(ref condition) = self.condition {
            if !condition(self.message) {
                return Ok(());
            }
        }

        if let Some(throttle_dur) = self.throttle_duration {
            let topic_key = format!("{}-{}", self.ctx.node_id(), self.topic.path());

            let mut throttle_state = THROTTLE_STATE.lock().await;

            if let Some(last_time) = throttle_state.get(&topic_key) {
                let elapsed = last_time.elapsed();
                if elapsed < throttle_dur {
                    return Ok(());
                }
            }

            throttle_state.insert(topic_key, Instant::now());
        }

        self.ctx.publish_to(self.topic, self.message).await
    }
}

pub trait ContextPublishExt {
    /// Start building a conditional publish operation
    fn publish_with<'a, 'b, T>(&'a self, topic: Topic<T>, message: &'b T) -> PublishBuilder<'a, 'b, T>
    where
        T: Message + Serialize;
}

impl ContextPublishExt for Context {
    fn publish_with<'a, 'b, T>(&'a self, topic: Topic<T>, message: &'b T) -> PublishBuilder<'a, 'b, T>
    where
        T: Message + Serialize,
    {
        PublishBuilder::new(self, topic, message)
    }
}

impl<'a, 'b, T: Message + Serialize> std::future::IntoFuture for PublishBuilder<'a, 'b, T>
where
    'b: 'a,
{
    type Output = Result<()>;
    type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>>;

    fn into_future(self) -> Self::IntoFuture {
        Box::pin(self.execute())
    }
}