//! Gossip broadcast trait for distributed replication. //! //! This module defines the `GossipBroadcast` trait that allows the IngestWorker //! to broadcast newly ingested assertions to peer nodes. //! //! # Design //! //! The trait is defined here in stemedb-ingest to avoid a cyclic dependency: //! - stemedb-ingest needs the trait for IngestWorker //! - stemedb-sync implements the trait (and depends on stemedb-ingest would cause cycle) //! //! By defining the trait here, stemedb-sync can implement it without the cycle. use async_trait::async_trait; use stemedb_core::types::HlcTimestamp; use thiserror::Error; /// Error type for gossip operations. #[derive(Debug, Error)] pub enum GossipError { /// Network error during broadcast. #[error("Network error: {0}")] Network(String), /// Serialization error. #[error("Serialization error: {0}")] Serialization(String), /// All peers failed to receive the message. #[error("All peers failed")] AllPeersFailed, } /// Trait for broadcasting assertions to peer nodes. /// /// Implementations should be: /// - **Non-blocking**: Don't wait for all peers to acknowledge /// - **Best-effort**: Log failures but don't block the ingestion pipeline /// - **Idempotent-friendly**: Receivers handle duplicates gracefully /// /// # Example /// /// ```ignore /// use stemedb_ingest::gossip::GossipBroadcast; /// /// struct MyBroadcaster { /* ... */ } /// /// #[async_trait] /// impl GossipBroadcast for MyBroadcaster { /// async fn broadcast(&self, hash: &[u8; 32], data: &[u8], hlc: &HlcTimestamp) -> Result<(), GossipError> { /// // Send to peers... /// Ok(()) /// } /// /// fn is_enabled(&self) -> bool { true } /// fn enable(&self) {} /// fn disable(&self) {} /// } /// ``` #[async_trait] pub trait GossipBroadcast: Send + Sync { /// Broadcast an assertion to peer nodes. /// /// # Arguments /// /// * `hash` - BLAKE3 hash of the assertion (32 bytes) /// * `data` - Serialized assertion data (rkyv format) /// * `hlc` - HLC timestamp for causal ordering /// /// # Returns /// /// `Ok(())` if at least one peer received the message, or if no peers /// are configured. The method should not fail the ingestion pipeline. async fn broadcast( &self, hash: &[u8; 32], data: &[u8], hlc: &HlcTimestamp, ) -> Result<(), GossipError>; /// Check if broadcasting is currently enabled. fn is_enabled(&self) -> bool; /// Enable broadcasting. fn enable(&self); /// Disable broadcasting (e.g., for testing or during recovery). fn disable(&self); } /// A no-op implementation for single-node deployments or testing. pub struct NoOpGossipBroadcast; #[async_trait] impl GossipBroadcast for NoOpGossipBroadcast { async fn broadcast( &self, _hash: &[u8; 32], _data: &[u8], _hlc: &HlcTimestamp, ) -> Result<(), GossipError> { // Do nothing Ok(()) } fn is_enabled(&self) -> bool { false } fn enable(&self) {} fn disable(&self) {} } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_noop_broadcast() { let broadcaster = NoOpGossipBroadcast; let hash = [1u8; 32]; let data = vec![1, 2, 3]; let hlc = HlcTimestamp::new(1000, [1u8; 16]); // Should always succeed broadcaster.broadcast(&hash, &data, &hlc).await.expect("broadcast"); assert!(!broadcaster.is_enabled()); } }