use std::sync::OnceLock;
pub trait EventEmitter: Send + Sync + 'static {
fn emit(&self, event: &str, payload: serde_json::Value);
}
pub struct NoOpEmitter;
impl EventEmitter for NoOpEmitter {
fn emit(&self, _event: &str, _payload: serde_json::Value) {}
}
static EVENT_EMITTER: OnceLock<Box<dyn EventEmitter>> = OnceLock::new();
pub fn set_event_emitter(emitter: Box<dyn EventEmitter>) {
let _ = EVENT_EMITTER.set(emitter);
}
pub fn emit_event<T: serde::Serialize>(event: &str, payload: &T) {
if let Some(emitter) = EVENT_EMITTER.get() {
if let Ok(value) = serde_json::to_value(payload) {
emitter.emit(event, value);
}
}
}
pub fn emit_event_json(event: &str, payload: serde_json::Value) {
if let Some(emitter) = EVENT_EMITTER.get() {
emitter.emit(event, payload);
}
}
pub fn has_event_emitter() -> bool {
EVENT_EMITTER.get().is_some()
}
pub trait SubscriptionRefresher: Send + Sync + 'static {
fn refresh(&self);
}
pub struct NoOpSubscriptionRefresher;
impl SubscriptionRefresher for NoOpSubscriptionRefresher {
fn refresh(&self) {}
}
static SUBSCRIPTION_REFRESHER: OnceLock<Box<dyn SubscriptionRefresher>> = OnceLock::new();
pub fn set_subscription_refresher(refresher: Box<dyn SubscriptionRefresher>) {
let _ = SUBSCRIPTION_REFRESHER.set(refresher);
}
pub fn refresh_subscriptions() {
if let Some(r) = SUBSCRIPTION_REFRESHER.get() {
r.refresh();
}
}
pub trait ProgressReporter: Send + Sync {
fn report_progress(&self, percentage: Option<u8>, bytes: Option<u64>, bytes_per_sec: Option<f64>) -> Result<(), &'static str>;
fn report_complete(&self) -> Result<(), &'static str>;
}
pub struct NoOpProgressReporter;
impl ProgressReporter for NoOpProgressReporter {
fn report_progress(&self, _: Option<u8>, _: Option<u64>, _: Option<f64>) -> Result<(), &'static str> { Ok(()) }
fn report_complete(&self) -> Result<(), &'static str> { Ok(()) }
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
static REFRESH_CALLS: AtomicUsize = AtomicUsize::new(0);
static REFRESH_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
struct CountingRefresher;
impl SubscriptionRefresher for CountingRefresher {
fn refresh(&self) {
REFRESH_CALLS.fetch_add(1, Ordering::Relaxed);
}
}
fn refresh_test_setup() -> std::sync::MutexGuard<'static, ()> {
let guard = REFRESH_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
set_subscription_refresher(Box::new(CountingRefresher));
REFRESH_CALLS.store(0, Ordering::Relaxed);
guard
}
#[test]
fn refresh_subscriptions_invokes_registered_impl_once() {
let _g = refresh_test_setup();
assert_eq!(REFRESH_CALLS.load(Ordering::Relaxed), 0);
refresh_subscriptions();
assert_eq!(REFRESH_CALLS.load(Ordering::Relaxed), 1);
}
#[test]
fn refresh_subscriptions_invokes_registered_impl_per_call() {
let _g = refresh_test_setup();
for _ in 0..5 {
refresh_subscriptions();
}
assert_eq!(REFRESH_CALLS.load(Ordering::Relaxed), 5);
}
#[test]
fn set_subscription_refresher_is_idempotent() {
let _g = refresh_test_setup();
struct PanicRefresher;
impl SubscriptionRefresher for PanicRefresher {
fn refresh(&self) {
panic!("PanicRefresher::refresh() should never be reached — OnceLock should have ignored the second set_subscription_refresher call");
}
}
set_subscription_refresher(Box::new(PanicRefresher));
refresh_subscriptions();
assert_eq!(REFRESH_CALLS.load(Ordering::Relaxed), 1);
}
#[test]
fn refresh_subscriptions_is_thread_safe() {
let _g = refresh_test_setup();
let handles: Vec<_> = (0..10).map(|_| {
std::thread::spawn(|| {
for _ in 0..100 {
refresh_subscriptions();
}
})
}).collect();
for h in handles { h.join().unwrap(); }
assert_eq!(REFRESH_CALLS.load(Ordering::Relaxed), 1_000);
}
#[test]
fn no_op_subscription_refresher_returns_quietly() {
let _g = refresh_test_setup();
let initial = REFRESH_CALLS.load(Ordering::Relaxed);
let r = NoOpSubscriptionRefresher;
r.refresh();
r.refresh();
assert_eq!(REFRESH_CALLS.load(Ordering::Relaxed), initial);
}
#[test]
fn no_op_emitter_does_not_panic() {
let e = NoOpEmitter;
e.emit("test_event", serde_json::json!({"k": "v"}));
}
#[test]
fn emit_event_when_unregistered_is_silent() {
crate::traits::emit_event("test_event", &serde_json::json!({"k": "v"}));
}
#[test]
fn emit_event_json_when_unregistered_is_silent() {
crate::traits::emit_event_json("test_event", serde_json::json!({"k": "v"}));
}
#[test]
fn no_op_progress_reporter_returns_ok() {
let r = NoOpProgressReporter;
assert!(r.report_progress(Some(50), Some(1024), Some(100.0)).is_ok());
assert!(r.report_complete().is_ok());
}
}