1use crate::error::Result;
13use alien_core::{CommandState, DeploymentModel};
14use async_trait::async_trait;
15use chrono::{DateTime, Utc};
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use std::sync::Arc;
19use tokio::sync::RwLock;
20use uuid::Uuid;
21
22#[derive(Debug, Clone)]
24pub struct CommandMetadata {
25 pub command_id: String,
27 pub deployment_model: DeploymentModel,
33 pub project_id: String,
35}
36
37#[derive(Debug, Clone)]
39pub struct CommandEnvelopeData {
40 pub command_id: String,
41 pub deployment_id: String,
42 pub command: String, pub attempt: u32,
44 pub deadline: Option<DateTime<Utc>>,
45 pub state: CommandState,
46 pub deployment_model: DeploymentModel,
48}
49
50#[derive(Debug, Clone)]
52pub struct CommandStatus {
53 pub command_id: String,
54 pub deployment_id: String,
55 pub command: String, pub state: CommandState,
57 pub attempt: u32,
58 pub deadline: Option<DateTime<Utc>>,
59 pub created_at: DateTime<Utc>,
60 pub dispatched_at: Option<DateTime<Utc>>,
61 pub completed_at: Option<DateTime<Utc>>,
62 pub error: Option<serde_json::Value>,
63 pub request_size_bytes: Option<u64>,
64 pub response_size_bytes: Option<u64>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70struct CommandRecord {
71 id: String,
72 deployment_id: String,
73 command: String,
74 state: CommandState,
75 attempt: u32,
76 deadline: Option<DateTime<Utc>>,
77 created_at: DateTime<Utc>,
78 dispatched_at: Option<DateTime<Utc>>,
79 completed_at: Option<DateTime<Utc>>,
80 request_size_bytes: Option<u64>,
81 response_size_bytes: Option<u64>,
82 error: Option<serde_json::Value>,
83 deployment_model: DeploymentModel,
84 project_id: String,
85}
86
87#[async_trait]
92pub trait CommandRegistry: Send + Sync {
93 async fn create_command(
98 &self,
99 deployment_id: &str,
100 command_name: &str,
101 initial_state: CommandState,
102 deadline: Option<DateTime<Utc>>,
103 request_size_bytes: Option<u64>,
104 ) -> Result<CommandMetadata>;
105
106 async fn get_command_metadata(&self, command_id: &str) -> Result<Option<CommandEnvelopeData>>;
110
111 async fn get_command_status(&self, command_id: &str) -> Result<Option<CommandStatus>>;
115
116 async fn update_command_state(
118 &self,
119 command_id: &str,
120 state: CommandState,
121 dispatched_at: Option<DateTime<Utc>>,
122 completed_at: Option<DateTime<Utc>>,
123 response_size_bytes: Option<u64>,
124 error: Option<serde_json::Value>,
125 ) -> Result<()>;
126
127 async fn increment_attempt(&self, command_id: &str) -> Result<u32>;
131}
132
133pub struct InMemoryCommandRegistry {
137 commands: Arc<RwLock<HashMap<String, CommandRecord>>>,
138 deployment_model: DeploymentModel,
139}
140
141impl InMemoryCommandRegistry {
142 pub fn new() -> Self {
144 Self {
145 commands: Arc::new(RwLock::new(HashMap::new())),
146 deployment_model: DeploymentModel::Pull,
147 }
148 }
149
150 pub fn with_deployment_model(deployment_model: DeploymentModel) -> Self {
152 Self {
153 commands: Arc::new(RwLock::new(HashMap::new())),
154 deployment_model,
155 }
156 }
157
158 #[allow(dead_code)]
160 pub async fn list_command_ids(&self) -> Vec<String> {
161 let commands = self.commands.read().await;
162 commands.keys().cloned().collect()
163 }
164}
165
166impl Default for InMemoryCommandRegistry {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172#[async_trait]
173impl CommandRegistry for InMemoryCommandRegistry {
174 async fn create_command(
175 &self,
176 deployment_id: &str,
177 command_name: &str,
178 initial_state: CommandState,
179 deadline: Option<DateTime<Utc>>,
180 request_size_bytes: Option<u64>,
181 ) -> Result<CommandMetadata> {
182 let command_id = format!("cmd_{}", Uuid::new_v4());
183
184 let record = CommandRecord {
185 id: command_id.clone(),
186 deployment_id: deployment_id.to_string(),
187 command: command_name.to_string(),
188 state: initial_state,
189 attempt: 1,
190 deadline,
191 created_at: Utc::now(),
192 dispatched_at: None,
193 completed_at: None,
194 request_size_bytes,
195 response_size_bytes: None,
196 error: None,
197 deployment_model: self.deployment_model,
198 project_id: "local-dev".to_string(),
199 };
200
201 self.commands
202 .write()
203 .await
204 .insert(command_id.clone(), record);
205
206 Ok(CommandMetadata {
207 command_id,
208 deployment_model: self.deployment_model,
209 project_id: "local-dev".to_string(),
210 })
211 }
212
213 async fn get_command_metadata(&self, command_id: &str) -> Result<Option<CommandEnvelopeData>> {
214 let commands = self.commands.read().await;
215
216 Ok(commands.get(command_id).map(|r| CommandEnvelopeData {
217 command_id: r.id.clone(),
218 deployment_id: r.deployment_id.clone(),
219 command: r.command.clone(),
220 attempt: r.attempt,
221 deadline: r.deadline,
222 state: r.state,
223 deployment_model: r.deployment_model,
224 }))
225 }
226
227 async fn get_command_status(&self, command_id: &str) -> Result<Option<CommandStatus>> {
228 let commands = self.commands.read().await;
229
230 Ok(commands.get(command_id).map(|r| CommandStatus {
231 command_id: r.id.clone(),
232 deployment_id: r.deployment_id.clone(),
233 command: r.command.clone(),
234 state: r.state,
235 attempt: r.attempt,
236 deadline: r.deadline,
237 created_at: r.created_at,
238 dispatched_at: r.dispatched_at,
239 completed_at: r.completed_at,
240 error: r.error.clone(),
241 request_size_bytes: r.request_size_bytes,
242 response_size_bytes: r.response_size_bytes,
243 }))
244 }
245
246 async fn update_command_state(
247 &self,
248 command_id: &str,
249 state: CommandState,
250 dispatched_at: Option<DateTime<Utc>>,
251 completed_at: Option<DateTime<Utc>>,
252 response_size_bytes: Option<u64>,
253 error: Option<serde_json::Value>,
254 ) -> Result<()> {
255 let mut commands = self.commands.write().await;
256
257 if let Some(record) = commands.get_mut(command_id) {
258 record.state = state;
259
260 if let Some(ts) = dispatched_at {
261 record.dispatched_at = Some(ts);
262 }
263
264 if let Some(ts) = completed_at {
265 record.completed_at = Some(ts);
266 }
267
268 if let Some(size) = response_size_bytes {
269 record.response_size_bytes = Some(size);
270 }
271
272 if let Some(err) = error {
273 record.error = Some(err);
274 }
275 }
276
277 Ok(())
278 }
279
280 async fn increment_attempt(&self, command_id: &str) -> Result<u32> {
281 let mut commands = self.commands.write().await;
282
283 if let Some(record) = commands.get_mut(command_id) {
284 record.attempt += 1;
285 Ok(record.attempt)
286 } else {
287 Ok(1) }
289 }
290}