1use std::sync::OnceLock;
7
8pub trait EventEmitter: Send + Sync + 'static {
14 fn emit(&self, event: &str, payload: serde_json::Value);
15}
16
17pub struct NoOpEmitter;
19
20impl EventEmitter for NoOpEmitter {
21 fn emit(&self, _event: &str, _payload: serde_json::Value) {}
22}
23
24static EVENT_EMITTER: OnceLock<Box<dyn EventEmitter>> = OnceLock::new();
26
27pub fn set_event_emitter(emitter: Box<dyn EventEmitter>) {
29 let _ = EVENT_EMITTER.set(emitter);
30}
31
32pub fn emit_event<T: serde::Serialize>(event: &str, payload: &T) {
34 if !crate::db::session_is_live() {
35 return;
36 }
37 if let Some(emitter) = EVENT_EMITTER.get() {
38 if let Ok(value) = serde_json::to_value(payload) {
39 emitter.emit(event, value);
40 }
41 }
42}
43
44pub fn emit_event_json(event: &str, payload: serde_json::Value) {
46 if !crate::db::session_is_live() {
47 return;
48 }
49 if let Some(emitter) = EVENT_EMITTER.get() {
50 emitter.emit(event, payload);
51 }
52}
53
54pub async fn emit_message_update(chat_id: &str, old_id: &str, message: &mut crate::types::Message) {
62 let _ = crate::db::events::populate_reply_context(message).await;
63 emit_event(
64 "message_update",
65 &serde_json::json!({ "old_id": old_id, "message": &*message, "chat_id": chat_id }),
66 );
67}
68
69pub fn has_event_emitter() -> bool {
71 EVENT_EMITTER.get().is_some()
72}
73
74pub trait SubscriptionRefresher: Send + Sync + 'static {
84 fn refresh(&self);
85}
86
87pub struct NoOpSubscriptionRefresher;
88impl SubscriptionRefresher for NoOpSubscriptionRefresher {
89 fn refresh(&self) {}
90}
91
92static SUBSCRIPTION_REFRESHER: OnceLock<Box<dyn SubscriptionRefresher>> = OnceLock::new();
93
94pub fn set_subscription_refresher(refresher: Box<dyn SubscriptionRefresher>) {
95 let _ = SUBSCRIPTION_REFRESHER.set(refresher);
96}
97
98pub fn refresh_subscriptions() {
99 if let Some(r) = SUBSCRIPTION_REFRESHER.get() {
100 r.refresh();
101 }
102}
103
104pub trait ProgressReporter: Send + Sync {
106 fn report_progress(&self, percentage: Option<u8>, bytes: Option<u64>, bytes_per_sec: Option<f64>) -> Result<(), &'static str>;
107 fn report_complete(&self) -> Result<(), &'static str>;
108}
109
110pub struct NoOpProgressReporter;
112
113impl ProgressReporter for NoOpProgressReporter {
114 fn report_progress(&self, _: Option<u8>, _: Option<u64>, _: Option<f64>) -> Result<(), &'static str> { Ok(()) }
115 fn report_complete(&self) -> Result<(), &'static str> { Ok(()) }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use std::sync::atomic::{AtomicUsize, Ordering};
122
123 static REFRESH_CALLS: AtomicUsize = AtomicUsize::new(0);
132 static REFRESH_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
133
134 struct CountingRefresher;
135 impl SubscriptionRefresher for CountingRefresher {
136 fn refresh(&self) {
137 REFRESH_CALLS.fetch_add(1, Ordering::Relaxed);
138 }
139 }
140
141 fn refresh_test_setup() -> std::sync::MutexGuard<'static, ()> {
147 let guard = REFRESH_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
148 set_subscription_refresher(Box::new(CountingRefresher));
149 REFRESH_CALLS.store(0, Ordering::Relaxed);
150 guard
151 }
152
153 #[test]
154 fn refresh_subscriptions_invokes_registered_impl_once() {
155 let _g = refresh_test_setup();
156 assert_eq!(REFRESH_CALLS.load(Ordering::Relaxed), 0);
157
158 refresh_subscriptions();
159
160 assert_eq!(REFRESH_CALLS.load(Ordering::Relaxed), 1);
161 }
162
163 #[test]
164 fn refresh_subscriptions_invokes_registered_impl_per_call() {
165 let _g = refresh_test_setup();
169
170 for _ in 0..5 {
171 refresh_subscriptions();
172 }
173
174 assert_eq!(REFRESH_CALLS.load(Ordering::Relaxed), 5);
175 }
176
177 #[test]
178 fn set_subscription_refresher_is_idempotent() {
179 let _g = refresh_test_setup();
185
186 struct PanicRefresher;
187 impl SubscriptionRefresher for PanicRefresher {
188 fn refresh(&self) {
189 panic!("PanicRefresher::refresh() should never be reached — OnceLock should have ignored the second set_subscription_refresher call");
190 }
191 }
192 set_subscription_refresher(Box::new(PanicRefresher));
193
194 refresh_subscriptions();
198
199 assert_eq!(REFRESH_CALLS.load(Ordering::Relaxed), 1);
200 }
201
202 #[test]
203 fn refresh_subscriptions_is_thread_safe() {
204 let _g = refresh_test_setup();
208 let handles: Vec<_> = (0..10).map(|_| {
209 std::thread::spawn(|| {
210 for _ in 0..100 {
211 refresh_subscriptions();
212 }
213 })
214 }).collect();
215 for h in handles { h.join().unwrap(); }
216
217 assert_eq!(REFRESH_CALLS.load(Ordering::Relaxed), 1_000);
218 }
219
220 #[test]
221 fn no_op_subscription_refresher_returns_quietly() {
222 let _g = refresh_test_setup();
225 let initial = REFRESH_CALLS.load(Ordering::Relaxed);
226 let r = NoOpSubscriptionRefresher;
227 r.refresh();
228 r.refresh();
229 assert_eq!(REFRESH_CALLS.load(Ordering::Relaxed), initial);
230 }
231
232 #[test]
237 fn no_op_emitter_does_not_panic() {
238 let e = NoOpEmitter;
239 e.emit("test_event", serde_json::json!({"k": "v"}));
240 }
242
243 #[test]
244 fn emit_event_when_unregistered_is_silent() {
245 crate::traits::emit_event("test_event", &serde_json::json!({"k": "v"}));
251 }
252
253 #[test]
254 fn emit_event_json_when_unregistered_is_silent() {
255 crate::traits::emit_event_json("test_event", serde_json::json!({"k": "v"}));
256 }
257
258 #[test]
259 fn no_op_progress_reporter_returns_ok() {
260 let r = NoOpProgressReporter;
261 assert!(r.report_progress(Some(50), Some(1024), Some(100.0)).is_ok());
262 assert!(r.report_complete().is_ok());
263 }
264}