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}