use std::collections::HashMap as StdHashMap; use redis::{AsyncCommands, aio::ConnectionManager}; use serde::Serialize; use serde::de::DeserializeOwned; use serde_json::{Map as JsonMap, Value}; use tokio::sync::Mutex; use crate::models::{ Actor, Context, Flow, FlowStatus, Job, JobStatus, Message, MessageStatus, Runner, }; type Result = std::result::Result>; /// Async Redis driver that saves/loads every model as a Redis hash (HSET), /// using canonical keys as specified in the specs. /// - Complex fields (arrays, maps, nested structs) are JSON-encoded per field /// - Scalars are written as plain strings (numbers/bools as their string representation) /// - On load, each field value is first attempted to parse as JSON; if that fails it is treated as a plain string pub struct RedisDriver { /// Base address, e.g. "127.0.0.1:6379" or "redis://127.0.0.1:6379" base_addr: String, /// Cache of connection managers per DB index managers: Mutex>, } impl RedisDriver { /// Create a new driver for the given Redis address. /// Accepts either "host:port" or "redis://host:port" pub async fn new(addr: impl Into) -> Result { let raw = addr.into(); let base_addr = if raw.starts_with("redis://") { raw } else { format!("redis://{}", raw) }; Ok(Self { base_addr, managers: Mutex::new(StdHashMap::new()), }) } /// Get or create a ConnectionManager for the given DB index. async fn manager_for_db(&self, db: u32) -> Result { { // Fast path: check existing let guard = self.managers.lock().await; if let Some(cm) = guard.get(&db) { return Ok(cm.clone()); } } // Slow path: create a new manager and cache it let url = format!("{}/{}", self.base_addr.trim_end_matches('/'), db); let client = redis::Client::open(url.as_str())?; let cm = client.get_connection_manager().await?; let mut guard = self.managers.lock().await; let entry = guard.entry(db).or_insert(cm); Ok(entry.clone()) } // ----------------------------- // Generic helpers (serde <-> HSET) // ----------------------------- fn struct_to_hset_pairs(value: &T) -> Result> { let json = serde_json::to_value(value)?; let obj = json .as_object() .ok_or("Model must serialize to a JSON object")?; let mut pairs = Vec::with_capacity(obj.len()); for (k, v) in obj { let s = match v { Value::Array(_) | Value::Object(_) => serde_json::to_string(v)?, // complex - store JSON Value::String(s) => s.clone(), // string - plain Value::Number(n) => n.to_string(), // number - plain Value::Bool(b) => b.to_string(), // bool - plain Value::Null => "null".to_string(), // null sentinel }; pairs.push((k.clone(), s)); } Ok(pairs) } fn hmap_to_struct(map: StdHashMap) -> Result { let mut obj = JsonMap::with_capacity(map.len()); for (k, s) in map { // Try parse as JSON first (works for arrays, objects, numbers, booleans, null) // If that fails, fallback to string. match serde_json::from_str::(&s) { Ok(v) => { obj.insert(k, v); } Err(_) => { obj.insert(k, Value::String(s)); } } } let json = Value::Object(obj); let model = serde_json::from_value(json)?; Ok(model) } async fn hset_model(&self, db: u32, key: &str, model: &T) -> Result<()> { let mut cm = self.manager_for_db(db).await?; let pairs = Self::struct_to_hset_pairs(model)?; // Ensure no stale fields let _: u64 = cm.del(key).await.unwrap_or(0); // Write all fields let _: usize = cm.hset_multiple(key, &pairs).await?; Ok(()) } async fn hget_model(&self, db: u32, key: &str) -> Result { let mut cm = self.manager_for_db(db).await?; let map: StdHashMap = cm.hgetall(key).await?; if map.is_empty() { return Err(format!("Key not found: {}", key).into()); } Self::hmap_to_struct(map) } // ----------------------------- // Key helpers (canonical keys) // ----------------------------- fn actor_key(id: u32) -> String { format!("actor:{}", id) } fn context_key(id: u32) -> String { format!("context:{}", id) } fn flow_key(id: u32) -> String { format!("flow:{}", id) } fn runner_key(id: u32) -> String { format!("runner:{}", id) } fn job_key(caller_id: u32, id: u32) -> String { format!("job:{}:{}", caller_id, id) } fn message_key(caller_id: u32, id: u32) -> String { format!("message:{}:{}", caller_id, id) } // ----------------------------- // Context (DB = context.id) // ----------------------------- /// Save a Context in its own DB (db index = context.id) pub async fn save_context(&self, ctx: &Context) -> Result<()> { // We don't have field access; compute db and key via JSON to avoid changing model definitions. // Extract "id" from serialized JSON object. let json = serde_json::to_value(ctx)?; let id = json .get("id") .and_then(|v| v.as_u64()) .ok_or("Context.id missing or not a number")? as u32; let key = Self::context_key(id); self.hset_model(id, &key, ctx).await } /// Load a Context from its own DB (db index = id) pub async fn load_context(&self, id: u32) -> Result { let key = Self::context_key(id); self.hget_model(id, &key).await } // ----------------------------- // Actor // ----------------------------- /// Save an Actor to the given DB (tenant/context DB) pub async fn save_actor(&self, db: u32, actor: &Actor) -> Result<()> { let json = serde_json::to_value(actor)?; let id = json .get("id") .and_then(|v| v.as_u64()) .ok_or("Actor.id missing or not a number")? as u32; let key = Self::actor_key(id); self.hset_model(db, &key, actor).await } /// Load an Actor by id from the given DB pub async fn load_actor(&self, db: u32, id: u32) -> Result { let key = Self::actor_key(id); self.hget_model(db, &key).await } // ----------------------------- // Runner // ----------------------------- pub async fn save_runner(&self, db: u32, runner: &Runner) -> Result<()> { let json = serde_json::to_value(runner)?; let id = json .get("id") .and_then(|v| v.as_u64()) .ok_or("Runner.id missing or not a number")? as u32; let key = Self::runner_key(id); self.hset_model(db, &key, runner).await } pub async fn load_runner(&self, db: u32, id: u32) -> Result { let key = Self::runner_key(id); self.hget_model(db, &key).await } // ----------------------------- // Flow // ----------------------------- pub async fn save_flow(&self, db: u32, flow: &Flow) -> Result<()> { let json = serde_json::to_value(flow)?; let id = json .get("id") .and_then(|v| v.as_u64()) .ok_or("Flow.id missing or not a number")? as u32; let key = Self::flow_key(id); self.hset_model(db, &key, flow).await } pub async fn load_flow(&self, db: u32, id: u32) -> Result { let key = Self::flow_key(id); self.hget_model(db, &key).await } // ----------------------------- // Job // ----------------------------- pub async fn save_job(&self, db: u32, job: &Job) -> Result<()> { let json = serde_json::to_value(job)?; let id = json .get("id") .and_then(|v| v.as_u64()) .ok_or("Job.id missing or not a number")? as u32; let caller_id = json .get("caller_id") .and_then(|v| v.as_u64()) .ok_or("Job.caller_id missing or not a number")? as u32; let key = Self::job_key(caller_id, id); self.hset_model(db, &key, job).await } pub async fn load_job(&self, db: u32, caller_id: u32, id: u32) -> Result { let key = Self::job_key(caller_id, id); self.hget_model(db, &key).await } /// Atomically update a job's status and `updated_at` fields. /// - No transition validation is performed. /// - Writes only the two fields via HSET to avoid rewriting the whole model. pub async fn update_job_status( &self, db: u32, caller_id: u32, id: u32, status: JobStatus, ) -> Result<()> { let mut cm = self.manager_for_db(db).await?; let key = Self::job_key(caller_id, id); // Serialize enum into the same plain string representation stored by create paths let status_str = match serde_json::to_value(&status)? { Value::String(s) => s, v => v.to_string(), }; let ts = crate::time::current_timestamp(); let pairs = vec![ ("status".to_string(), status_str), ("updated_at".to_string(), ts.to_string()), ]; let _: usize = cm.hset_multiple(key, &pairs).await?; Ok(()) } // ----------------------------- // Message // ----------------------------- pub async fn save_message(&self, db: u32, message: &Message) -> Result<()> { let json = serde_json::to_value(message)?; let id = json .get("id") .and_then(|v| v.as_u64()) .ok_or("Message.id missing or not a number")? as u32; let caller_id = json .get("caller_id") .and_then(|v| v.as_u64()) .ok_or("Message.caller_id missing or not a number")? as u32; let key = Self::message_key(caller_id, id); self.hset_model(db, &key, message).await } pub async fn load_message(&self, db: u32, caller_id: u32, id: u32) -> Result { let key = Self::message_key(caller_id, id); self.hget_model(db, &key).await } // ----------------------------- // Partial update helpers // ----------------------------- /// Flow: update only status and updated_at pub async fn update_flow_status(&self, db: u32, id: u32, status: FlowStatus) -> Result<()> { let mut cm = self.manager_for_db(db).await?; let key = Self::flow_key(id); let status_str = match serde_json::to_value(&status)? { Value::String(s) => s, v => v.to_string(), }; let ts = crate::time::current_timestamp(); let pairs = vec![ ("status".to_string(), status_str), ("updated_at".to_string(), ts.to_string()), ]; let _: usize = cm.hset_multiple(key, &pairs).await?; Ok(()) } /// Message: update only status and updated_at pub async fn update_message_status( &self, db: u32, caller_id: u32, id: u32, status: MessageStatus, ) -> Result<()> { let mut cm = self.manager_for_db(db).await?; let key = Self::message_key(caller_id, id); let status_str = match serde_json::to_value(&status)? { Value::String(s) => s, v => v.to_string(), }; let ts = crate::time::current_timestamp(); let pairs = vec![ ("status".to_string(), status_str), ("updated_at".to_string(), ts.to_string()), ]; let _: usize = cm.hset_multiple(key, &pairs).await?; Ok(()) } /// Flow: merge env_vars map and bump updated_at pub async fn update_flow_env_vars_merge( &self, db: u32, id: u32, patch: StdHashMap, ) -> Result<()> { let mut cm = self.manager_for_db(db).await?; let key = Self::flow_key(id); let current: Option = cm.hget(&key, "env_vars").await.ok(); let mut obj = match current .and_then(|s| serde_json::from_str::(&s).ok()) .and_then(|v| v.as_object().cloned()) { Some(m) => m, None => JsonMap::new(), }; for (k, v) in patch { obj.insert(k, Value::String(v)); } let env_vars_str = Value::Object(obj).to_string(); let ts = crate::time::current_timestamp(); let pairs = vec![ ("env_vars".to_string(), env_vars_str), ("updated_at".to_string(), ts.to_string()), ]; let _: usize = cm.hset_multiple(key, &pairs).await?; Ok(()) } /// Flow: merge result map and bump updated_at pub async fn update_flow_result_merge( &self, db: u32, id: u32, patch: StdHashMap, ) -> Result<()> { let mut cm = self.manager_for_db(db).await?; let key = Self::flow_key(id); let current: Option = cm.hget(&key, "result").await.ok(); let mut obj = match current .and_then(|s| serde_json::from_str::(&s).ok()) .and_then(|v| v.as_object().cloned()) { Some(m) => m, None => JsonMap::new(), }; for (k, v) in patch { obj.insert(k, Value::String(v)); } let result_str = Value::Object(obj).to_string(); let ts = crate::time::current_timestamp(); let pairs = vec![ ("result".to_string(), result_str), ("updated_at".to_string(), ts.to_string()), ]; let _: usize = cm.hset_multiple(key, &pairs).await?; Ok(()) } /// Job: merge env_vars map and bump updated_at pub async fn update_job_env_vars_merge( &self, db: u32, caller_id: u32, id: u32, patch: StdHashMap, ) -> Result<()> { let mut cm = self.manager_for_db(db).await?; let key = Self::job_key(caller_id, id); let current: Option = cm.hget(&key, "env_vars").await.ok(); let mut obj = match current .and_then(|s| serde_json::from_str::(&s).ok()) .and_then(|v| v.as_object().cloned()) { Some(m) => m, None => JsonMap::new(), }; for (k, v) in patch { obj.insert(k, Value::String(v)); } let env_vars_str = Value::Object(obj).to_string(); let ts = crate::time::current_timestamp(); let pairs = vec![ ("env_vars".to_string(), env_vars_str), ("updated_at".to_string(), ts.to_string()), ]; let _: usize = cm.hset_multiple(key, &pairs).await?; Ok(()) } /// Job: merge result map and bump updated_at pub async fn update_job_result_merge( &self, db: u32, caller_id: u32, id: u32, patch: StdHashMap, ) -> Result<()> { let mut cm = self.manager_for_db(db).await?; let key = Self::job_key(caller_id, id); let current: Option = cm.hget(&key, "result").await.ok(); let mut obj = match current .and_then(|s| serde_json::from_str::(&s).ok()) .and_then(|v| v.as_object().cloned()) { Some(m) => m, None => JsonMap::new(), }; for (k, v) in patch { obj.insert(k, Value::String(v)); } let result_str = Value::Object(obj).to_string(); let ts = crate::time::current_timestamp(); let pairs = vec![ ("result".to_string(), result_str), ("updated_at".to_string(), ts.to_string()), ]; let _: usize = cm.hset_multiple(key, &pairs).await?; Ok(()) } /// Flow: set jobs list and bump updated_at pub async fn update_flow_jobs_set(&self, db: u32, id: u32, new_jobs: Vec) -> Result<()> { let mut cm = self.manager_for_db(db).await?; let key = Self::flow_key(id); let jobs_str = serde_json::to_string(&new_jobs)?; let ts = crate::time::current_timestamp(); let pairs = vec![ ("jobs".to_string(), jobs_str), ("updated_at".to_string(), ts.to_string()), ]; let _: usize = cm.hset_multiple(key, &pairs).await?; Ok(()) } /// Message: append logs (no dedup) and bump updated_at pub async fn append_message_logs( &self, db: u32, caller_id: u32, id: u32, new_logs: Vec, ) -> Result<()> { let mut cm = self.manager_for_db(db).await?; let key = Self::message_key(caller_id, id); let current: Option = cm.hget(&key, "logs").await.ok(); let mut arr: Vec = current .and_then(|s| serde_json::from_str::(&s).ok()) .and_then(|v| v.as_array().cloned()) .unwrap_or_default(); for l in new_logs { arr.push(Value::String(l)); } let logs_str = Value::Array(arr).to_string(); let ts = crate::time::current_timestamp(); let pairs = vec![ ("logs".to_string(), logs_str), ("updated_at".to_string(), ts.to_string()), ]; let _: usize = cm.hset_multiple(key, &pairs).await?; Ok(()) } // ----------------------------- // Queues (lists) // ----------------------------- /// Push a value onto a Redis list using LPUSH in the given DB. pub async fn lpush_list(&self, db: u32, list: &str, value: &str) -> Result<()> { let mut cm = self.manager_for_db(db).await?; let _: i64 = cm.lpush(list, value).await?; Ok(()) } /// Enqueue a message key onto the outbound queue (msg_out). /// The value is the canonical message key "message:{caller_id}:{id}". pub async fn enqueue_msg_out(&self, db: u32, caller_id: u32, id: u32) -> Result<()> { let key = Self::message_key(caller_id, id); self.lpush_list(db, "msg_out", &key).await } }