moonpool-sim 0.7.0

Simulation engine for the moonpool framework
//! Type-safe shared state for cross-workload mutable values.
//!
//! `StateHandle` provides an `Arc<RwLock<HashMap>>`-backed store using
//! `Box<dyn Any + Send + Sync>` so workloads can publish and consume typed
//! values across tasks within a simulation iteration. It is **not** an event
//! log — for event timelines and invariants see the
//! [`crate::observability`] module.
//!
//! # Usage
//!
//! ```ignore
//! use moonpool_sim::StateHandle;
//!
//! let state = StateHandle::new();
//! state.publish("counter", 42u64);
//! let val: Option<u64> = state.get("counter");
//! assert_eq!(val, Some(42));
//! ```

use std::any::Any;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};

/// Shared state handle for cross-workload publish/get communication.
///
/// Provides type-safe publish/get semantics using `std::any::Any` for type
/// erasure. Clone is cheap (Arc-based) — all clones share the same underlying
/// storage.
#[derive(Clone, Default)]
pub struct StateHandle {
    inner: Arc<RwLock<HashMap<String, Box<dyn Any + Send + Sync>>>>,
}

impl StateHandle {
    /// Create a new empty state handle.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Publish a value under a key, replacing any existing value.
    ///
    /// # Panics
    ///
    /// Panics if the state lock is poisoned by a prior task panic.
    pub fn publish<T: Any + Send + Sync + 'static>(&self, key: &str, value: T) {
        self.inner
            .write()
            .expect("RwLock poisoned: prior task panicked")
            .insert(key.to_string(), Box::new(value));
    }

    /// Get a cloned copy of the value under a key, if it exists and matches the type.
    ///
    /// Returns `None` if the key doesn't exist or the type doesn't match.
    ///
    /// # Panics
    ///
    /// Panics if the state lock is poisoned by a prior task panic.
    #[must_use]
    pub fn get<T: Any + Clone + Send + Sync + 'static>(&self, key: &str) -> Option<T> {
        self.inner
            .read()
            .expect("RwLock poisoned: prior task panicked")
            .get(key)
            .and_then(|v| v.downcast_ref::<T>())
            .cloned()
    }

    /// Check whether a key exists in the state.
    ///
    /// # Panics
    ///
    /// Panics if the state lock is poisoned by a prior task panic.
    #[must_use]
    pub fn contains(&self, key: &str) -> bool {
        self.inner
            .read()
            .expect("RwLock poisoned: prior task panicked")
            .contains_key(key)
    }
}

impl std::fmt::Debug for StateHandle {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let state_count = self
            .inner
            .read()
            .expect("RwLock poisoned: prior task panicked")
            .len();
        f.debug_struct("StateHandle")
            .field("state_keys", &state_count)
            .finish()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_publish_and_get() {
        let state = StateHandle::new();
        state.publish("count", 42u64);
        assert_eq!(state.get::<u64>("count"), Some(42));
    }

    #[test]
    fn test_get_wrong_type_returns_none() {
        let state = StateHandle::new();
        state.publish("count", 42u64);
        assert_eq!(state.get::<String>("count"), None);
    }

    #[test]
    fn test_get_missing_key_returns_none() {
        let state = StateHandle::new();
        assert_eq!(state.get::<u64>("missing"), None);
    }

    #[test]
    fn test_contains() {
        let state = StateHandle::new();
        assert!(!state.contains("key"));
        state.publish("key", "value".to_string());
        assert!(state.contains("key"));
    }

    #[test]
    fn test_publish_replaces() {
        let state = StateHandle::new();
        state.publish("x", 1u64);
        state.publish("x", 2u64);
        assert_eq!(state.get::<u64>("x"), Some(2));
    }

    #[test]
    fn test_clone_shares_state() {
        let state = StateHandle::new();
        let clone = state.clone();
        state.publish("shared", true);
        assert_eq!(clone.get::<bool>("shared"), Some(true));
    }
}