use gloo::net::http::Request; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::HashMap; use std::path::PathBuf; use thiserror::Error; use uuid::Uuid; /// WASM-compatible client for Hero Supervisor OpenRPC server #[derive(Clone)] pub struct WasmSupervisorClient { server_url: String, request_id: u64, } /// Error types for client operations #[derive(Error, Debug)] pub enum WasmClientError { #[error("HTTP request error: {0}")] Http(String), #[error("JSON serialization error: {0}")] Serialization(#[from] serde_json::Error), #[error("Server error: {message}")] Server { message: String }, } /// Result type for client operations pub type WasmClientResult = Result; /// Types of runners supported by the supervisor #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum RunnerType { SALRunner, OSISRunner, VRunner, } impl Default for RunnerType { fn default() -> Self { RunnerType::SALRunner } } /// Process manager types #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ProcessManagerType { Simple, Tmux, } impl Default for ProcessManagerType { fn default() -> Self { ProcessManagerType::Simple } } /// Configuration for an actor runner #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunnerConfig { pub actor_id: String, pub runner_type: RunnerType, pub binary_path: PathBuf, pub script_type: String, pub args: Vec, pub env_vars: HashMap, pub working_dir: Option, pub restart_policy: String, pub health_check_command: Option, pub dependencies: Vec, } /// Job type enumeration #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum JobType { SAL, OSIS, V, } /// Job structure for creating and managing jobs #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Job { pub id: String, pub caller_id: String, pub context_id: String, pub payload: String, pub job_type: JobType, pub runner: String, pub timeout: Option, pub env_vars: HashMap, } /// Process status information #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ProcessStatus { Running, Stopped, Starting, Stopping, Failed, Unknown, } /// Log information structure #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct LogInfo { pub timestamp: String, pub level: String, pub message: String, } impl WasmSupervisorClient { /// Create a new supervisor client pub fn new(server_url: impl Into) -> Self { Self { server_url: server_url.into(), request_id: 0, } } /// Get the server URL pub fn server_url(&self) -> &str { &self.server_url } /// Make a JSON-RPC request async fn make_request(&mut self, method: &str, params: Value) -> WasmClientResult where T: for<'de> Deserialize<'de>, { self.request_id += 1; let request_body = json!({ "jsonrpc": "2.0", "method": method, "params": params, "id": self.request_id }); let response = Request::post(&self.server_url) .header("Content-Type", "application/json") .json(&request_body) .map_err(|e| WasmClientError::Http(e.to_string()))? .send() .await .map_err(|e| WasmClientError::Http(e.to_string()))?; if !response.ok() { return Err(WasmClientError::Http(format!( "HTTP error: {} {}", response.status(), response.status_text() ))); } let response_text = response .text() .await .map_err(|e| WasmClientError::Http(e.to_string()))?; let response_json: Value = serde_json::from_str(&response_text)?; if let Some(error) = response_json.get("error") { return Err(WasmClientError::Server { message: error.get("message") .and_then(|m| m.as_str()) .unwrap_or("Unknown server error") .to_string(), }); } let result = response_json .get("result") .ok_or_else(|| WasmClientError::Server { message: "No result in response".to_string(), })?; serde_json::from_value(result.clone()).map_err(Into::into) } /// Add a new runner to the supervisor pub async fn add_runner( &mut self, config: RunnerConfig, process_manager_type: ProcessManagerType, ) -> WasmClientResult<()> { let params = json!({ "config": config, "process_manager_type": process_manager_type }); self.make_request("add_runner", params).await } /// Remove a runner from the supervisor pub async fn remove_runner(&mut self, actor_id: &str) -> WasmClientResult<()> { let params = json!({ "actor_id": actor_id }); self.make_request("remove_runner", params).await } /// List all runner IDs pub async fn list_runners(&mut self) -> WasmClientResult> { self.make_request("list_runners", json!({})).await } /// Start a specific runner pub async fn start_runner(&mut self, actor_id: &str) -> WasmClientResult<()> { let params = json!({ "actor_id": actor_id }); self.make_request("start_runner", params).await } /// Stop a specific runner pub async fn stop_runner(&mut self, actor_id: &str, force: bool) -> WasmClientResult<()> { let params = json!({ "actor_id": actor_id, "force": force }); self.make_request("stop_runner", params).await } /// Get status of a specific runner pub async fn get_runner_status(&mut self, actor_id: &str) -> WasmClientResult { let params = json!({ "actor_id": actor_id }); self.make_request("get_runner_status", params).await } /// Get logs for a specific runner pub async fn get_runner_logs( &mut self, actor_id: &str, lines: Option, follow: bool, ) -> WasmClientResult> { let params = json!({ "actor_id": actor_id, "lines": lines, "follow": follow }); self.make_request("get_runner_logs", params).await } /// Queue a job to a specific runner pub async fn queue_job_to_runner(&mut self, runner: &str, job: Job) -> WasmClientResult<()> { let params = json!({ "runner": runner, "job": job }); self.make_request("queue_job_to_runner", params).await } /// Queue a job to a specific runner and wait for the result pub async fn queue_and_wait( &mut self, runner: &str, job: Job, timeout_secs: u64, ) -> WasmClientResult> { let params = json!({ "runner": runner, "job": job, "timeout_secs": timeout_secs }); self.make_request("queue_and_wait", params).await } /// Get job result by job ID pub async fn get_job_result(&mut self, job_id: &str) -> WasmClientResult> { let params = json!({ "job_id": job_id }); self.make_request("get_job_result", params).await } /// Get status of all runners pub async fn get_all_runner_status(&mut self) -> WasmClientResult> { self.make_request("get_all_runner_status", json!({})).await } /// Start all runners pub async fn start_all(&mut self) -> WasmClientResult> { self.make_request("start_all", json!({})).await } /// Stop all runners pub async fn stop_all(&mut self, force: bool) -> WasmClientResult> { let params = json!({ "force": force }); self.make_request("stop_all", params).await } } /// Builder for creating jobs with a fluent API #[derive(Debug, Clone, Default)] pub struct JobBuilder { id: Option, caller_id: Option, context_id: Option, payload: Option, job_type: Option, runner: Option, timeout: Option, env_vars: HashMap, } impl JobBuilder { /// Create a new job builder pub fn new() -> Self { Self::default() } /// Set the caller ID for this job pub fn caller_id(mut self, caller_id: impl Into) -> Self { self.caller_id = Some(caller_id.into()); self } /// Set the context ID for this job pub fn context_id(mut self, context_id: impl Into) -> Self { self.context_id = Some(context_id.into()); self } /// Set the payload (script content) for this job pub fn payload(mut self, payload: impl Into) -> Self { self.payload = Some(payload.into()); self } /// Set the job type pub fn job_type(mut self, job_type: JobType) -> Self { self.job_type = Some(job_type); self } /// Set the runner name for this job pub fn runner(mut self, runner: impl Into) -> Self { self.runner = Some(runner.into()); self } /// Set the timeout for job execution pub fn timeout(mut self, timeout_secs: u64) -> Self { self.timeout = Some(timeout_secs); self } /// Set a single environment variable pub fn env_var(mut self, key: impl Into, value: impl Into) -> Self { self.env_vars.insert(key.into(), value.into()); self } /// Set multiple environment variables from a HashMap pub fn env_vars(mut self, env_vars: HashMap) -> Self { self.env_vars = env_vars; self } /// Build the job pub fn build(self) -> WasmClientResult { Ok(Job { id: self.id.unwrap_or_else(|| Uuid::new_v4().to_string()), caller_id: self.caller_id.ok_or_else(|| WasmClientError::Server { message: "caller_id is required".to_string(), })?, context_id: self.context_id.ok_or_else(|| WasmClientError::Server { message: "context_id is required".to_string(), })?, payload: self.payload.ok_or_else(|| WasmClientError::Server { message: "payload is required".to_string(), })?, job_type: self.job_type.ok_or_else(|| WasmClientError::Server { message: "job_type is required".to_string(), })?, runner: self.runner.ok_or_else(|| WasmClientError::Server { message: "runner is required".to_string(), })?, timeout: self.timeout, env_vars: self.env_vars, }) } }