use std::{error::Error, sync::Mutex}; use cfgsync_core::NodeRegistration; use serde_json::to_string; use crate::{ArtifactSet, NodeArtifactsCatalog, RegistrationSnapshot}; /// Type-erased cfgsync adapter error used to preserve source context. pub type DynCfgsyncError = Box; /// Adapter-side materialization contract for a single registered node. pub trait NodeArtifactsMaterializer: Send + Sync { fn materialize( &self, registration: &NodeRegistration, registrations: &RegistrationSnapshot, ) -> Result, DynCfgsyncError>; } /// Adapter contract for materializing a whole registration snapshot into /// per-node cfgsync artifacts. pub trait RegistrationSnapshotMaterializer: Send + Sync { fn materialize_snapshot( &self, registrations: &RegistrationSnapshot, ) -> Result, DynCfgsyncError>; } /// Snapshot materializer wrapper that caches the last materialized result. pub struct CachedSnapshotMaterializer { inner: M, cache: Mutex>, } struct CachedSnapshot { key: String, catalog: Option, } impl CachedSnapshotMaterializer { #[must_use] pub fn new(inner: M) -> Self { Self { inner, cache: Mutex::new(None), } } fn snapshot_key(registrations: &RegistrationSnapshot) -> Result { Ok(to_string(registrations)?) } } impl RegistrationSnapshotMaterializer for CachedSnapshotMaterializer where M: RegistrationSnapshotMaterializer, { fn materialize_snapshot( &self, registrations: &RegistrationSnapshot, ) -> Result, DynCfgsyncError> { let key = Self::snapshot_key(registrations)?; { let cache = self .cache .lock() .expect("cfgsync snapshot cache should not be poisoned"); if let Some(cached) = &*cache && cached.key == key { return Ok(cached.catalog.clone()); } } let catalog = self.inner.materialize_snapshot(registrations)?; let mut cache = self .cache .lock() .expect("cfgsync snapshot cache should not be poisoned"); *cache = Some(CachedSnapshot { key, catalog: catalog.clone(), }); Ok(catalog) } }