gglib_core/events/
server.rs

1//! Model server lifecycle events.
2
3use serde::{Deserialize, Serialize};
4
5use super::AppEvent;
6
7/// Summary of a running server for event emission.
8///
9/// This is a lightweight representation used by the `ServerEvents` port
10/// to decouple lifecycle logic from transport-specific implementations.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct ServerSummary {
14    /// Unique server instance ID.
15    pub id: String,
16    /// Model ID being served.
17    pub model_id: String,
18    /// Model name.
19    pub model_name: String,
20    /// Port the server is listening on.
21    pub port: u16,
22    /// Health status (None = unknown/pending).
23    pub healthy: Option<bool>,
24}
25
26impl ServerSummary {
27    /// Parse the `model_id` string as a u32.
28    ///
29    /// Returns `None` if parsing fails.
30    ///
31    /// Pure helper: adapters decide how to handle/log parse failures.
32    pub fn parsed_model_id(&self) -> Option<u32> {
33        self.model_id.parse::<u32>().ok()
34    }
35}
36
37/// Port for emitting server lifecycle events.
38///
39/// This trait decouples the core server lifecycle logic from transport-specific
40/// event emission (Tauri events, SSE, logging, etc.). Implementations convert
41/// `ServerSummary` to their native event format.
42///
43/// # Design
44///
45/// - **Object-safe**: Uses `&self` for dynamic dispatch via `Arc<dyn ServerEvents>`
46/// - **Fire-and-forget**: Methods don't return `Result` — adapters handle errors internally
47/// - **Generic**: No knowledge of Tauri/Axum/CLI specifics
48///
49/// # Example
50///
51/// ```rust
52/// use gglib_core::events::{ServerEvents, ServerSummary};
53///
54/// struct LoggingEvents;
55///
56/// impl ServerEvents for LoggingEvents {
57///     fn started(&self, server: &ServerSummary) {
58///         println!("Server {} started on port {}", server.model_name, server.port);
59///     }
60///     fn stopping(&self, server: &ServerSummary) {
61///         println!("Stopping server {}", server.model_name);
62///     }
63///     fn stopped(&self, server: &ServerSummary) {
64///         println!("Server {} stopped", server.model_name);
65///     }
66///     fn snapshot(&self, servers: &[ServerSummary]) {
67///         println!("Server snapshot: {} running", servers.len());
68///     }
69///     fn error(&self, server: &ServerSummary, error: &str) {
70///         eprintln!("Server {} error: {}", server.model_name, error);
71///     }
72/// }
73/// ```
74pub trait ServerEvents: Send + Sync {
75    /// Called when a server has successfully started.
76    fn started(&self, server: &ServerSummary);
77
78    /// Called just before stopping a server.
79    fn stopping(&self, server: &ServerSummary);
80
81    /// Called after a server has stopped.
82    fn stopped(&self, server: &ServerSummary);
83
84    /// Called to broadcast the current state of all running servers.
85    fn snapshot(&self, servers: &[ServerSummary]);
86
87    /// Called when a server error occurs.
88    fn error(&self, server: &ServerSummary, error: &str);
89}
90
91/// No-op implementation of `ServerEvents` for testing and non-GUI contexts.
92///
93/// This is the default when `GuiBackend` is constructed without explicit
94/// event handling (e.g., in unit tests or CLI contexts).
95#[derive(Debug, Clone, Copy, Default)]
96pub struct NoopServerEvents;
97
98impl ServerEvents for NoopServerEvents {
99    fn started(&self, _server: &ServerSummary) {}
100    fn stopping(&self, _server: &ServerSummary) {}
101    fn stopped(&self, _server: &ServerSummary) {}
102    fn snapshot(&self, _servers: &[ServerSummary]) {}
103    fn error(&self, _server: &ServerSummary, _error: &str) {}
104}
105
106/// Entry in a server snapshot.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108#[serde(rename_all = "camelCase")]
109pub struct ServerSnapshotEntry {
110    /// Model ID being served.
111    pub model_id: i64,
112    /// Model name.
113    pub model_name: String,
114    /// Port the server is listening on.
115    pub port: u16,
116    /// Unix timestamp (seconds) when started.
117    pub started_at: u64,
118    /// Whether the server is healthy.
119    pub healthy: bool,
120}
121
122impl AppEvent {
123    /// Create a server started event.
124    pub fn server_started(model_id: i64, model_name: impl Into<String>, port: u16) -> Self {
125        Self::ServerStarted {
126            model_id,
127            model_name: model_name.into(),
128            port,
129        }
130    }
131
132    /// Create a server stopped event.
133    pub fn server_stopped(model_id: i64, model_name: impl Into<String>) -> Self {
134        Self::ServerStopped {
135            model_id,
136            model_name: model_name.into(),
137        }
138    }
139
140    /// Create a server error event.
141    pub fn server_error(
142        model_id: Option<i64>,
143        model_name: impl Into<String>,
144        error: impl Into<String>,
145    ) -> Self {
146        Self::ServerError {
147            model_id,
148            model_name: model_name.into(),
149            error: error.into(),
150        }
151    }
152
153    /// Create a server snapshot event.
154    pub const fn server_snapshot(servers: Vec<ServerSnapshotEntry>) -> Self {
155        Self::ServerSnapshot { servers }
156    }
157}