Skip to main content

gglib_core/events/
server.rs

1//! Model server lifecycle events.
2
3use serde::{Deserialize, Serialize};
4
5use crate::ports::model_runtime::{ModelRuntimeError, RuntimeErrorEnvelope};
6
7use super::AppEvent;
8
9/// Summary of a running server for event emission.
10///
11/// This is a lightweight representation used by the `ServerEvents` port
12/// to decouple lifecycle logic from transport-specific implementations.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct ServerSummary {
16    /// Unique server instance ID.
17    pub id: String,
18    /// Model ID being served.
19    pub model_id: String,
20    /// Model name.
21    pub model_name: String,
22    /// Port the server is listening on.
23    pub port: u16,
24    /// Health status (None = unknown/pending).
25    pub healthy: Option<bool>,
26}
27
28impl ServerSummary {
29    /// Parse the `model_id` string as a u32.
30    ///
31    /// Returns `None` if parsing fails.
32    ///
33    /// Pure helper: adapters decide how to handle/log parse failures.
34    pub fn parsed_model_id(&self) -> Option<u32> {
35        self.model_id.parse::<u32>().ok()
36    }
37}
38
39/// Port for emitting server lifecycle events.
40///
41/// This trait decouples the core server lifecycle logic from transport-specific
42/// event emission (Tauri events, SSE, logging, etc.). Implementations convert
43/// `ServerSummary` to their native event format.
44///
45/// # Design
46///
47/// - **Object-safe**: Uses `&self` for dynamic dispatch via `Arc<dyn ServerEvents>`
48/// - **Fire-and-forget**: Methods don't return `Result` — adapters handle errors internally
49/// - **Generic**: No knowledge of Tauri/Axum/CLI specifics
50///
51/// # Example
52///
53/// ```rust
54/// use gglib_core::events::{ServerEvents, ServerSummary};
55/// use gglib_core::ports::ModelRuntimeError;
56///
57/// struct LoggingEvents;
58///
59/// impl ServerEvents for LoggingEvents {
60///     fn started(&self, server: &ServerSummary) {
61///         println!("Server {} started on port {}", server.model_name, server.port);
62///     }
63///     fn stopping(&self, server: &ServerSummary) {
64///         println!("Stopping server {}", server.model_name);
65///     }
66///     fn stopped(&self, server: &ServerSummary) {
67///         println!("Server {} stopped", server.model_name);
68///     }
69///     fn snapshot(&self, servers: &[ServerSummary]) {
70///         println!("Server snapshot: {} running", servers.len());
71///     }
72///     fn error(&self, server: &ServerSummary, error: &ModelRuntimeError) {
73///         eprintln!("Server {} error: {}", server.model_name, error);
74///     }
75/// }
76/// ```
77pub trait ServerEvents: Send + Sync {
78    /// Called when a server has successfully started.
79    fn started(&self, server: &ServerSummary);
80
81    /// Called just before stopping a server.
82    fn stopping(&self, server: &ServerSummary);
83
84    /// Called after a server has stopped.
85    fn stopped(&self, server: &ServerSummary);
86
87    /// Called to broadcast the current state of all running servers.
88    fn snapshot(&self, servers: &[ServerSummary]);
89
90    /// Called when a server error occurs.
91    fn error(&self, server: &ServerSummary, error: &ModelRuntimeError);
92}
93
94/// No-op implementation of `ServerEvents` for testing and non-GUI contexts.
95///
96/// This is the default when `GuiBackend` is constructed without explicit
97/// event handling (e.g., in unit tests or CLI contexts).
98#[derive(Debug, Clone, Copy, Default)]
99pub struct NoopServerEvents;
100
101impl ServerEvents for NoopServerEvents {
102    fn started(&self, _server: &ServerSummary) {}
103    fn stopping(&self, _server: &ServerSummary) {}
104    fn stopped(&self, _server: &ServerSummary) {}
105    fn snapshot(&self, _servers: &[ServerSummary]) {}
106    fn error(&self, _server: &ServerSummary, _error: &ModelRuntimeError) {}
107}
108
109/// Entry in a server snapshot.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct ServerSnapshotEntry {
113    /// Model ID being served.
114    pub model_id: i64,
115    /// Model name.
116    pub model_name: String,
117    /// Port the server is listening on.
118    pub port: u16,
119    /// Unix timestamp (seconds) when started.
120    pub started_at: u64,
121    /// Whether the server is healthy.
122    pub healthy: bool,
123}
124
125impl AppEvent {
126    /// Create a server started event.
127    pub fn server_started(model_id: i64, model_name: impl Into<String>, port: u16) -> Self {
128        Self::ServerStarted {
129            model_id,
130            model_name: model_name.into(),
131            port,
132        }
133    }
134
135    /// Create a server stopped event.
136    pub fn server_stopped(model_id: i64, model_name: impl Into<String>) -> Self {
137        Self::ServerStopped {
138            model_id,
139            model_name: model_name.into(),
140        }
141    }
142
143    /// Create a server error event.
144    pub fn server_error(
145        model_id: Option<i64>,
146        model_name: impl Into<String>,
147        error: RuntimeErrorEnvelope,
148    ) -> Self {
149        Self::ServerError {
150            model_id,
151            model_name: model_name.into(),
152            error,
153        }
154    }
155
156    /// Create a server snapshot event.
157    pub const fn server_snapshot(servers: Vec<ServerSnapshotEntry>) -> Self {
158        Self::ServerSnapshot { servers }
159    }
160
161    /// Build a `ServerStarted` event from a `ServerSummary`.
162    pub fn from_server_started(server: &ServerSummary) -> Self {
163        let model_id = server.model_id.parse::<i64>().unwrap_or(0);
164        Self::server_started(model_id, &server.model_name, server.port)
165    }
166
167    /// Build a `ServerStopped` event from a `ServerSummary`.
168    pub fn from_server_stopped(server: &ServerSummary) -> Self {
169        let model_id = server.model_id.parse::<i64>().unwrap_or(0);
170        Self::server_stopped(model_id, &server.model_name)
171    }
172
173    /// Build a `ServerError` event from a `ServerSummary`.
174    pub fn from_server_error(server: &ServerSummary, error: RuntimeErrorEnvelope) -> Self {
175        let model_id = server.model_id.parse::<i64>().ok();
176        Self::server_error(model_id, &server.model_name, error)
177    }
178
179    /// Build a `ServerSnapshot` event from a slice of `ServerSummary`.
180    pub fn from_server_snapshot(servers: &[ServerSummary]) -> Self {
181        let started_at = std::time::SystemTime::now()
182            .duration_since(std::time::UNIX_EPOCH)
183            .unwrap()
184            .as_secs();
185        let entries: Vec<ServerSnapshotEntry> = servers
186            .iter()
187            .map(|s| ServerSnapshotEntry {
188                model_id: s.model_id.parse::<i64>().unwrap_or(0),
189                model_name: s.model_name.clone(),
190                port: s.port,
191                started_at,
192                healthy: s.healthy.unwrap_or(false),
193            })
194            .collect();
195        Self::server_snapshot(entries)
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    fn make_server(id: &str, model_id: &str, name: &str, port: u16) -> ServerSummary {
204        ServerSummary {
205            id: id.to_string(),
206            model_id: model_id.to_string(),
207            model_name: name.to_string(),
208            port,
209            healthy: Some(true),
210        }
211    }
212
213    #[test]
214    fn test_from_server_started() {
215        let server = make_server("srv-1", "42", "test-model", 8080);
216        let event = AppEvent::from_server_started(&server);
217        match event {
218            AppEvent::ServerStarted {
219                model_id,
220                model_name,
221                port,
222            } => {
223                assert_eq!(model_id, 42);
224                assert_eq!(model_name, "test-model");
225                assert_eq!(port, 8080);
226            }
227            _ => panic!("expected ServerStarted"),
228        }
229    }
230
231    #[test]
232    fn test_from_server_stopped() {
233        let server = make_server("srv-1", "42", "test-model", 8080);
234        let event = AppEvent::from_server_stopped(&server);
235        match event {
236            AppEvent::ServerStopped {
237                model_id,
238                model_name,
239            } => {
240                assert_eq!(model_id, 42);
241                assert_eq!(model_name, "test-model");
242            }
243            _ => panic!("expected ServerStopped"),
244        }
245    }
246
247    #[test]
248    fn test_from_server_error() {
249        let server = make_server("srv-1", "42", "test-model", 8080);
250        let runtime_err = ModelRuntimeError::Internal("something failed".to_string());
251        let event = AppEvent::from_server_error(&server, RuntimeErrorEnvelope::from(&runtime_err));
252        match event {
253            AppEvent::ServerError {
254                model_id,
255                model_name,
256                error,
257            } => {
258                assert_eq!(model_id, Some(42));
259                assert_eq!(model_name, "test-model");
260                assert_eq!(error.message, "Internal error: something failed");
261                assert_eq!(error.r#type, "server_error");
262                assert!(!error.retryable);
263            }
264            _ => panic!("expected ServerError"),
265        }
266    }
267
268    #[test]
269    fn test_from_server_error_invalid_model_id() {
270        let server = make_server("srv-1", "abc", "test-model", 8080);
271        let runtime_err = ModelRuntimeError::Internal("something failed".to_string());
272        let event = AppEvent::from_server_error(&server, RuntimeErrorEnvelope::from(&runtime_err));
273        match event {
274            AppEvent::ServerError {
275                model_id,
276                model_name,
277                error,
278            } => {
279                assert_eq!(model_id, None);
280                assert_eq!(model_name, "test-model");
281                assert_eq!(error.message, "Internal error: something failed");
282            }
283            _ => panic!("expected ServerError"),
284        }
285    }
286
287    #[test]
288    fn test_from_server_snapshot() {
289        let servers = vec![
290            make_server("srv-a", "1", "model-a", 9001),
291            make_server("srv-b", "2", "model-b", 9002),
292        ];
293        let event = AppEvent::from_server_snapshot(&servers);
294        match event {
295            AppEvent::ServerSnapshot { servers: entries } => {
296                assert_eq!(entries.len(), 2);
297                assert_eq!(entries[0].model_id, 1);
298                assert_eq!(entries[0].port, 9001);
299                assert_eq!(entries[1].model_id, 2);
300                assert_eq!(entries[1].port, 9002);
301            }
302            _ => panic!("expected ServerSnapshot"),
303        }
304    }
305}