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