1use serde::{Deserialize, Serialize};
4
5use crate::ports::model_runtime::{ModelRuntimeError, RuntimeErrorEnvelope};
6
7use super::AppEvent;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct ServerSummary {
16 pub id: String,
18 pub model_id: String,
20 pub model_name: String,
22 pub port: u16,
24}
25
26pub trait ServerEvents: Send + Sync {
65 fn started(&self, server: &ServerSummary);
67
68 fn stopping(&self, server: &ServerSummary);
70
71 fn stopped(&self, server: &ServerSummary);
73
74 fn snapshot(&self, servers: &[ServerSummary]);
76
77 fn error(&self, server: &ServerSummary, error: &ModelRuntimeError);
79}
80
81#[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#[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 #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
103 pub model_id: i64,
104 pub model_name: String,
106 pub port: u16,
108 #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
110 pub started_at: u64,
111}
112
113impl AppEvent {
114 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 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 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 pub const fn server_snapshot(servers: Vec<ServerSnapshotEntry>) -> Self {
146 Self::ServerSnapshot { servers }
147 }
148
149 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 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 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 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}