gglib_core/events/mod.rs
1#![doc = include_str!("README.md")]
2mod app;
3mod remote;
4mod server;
5
6#[cfg(test)]
7mod app_event_tests;
8
9use serde::{Deserialize, Serialize};
10
11use crate::ports::RuntimeErrorEnvelope;
12
13// Re-export event types
14pub use app::ModelSummary;
15pub use server::{NoopServerEvents, ServerEvents, ServerSnapshotEntry, ServerSummary};
16
17// Import download types for AppEvent::Download wrapper
18use crate::download::DownloadEvent;
19
20/// Canonical event types for all adapters.
21///
22/// This enum unifies server, download, and model events into a single
23/// discriminated union. Each variant includes all necessary context
24/// for the event to be self-describing.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS), ts(export))]
27#[serde(tag = "type", rename_all = "snake_case")]
28pub enum AppEvent {
29 // ========== Server Events ==========
30 /// A model server has started and is ready to accept requests.
31 ServerStarted {
32 /// ID of the model being served.
33 #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
34 #[serde(rename = "modelId")]
35 model_id: i64,
36 /// Name of the model being served.
37 #[serde(rename = "modelName")]
38 model_name: String,
39 /// Port the server is listening on.
40 port: u16,
41 },
42
43 /// A model server has been stopped (clean shutdown).
44 ServerStopped {
45 /// ID of the model that was being served.
46 #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
47 #[serde(rename = "modelId")]
48 model_id: i64,
49 /// Name of the model that was being served.
50 #[serde(rename = "modelName")]
51 model_name: String,
52 },
53
54 /// A model server encountered an error.
55 ServerError {
56 /// ID of the model being served (if known).
57 ///
58 /// Serde always sends the key — an unparseable model ID arrives as
59 /// `null`, not as an absent field.
60 #[cfg_attr(feature = "ts-bindings", ts(type = "number | null"))]
61 #[serde(rename = "modelId")]
62 model_id: Option<i64>,
63 /// Name of the model being served.
64 #[serde(rename = "modelName")]
65 model_name: String,
66 /// Structured error detail (message, type discriminant, retryable
67 /// flag), mirroring the HTTP layer's `ErrorResponse` shape.
68 error: RuntimeErrorEnvelope,
69 },
70
71 /// Snapshot of all currently running servers.
72 ServerSnapshot {
73 /// List of currently running servers.
74 servers: Vec<ServerSnapshotEntry>,
75 },
76
77 // ========== Download Events ==========
78 /// Download lifecycle + progress events (including shard progress).
79 ///
80 /// Wraps `DownloadEvent` verbatim to preserve all detail including
81 /// shard-specific progress information.
82 #[serde(rename = "download")]
83 Download {
84 /// The download event payload.
85 event: DownloadEvent,
86 },
87
88 // ========== Model Events ==========
89 /// A model was added to the library.
90 ModelAdded {
91 /// Summary of the added model.
92 model: ModelSummary,
93 },
94
95 /// A model was removed from the library.
96 ModelRemoved {
97 /// ID of the removed model.
98 #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
99 #[serde(rename = "modelId")]
100 model_id: i64,
101 },
102
103 /// A model was updated in the library.
104 ModelUpdated {
105 /// Summary of the updated model.
106 model: ModelSummary,
107 },
108
109 // ========== Verification Events ==========
110 /// Model verification progress update.
111 VerificationProgress {
112 /// ID of the model being verified.
113 #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
114 #[serde(rename = "modelId")]
115 model_id: i64,
116 /// Name of the model being verified.
117 #[serde(rename = "modelName")]
118 model_name: String,
119 /// Name of the shard being verified.
120 #[serde(rename = "shardName")]
121 shard_name: String,
122 /// Bytes processed so far.
123 #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
124 #[serde(rename = "bytesProcessed")]
125 bytes_processed: u64,
126 /// Total bytes to process.
127 #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
128 #[serde(rename = "totalBytes")]
129 total_bytes: u64,
130 },
131
132 /// Model verification completed.
133 VerificationComplete {
134 /// ID of the verified model.
135 #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
136 #[serde(rename = "modelId")]
137 model_id: i64,
138 /// Name of the verified model.
139 #[serde(rename = "modelName")]
140 model_name: String,
141 /// Overall health status.
142 #[serde(rename = "overallHealth")]
143 overall_health: crate::services::OverallHealth,
144 },
145
146 /// Server health status has changed.
147 ///
148 /// Emitted by continuous monitoring when a server's health state changes.
149 ServerHealthChanged {
150 /// Unique server instance identifier.
151 #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
152 #[serde(rename = "serverId")]
153 server_id: i64,
154 /// ID of the model being served.
155 #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
156 #[serde(rename = "modelId")]
157 model_id: i64,
158 /// New health status.
159 status: crate::ports::ServerHealthStatus,
160 /// Optional detail message (e.g., error description).
161 #[cfg_attr(feature = "ts-bindings", ts(optional))]
162 #[serde(skip_serializing_if = "Option::is_none")]
163 detail: Option<String>,
164 /// Unix timestamp in milliseconds when status changed.
165 #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
166 timestamp: u64,
167 },
168
169 // ========== Proxy Events ==========
170 /// The OpenAI-compatible proxy has started.
171 ProxyStarted {
172 /// Port the proxy is listening on.
173 port: u16,
174 },
175
176 /// The proxy has been stopped (clean shutdown).
177 ProxyStopped,
178
179 /// The proxy crashed (task exited without cancellation).
180 ProxyCrashed,
181
182 // ========== Remote tunnel events (ADR 0012) ==========
183 /// The remote tunnel is up and has a ticket. The ticket itself is
184 /// never on the event stream — any local GUI client can read it — so
185 /// this carries the same twelve-character fingerprint the logs use.
186 RemoteEnabled {
187 /// Fingerprint of the ticket this session minted.
188 #[serde(rename = "ticketFingerprint")]
189 ticket_fingerprint: String,
190 },
191
192 /// The remote tunnel was taken down; nothing answers its ticket until
193 /// `enable` puts the same one back.
194 RemoteDisabled,
195
196 /// A device redeemed the pairing code and now holds the key.
197 RemotePaired {
198 /// The tunnel edge's fingerprint for that device, when it sent one.
199 peer: Option<String>,
200 },
201
202 /// This machine's connect side is up: a local port that is the remote
203 /// proxy.
204 RemoteConnected {
205 /// The loopback port the tunnel is bound to on this machine.
206 port: u16,
207 },
208
209 /// The connect side was taken down.
210 RemoteDisconnected,
211
212 /// The far machine has been away past the grace. The port stays bound
213 /// and is still being dialled; this says so, so a client is not left
214 /// reading "connected" over nothing.
215 RemoteAway {
216 /// The loopback port that is still bound.
217 port: u16,
218 },
219
220 /// The far machine answered again after being away.
221 RemoteBack {
222 /// The loopback port, unchanged.
223 port: u16,
224 },
225}
226
227impl AppEvent {
228 /// Colon-separated event names.
229 ///
230 /// **Not what goes on the wire.** `AppEvent` is `#[serde(tag = "type",
231 /// rename_all = "snake_case")]`, so SSE carries `download`/`model_added`;
232 /// these `download:started` spellings are the Tauri-bus vocabulary from
233 /// before the GUI backend moved into the daemon, and the Tauri event
234 /// branch that subscribed to them is gone.
235 ///
236 /// Nothing outside this module's tests calls it. Retiring it belongs with
237 /// the rest of the residual-Rust sweep, not here.
238 pub const fn event_name(&self) -> &'static str {
239 match self {
240 Self::ServerStarted { .. } => "server:started",
241 Self::ServerStopped { .. } => "server:stopped",
242 Self::ServerError { .. } => "server:error",
243 Self::ServerSnapshot { .. } => "server:snapshot",
244 Self::ServerHealthChanged { .. } => "server:health_changed",
245 Self::Download { event } => event.event_name(),
246 Self::ModelAdded { .. } => "model:added",
247 Self::ModelRemoved { .. } => "model:removed",
248 Self::ModelUpdated { .. } => "model:updated",
249 Self::VerificationProgress { .. } => "verification:progress",
250 Self::VerificationComplete { .. } => "verification:complete",
251 Self::ProxyStarted { .. } => "proxy:started",
252 Self::ProxyStopped => "proxy:stopped",
253 Self::ProxyCrashed => "proxy:crashed",
254 Self::RemoteEnabled { .. } => "remote:enabled",
255 Self::RemoteDisabled => "remote:disabled",
256 Self::RemotePaired { .. } => "remote:paired",
257 Self::RemoteConnected { .. } => "remote:connected",
258 Self::RemoteDisconnected => "remote:disconnected",
259 Self::RemoteAway { .. } => "remote:away",
260 Self::RemoteBack { .. } => "remote:back",
261 }
262 }
263}
264
265impl AppEvent {
266 /// Create a [`AppEvent::ProxyStarted`] event.
267 pub const fn proxy_started(port: u16) -> Self {
268 Self::ProxyStarted { port }
269 }
270
271 /// Create a [`AppEvent::ProxyStopped`] event.
272 pub const fn proxy_stopped() -> Self {
273 Self::ProxyStopped
274 }
275
276 /// Create a [`AppEvent::ProxyCrashed`] event.
277 pub const fn proxy_crashed() -> Self {
278 Self::ProxyCrashed
279 }
280}