gglib_core/ports/
download_event_emitter.rs1use std::sync::Arc;
7
8use crate::download::DownloadEvent;
9use crate::events::AppEvent;
10
11use super::AppEventEmitter;
12
13pub trait DownloadEventEmitterPort: Send + Sync {
27 fn emit(&self, event: DownloadEvent);
32
33 fn clone_box(&self) -> Box<dyn DownloadEventEmitterPort>;
38}
39
40#[derive(Debug, Clone, Default)]
47pub struct NoopDownloadEmitter;
48
49impl NoopDownloadEmitter {
50 #[must_use]
52 pub const fn new() -> Self {
53 Self
54 }
55}
56
57impl DownloadEventEmitterPort for NoopDownloadEmitter {
58 fn emit(&self, _event: DownloadEvent) {
59 }
61
62 fn clone_box(&self) -> Box<dyn DownloadEventEmitterPort> {
63 Box::new(self.clone())
64 }
65}
66
67#[derive(Clone)]
86pub struct AppEventBridge {
87 inner: Arc<dyn AppEventEmitter>,
88}
89
90impl AppEventBridge {
91 pub fn new(emitter: Arc<dyn AppEventEmitter>) -> Self {
93 Self { inner: emitter }
94 }
95
96 const fn map_event(event: DownloadEvent) -> AppEvent {
100 AppEvent::Download { event }
101 }
102}
103
104impl DownloadEventEmitterPort for AppEventBridge {
105 fn emit(&self, event: DownloadEvent) {
106 let app_event = Self::map_event(event);
107 self.inner.emit(app_event);
108 }
109
110 fn clone_box(&self) -> Box<dyn DownloadEventEmitterPort> {
111 Box::new(self.clone())
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_noop_emitter() {
121 let emitter = NoopDownloadEmitter::new();
122
123 emitter.emit(DownloadEvent::DownloadStarted {
125 id: "test".to_string(),
126 shard_index: None,
127 total_shards: None,
128 });
129 }
130
131 #[test]
132 fn test_noop_emitter_clone_box() {
133 let emitter = NoopDownloadEmitter::new();
134 let _boxed: Box<dyn DownloadEventEmitterPort> = emitter.clone_box();
135 }
136
137 #[test]
138 fn test_arc_emitter() {
139 let emitter: Arc<dyn DownloadEventEmitterPort> = Arc::new(NoopDownloadEmitter::new());
140 emitter.emit(DownloadEvent::DownloadStarted {
141 id: "test".to_string(),
142 shard_index: None,
143 total_shards: None,
144 });
145 }
146
147 #[test]
152 fn test_shard_progress_preserved_in_bridge() {
153 use std::sync::Mutex;
154
155 #[derive(Clone)]
157 struct MockEmitter {
158 captured: Arc<Mutex<Vec<AppEvent>>>,
159 }
160
161 impl AppEventEmitter for MockEmitter {
162 fn emit(&self, event: AppEvent) {
163 self.captured.lock().unwrap().push(event);
164 }
165
166 fn clone_box(&self) -> Box<dyn AppEventEmitter> {
167 Box::new(self.clone())
168 }
169 }
170
171 let captured = Arc::new(Mutex::new(Vec::new()));
172 let mock = Arc::new(MockEmitter {
173 captured: captured.clone(),
174 });
175 let bridge = AppEventBridge::new(mock);
176
177 let shard_event = DownloadEvent::shard_progress(
179 "model:Q4_K_M",
180 0,
181 4,
182 "model-00001-of-00004.gguf",
183 500_000,
184 1_000_000,
185 500_000,
186 4_000_000,
187 1024.0,
188 );
189
190 bridge.emit(shard_event);
191
192 let events = captured.lock().unwrap();
194 assert_eq!(events.len(), 1, "Should emit exactly one AppEvent");
195
196 match &events[0] {
197 AppEvent::Download { event } => match event {
198 DownloadEvent::ShardProgress {
199 shard_index,
200 total_shards,
201 shard_filename,
202 shard_downloaded,
203 shard_total,
204 aggregate_downloaded,
205 aggregate_total,
206 ..
207 } => {
208 assert_eq!(*shard_index, 0);
209 assert_eq!(*total_shards, 4);
210 assert_eq!(shard_filename, "model-00001-of-00004.gguf");
211 assert_eq!(*shard_downloaded, 500_000);
212 assert_eq!(*shard_total, 1_000_000);
213 assert_eq!(*aggregate_downloaded, 500_000);
214 assert_eq!(*aggregate_total, 4_000_000);
215 }
216 _ => panic!("Expected ShardProgress to be preserved, got {event:?}"),
217 },
218 other => panic!("Expected AppEvent::Download wrapper, got {other:?}"),
219 }
220 }
221}