Skip to main content

ant_core/node/
events.rs

1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4
5/// Structured events emitted by the daemon supervisor and long-running operations.
6///
7/// Serialized to JSON for SSE streaming at `GET /api/v1/events`.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(tag = "type", rename_all = "snake_case")]
10pub enum NodeEvent {
11    NodeStarting {
12        node_id: u32,
13    },
14    NodeStarted {
15        node_id: u32,
16        pid: u32,
17    },
18    NodeStopping {
19        node_id: u32,
20    },
21    NodeStopped {
22        node_id: u32,
23    },
24    NodeCrashed {
25        node_id: u32,
26        exit_code: Option<i32>,
27    },
28    NodeRestarting {
29        node_id: u32,
30        attempt: u32,
31    },
32    NodeErrored {
33        node_id: u32,
34        message: String,
35    },
36    DownloadStarted {
37        version: String,
38    },
39    DownloadProgress {
40        bytes: u64,
41        total: u64,
42    },
43    DownloadComplete {
44        version: String,
45        path: PathBuf,
46    },
47    /// Emitted after the supervisor has respawned a node against its replaced binary and
48    /// observed the new version.
49    NodeUpgraded {
50        node_id: u32,
51        old_version: String,
52        new_version: String,
53    },
54    /// Emitted when the daemon automatically evicts a node to reclaim disk space: its process was
55    /// stopped and its data directory deleted. The node now reports as `Evicted` until dismissed.
56    NodeEvicted {
57        node_id: u32,
58        /// Human-readable explanation of the eviction.
59        reason: String,
60        /// Approximate bytes reclaimed by deleting the node's data directory.
61        reclaimed_bytes: u64,
62    },
63    /// Emitted when the fleet's overall health level changes (e.g. green → warning as disk fills),
64    /// so a connected GUI can refresh its always-visible health indicator without polling.
65    FleetHealthChanged {
66        /// Snake-case overall level: `green` | `warning` | `critical`.
67        overall: String,
68    },
69}
70
71impl NodeEvent {
72    /// Returns the SSE event type name for this event.
73    pub fn event_type(&self) -> &'static str {
74        match self {
75            NodeEvent::NodeStarting { .. } => "node_starting",
76            NodeEvent::NodeStarted { .. } => "node_started",
77            NodeEvent::NodeStopping { .. } => "node_stopping",
78            NodeEvent::NodeStopped { .. } => "node_stopped",
79            NodeEvent::NodeCrashed { .. } => "node_crashed",
80            NodeEvent::NodeRestarting { .. } => "node_restarting",
81            NodeEvent::NodeErrored { .. } => "node_errored",
82            NodeEvent::DownloadStarted { .. } => "download_started",
83            NodeEvent::DownloadProgress { .. } => "download_progress",
84            NodeEvent::DownloadComplete { .. } => "download_complete",
85            NodeEvent::NodeUpgraded { .. } => "node_upgraded",
86            NodeEvent::NodeEvicted { .. } => "node_evicted",
87            NodeEvent::FleetHealthChanged { .. } => "fleet_health_changed",
88        }
89    }
90}
91
92/// Trait for receiving node lifecycle events.
93pub trait EventListener: Send + Sync {
94    fn on_event(&self, event: NodeEvent);
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn event_serializes_with_type_tag() {
103        let event = NodeEvent::NodeStarted {
104            node_id: 1,
105            pid: 1234,
106        };
107        let json = serde_json::to_string(&event).unwrap();
108        assert!(json.contains("\"type\":\"node_started\""));
109        assert!(json.contains("\"node_id\":1"));
110        assert!(json.contains("\"pid\":1234"));
111    }
112
113    #[test]
114    fn event_type_matches_serde_tag() {
115        let event = NodeEvent::NodeCrashed {
116            node_id: 2,
117            exit_code: Some(1),
118        };
119        assert_eq!(event.event_type(), "node_crashed");
120
121        // Verify the serde tag matches
122        let json = serde_json::to_string(&event).unwrap();
123        assert!(json.contains(&format!("\"type\":\"{}\"", event.event_type())));
124    }
125
126    #[test]
127    fn event_roundtrips() {
128        let event = NodeEvent::DownloadProgress {
129            bytes: 1024,
130            total: 4096,
131        };
132        let json = serde_json::to_string(&event).unwrap();
133        let deserialized: NodeEvent = serde_json::from_str(&json).unwrap();
134        assert_eq!(deserialized.event_type(), "download_progress");
135    }
136
137    #[test]
138    fn node_upgraded_event_serializes() {
139        let event = NodeEvent::NodeUpgraded {
140            node_id: 3,
141            old_version: "0.10.1".to_string(),
142            new_version: "0.10.11-rc.1".to_string(),
143        };
144        let json = serde_json::to_string(&event).unwrap();
145        assert!(json.contains("\"type\":\"node_upgraded\""));
146        assert!(json.contains("\"old_version\":\"0.10.1\""));
147        assert!(json.contains("\"new_version\":\"0.10.11-rc.1\""));
148        assert_eq!(event.event_type(), "node_upgraded");
149    }
150}