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 when the supervisor detects that a node's on-disk binary has been
48    /// replaced by its auto-upgrade, ahead of the node process restarting.
49    UpgradeScheduled {
50        node_id: u32,
51        pending_version: String,
52    },
53    /// Emitted after the supervisor has respawned a node against its replaced binary and
54    /// observed the new version.
55    NodeUpgraded {
56        node_id: u32,
57        old_version: String,
58        new_version: String,
59    },
60    /// Emitted when the daemon automatically evicts a node to reclaim disk space: its process was
61    /// stopped and its data directory deleted. The node now reports as `Evicted` until dismissed.
62    NodeEvicted {
63        node_id: u32,
64        /// Human-readable explanation of the eviction.
65        reason: String,
66        /// Approximate bytes reclaimed by deleting the node's data directory.
67        reclaimed_bytes: u64,
68    },
69    /// Emitted when the fleet's overall health level changes (e.g. green → warning as disk fills),
70    /// so a connected GUI can refresh its always-visible health indicator without polling.
71    FleetHealthChanged {
72        /// Snake-case overall level: `green` | `warning` | `critical`.
73        overall: String,
74    },
75}
76
77impl NodeEvent {
78    /// Returns the SSE event type name for this event.
79    pub fn event_type(&self) -> &'static str {
80        match self {
81            NodeEvent::NodeStarting { .. } => "node_starting",
82            NodeEvent::NodeStarted { .. } => "node_started",
83            NodeEvent::NodeStopping { .. } => "node_stopping",
84            NodeEvent::NodeStopped { .. } => "node_stopped",
85            NodeEvent::NodeCrashed { .. } => "node_crashed",
86            NodeEvent::NodeRestarting { .. } => "node_restarting",
87            NodeEvent::NodeErrored { .. } => "node_errored",
88            NodeEvent::DownloadStarted { .. } => "download_started",
89            NodeEvent::DownloadProgress { .. } => "download_progress",
90            NodeEvent::DownloadComplete { .. } => "download_complete",
91            NodeEvent::UpgradeScheduled { .. } => "upgrade_scheduled",
92            NodeEvent::NodeUpgraded { .. } => "node_upgraded",
93            NodeEvent::NodeEvicted { .. } => "node_evicted",
94            NodeEvent::FleetHealthChanged { .. } => "fleet_health_changed",
95        }
96    }
97}
98
99/// Trait for receiving node lifecycle events.
100pub trait EventListener: Send + Sync {
101    fn on_event(&self, event: NodeEvent);
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn event_serializes_with_type_tag() {
110        let event = NodeEvent::NodeStarted {
111            node_id: 1,
112            pid: 1234,
113        };
114        let json = serde_json::to_string(&event).unwrap();
115        assert!(json.contains("\"type\":\"node_started\""));
116        assert!(json.contains("\"node_id\":1"));
117        assert!(json.contains("\"pid\":1234"));
118    }
119
120    #[test]
121    fn event_type_matches_serde_tag() {
122        let event = NodeEvent::NodeCrashed {
123            node_id: 2,
124            exit_code: Some(1),
125        };
126        assert_eq!(event.event_type(), "node_crashed");
127
128        // Verify the serde tag matches
129        let json = serde_json::to_string(&event).unwrap();
130        assert!(json.contains(&format!("\"type\":\"{}\"", event.event_type())));
131    }
132
133    #[test]
134    fn event_roundtrips() {
135        let event = NodeEvent::DownloadProgress {
136            bytes: 1024,
137            total: 4096,
138        };
139        let json = serde_json::to_string(&event).unwrap();
140        let deserialized: NodeEvent = serde_json::from_str(&json).unwrap();
141        assert_eq!(deserialized.event_type(), "download_progress");
142    }
143
144    #[test]
145    fn upgrade_scheduled_event_serializes() {
146        let event = NodeEvent::UpgradeScheduled {
147            node_id: 2,
148            pending_version: "0.10.11-rc.1".to_string(),
149        };
150        let json = serde_json::to_string(&event).unwrap();
151        assert!(json.contains("\"type\":\"upgrade_scheduled\""));
152        assert!(json.contains("\"node_id\":2"));
153        assert!(json.contains("\"pending_version\":\"0.10.11-rc.1\""));
154        assert_eq!(event.event_type(), "upgrade_scheduled");
155    }
156
157    #[test]
158    fn node_upgraded_event_serializes() {
159        let event = NodeEvent::NodeUpgraded {
160            node_id: 3,
161            old_version: "0.10.1".to_string(),
162            new_version: "0.10.11-rc.1".to_string(),
163        };
164        let json = serde_json::to_string(&event).unwrap();
165        assert!(json.contains("\"type\":\"node_upgraded\""));
166        assert!(json.contains("\"old_version\":\"0.10.1\""));
167        assert!(json.contains("\"new_version\":\"0.10.11-rc.1\""));
168        assert_eq!(event.event_type(), "node_upgraded");
169    }
170}