use std::sync::OnceLock;
pub trait EventEmitter: Send + Sync + 'static {
fn emit(&self, event: &str, payload: serde_json::Value);
fn emit_json(&self, event: &str, payload: &serde_json::value::RawValue) {
if let Ok(value) = serde_json::from_str(payload.get()) {
self.emit(event, value);
}
}
fn prefers_json(&self) -> bool {
false
}
}
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 !crate::db::session_is_live() {
return;
}
if let Some(emitter) = EVENT_EMITTER.get() {
if emitter.prefers_json() {
if let Ok(json) = serde_json::value::to_raw_value(payload) {
emitter.emit_json(event, &json);
}
} else if let Ok(value) = serde_json::to_value(payload) {
emitter.emit(event, value);
}
}
}
#[derive(serde::Serialize)]
struct MessagePayload<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
old_id: Option<&'a str>,
message: &'a crate::types::Message,
chat_id: &'a str,
}
pub fn emit_message_new(chat_id: &str, message: &crate::types::Message) {
emit_event("message_new", &MessagePayload { old_id: None, message, chat_id });
}
pub fn emit_message_replaced(chat_id: &str, old_id: &str, message: &crate::types::Message) {
emit_event("message_update", &MessagePayload { old_id: Some(old_id), message, chat_id });
}
pub fn emit_event_json(event: &str, payload: serde_json::Value) {
if !crate::db::session_is_live() {
return;
}
if let Some(emitter) = EVENT_EMITTER.get() {
emitter.emit(event, payload);
}
}
pub async fn emit_message_update(chat_id: &str, old_id: &str, message: &mut crate::types::Message) {
let _ = crate::db::events::populate_reply_context(message).await;
emit_message_replaced(chat_id, old_id, message);
}
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());
}
}