//! OpenRPC client for Hero Supervisor //! //! This crate provides a client library for interacting with the Hero Supervisor //! OpenRPC server. It offers a simple, async interface for managing actors and jobs. //! //! ## Features //! //! - **Native client**: Full-featured client for native Rust applications //! - **WASM client**: Browser-compatible client using fetch APIs //! //! ## Usage //! //! ### Native Client //! ```rust //! use hero_supervisor_openrpc_client::SupervisorClient; //! //! let client = SupervisorClient::new("http://localhost:3030")?; //! let runners = client.list_runners().await?; //! ``` //! //! ### WASM Client //! ```rust //! use hero_supervisor_openrpc_client::wasm::WasmSupervisorClient; //! //! let client = WasmSupervisorClient::new("http://localhost:3030".to_string()); //! let runners = client.list_runners().await?; //! ``` use serde::{Deserialize, Serialize}; use thiserror::Error; use serde_json; // Import types from the main supervisor crate // WASM-compatible client module #[cfg(target_arch = "wasm32")] pub mod wasm; // Re-export WASM types for convenience #[cfg(target_arch = "wasm32")] pub use wasm::{WasmSupervisorClient, WasmJobType, WasmRunnerType, create_job_canonical_repr, sign_job_canonical}; // Native client dependencies #[cfg(not(target_arch = "wasm32"))] use jsonrpsee::{ core::client::ClientT, http_client::{HttpClient, HttpClientBuilder}, rpc_params, }; #[cfg(not(target_arch = "wasm32"))] use std::path::PathBuf; /// Client for communicating with Hero Supervisor OpenRPC server #[cfg(not(target_arch = "wasm32"))] pub struct SupervisorClient { client: HttpClient, server_url: String, secret: Option, } /// Error types for client operations #[cfg(not(target_arch = "wasm32"))] #[derive(Error, Debug)] pub enum ClientError { #[error("JSON-RPC error: {0}")] JsonRpc(#[from] jsonrpsee::core::ClientError), #[error("Serialization error: {0}")] Serialization(#[from] serde_json::Error), #[error("HTTP client error: {0}")] Http(String), #[error("Server error: {message}")] Server { message: String }, } /// Error types for WASM client operations #[cfg(target_arch = "wasm32")] #[derive(Error, Debug)] pub enum ClientError { #[error("JSON-RPC error: {0}")] JsonRpc(String), #[error("Serialization error: {0}")] Serialization(#[from] serde_json::Error), #[error("HTTP client error: {0}")] Http(String), #[error("Server error: {message}")] Server { message: String }, #[error("JavaScript error: {0}")] JavaScript(String), #[error("Network error: {0}")] Network(String), } // Implement From for jsonrpsee ClientError for WASM #[cfg(target_arch = "wasm32")] impl From for ClientError { fn from(js_val: wasm_bindgen::JsValue) -> Self { ClientError::JavaScript(format!("{:?}", js_val)) } } /// Result type for client operations pub type ClientResult = Result; /// Configuration for a runner #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RunnerConfig { /// Name of the runner pub name: String, /// Command to run the runner (full command line) pub command: String, /// Optional environment variables #[serde(skip_serializing_if = "Option::is_none")] pub env: Option>, } /// Job result response #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum JobResult { Success { success: String }, Error { error: String }, } /// Job status response #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JobStatusResponse { pub job_id: String, pub status: String, pub created_at: String, pub started_at: Option, pub completed_at: Option, } /// Response from job.run (blocking execution) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JobRunResponse { pub job_id: String, pub status: String, pub result: Option, } /// Response from job.start (non-blocking execution) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JobStartResponse { pub job_id: String, pub status: String, } // Re-export Job types from hero-job crate (both native and WASM) pub use hero_job::{Job, JobStatus, JobError, JobBuilder, JobSignature}; // Re-export Client from hero-job-client (native only, requires Redis) #[cfg(not(target_arch = "wasm32"))] pub use hero_job_client::{Client, ClientBuilder}; /// Process status wrapper for OpenRPC serialization (matches server response) #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ProcessStatusWrapper { Running, Stopped, Starting, Stopping, Error(String), } /// Log information wrapper for OpenRPC serialization (matches server response) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LogInfoWrapper { pub timestamp: String, pub level: String, pub message: String, } /// Supervisor information response containing secret counts and server details #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SupervisorInfo { pub server_url: String, pub admin_secrets_count: usize, pub user_secrets_count: usize, pub register_secrets_count: usize, pub runners_count: usize, } /// Simple ProcessStatus type for native builds to avoid service manager dependency #[cfg(not(target_arch = "wasm32"))] pub type ProcessStatus = ProcessStatusWrapper; /// Re-export types from supervisor crate for native builds #[cfg(not(target_arch = "wasm32"))] pub use hero_supervisor::RunnerStatus; #[cfg(not(target_arch = "wasm32"))] pub use hero_supervisor::runner::LogInfo; /// Type aliases for WASM compatibility #[cfg(target_arch = "wasm32")] pub type ProcessStatus = ProcessStatusWrapper; #[cfg(target_arch = "wasm32")] pub type RunnerStatus = ProcessStatusWrapper; #[cfg(target_arch = "wasm32")] pub type LogInfo = LogInfoWrapper; #[cfg(not(target_arch = "wasm32"))] impl SupervisorClient { /// Create a new supervisor client pub fn new(server_url: impl Into) -> ClientResult { let server_url = server_url.into(); let client = HttpClientBuilder::default() .request_timeout(std::time::Duration::from_secs(30)) .build(&server_url) .map_err(|e| ClientError::Http(e.to_string()))?; Ok(Self { client, server_url, secret: None, }) } /// Create a new supervisor client with authentication secret pub fn with_secret(server_url: impl Into, secret: impl Into) -> ClientResult { let server_url = server_url.into(); let secret = secret.into(); let client = HttpClientBuilder::default() .request_timeout(std::time::Duration::from_secs(30)) .build(&server_url) .map_err(|e| ClientError::Http(e.to_string()))?; Ok(Self { client, server_url, secret: Some(secret), }) } /// Get the server URL pub fn server_url(&self) -> &str { &self.server_url } /// Test connection using OpenRPC discovery method /// This calls the standard `rpc.discover` method that should be available on any OpenRPC server pub async fn discover(&self) -> ClientResult { let result: serde_json::Value = self .client .request("rpc.discover", rpc_params![]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(result) } /// Register a new runner to the supervisor with secret authentication /// The runner name is also used as the queue name pub async fn register_runner( &self, secret: &str, name: &str, ) -> ClientResult<()> { let params = serde_json::json!({ "secret": secret, "name": name }); let _: String = self .client .request("register_runner", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(()) } /// Create a new job without queuing it to a runner pub async fn jobs_create( &self, secret: &str, job: Job, ) -> ClientResult { let params = serde_json::json!({ "secret": secret, "job": job }); let job_id: String = self .client .request("jobs.create", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(job_id) } /// List all jobs pub async fn jobs_list(&self) -> ClientResult> { let jobs: Vec = self .client .request("jobs.list", rpc_params![]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(jobs) } /// Run a job on the appropriate runner and wait for the result (blocking) /// This method queues the job and waits for completion before returning pub async fn job_run( &self, secret: &str, job: Job, timeout: Option, ) -> ClientResult { let mut params = serde_json::json!({ "secret": secret, "job": job }); if let Some(t) = timeout { params["timeout"] = serde_json::json!(t); } let result: JobRunResponse = self .client .request("job.run", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(result) } /// Start a job without waiting for the result (non-blocking) /// This method queues the job and returns immediately with the job_id pub async fn job_start( &self, secret: &str, job: Job, ) -> ClientResult { let params = serde_json::json!({ "secret": secret, "job": job }); let result: JobStartResponse = self .client .request("job.start", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(result) } /// Get the current status of a job pub async fn job_status(&self, job_id: &str) -> ClientResult { let status: JobStatusResponse = self .client .request("job.status", rpc_params![job_id]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(status) } /// Get the result of a completed job (blocks until result is available) pub async fn job_result(&self, job_id: &str) -> ClientResult { let result: JobResult = self .client .request("job.result", rpc_params![job_id]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(result) } /// Remove a runner from the supervisor pub async fn remove_runner(&self, secret: &str, actor_id: &str) -> ClientResult<()> { let params = serde_json::json!({ "secret": secret, "actor_id": actor_id }); let _: () = self .client .request("remove_runner", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(()) } /// List all runner IDs pub async fn list_runners(&self) -> ClientResult> { let runners: Vec = self .client .request("list_runners", rpc_params![]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(runners) } /// Start a specific runner pub async fn start_runner(&self, secret: &str, actor_id: &str) -> ClientResult<()> { let params = serde_json::json!({ "secret": secret, "actor_id": actor_id }); let _: () = self .client .request("start_runner", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(()) } /// Stop a specific runner pub async fn stop_runner(&self, secret: &str, actor_id: &str, force: bool) -> ClientResult<()> { let params = serde_json::json!({ "secret": secret, "actor_id": actor_id, "force": force }); let _: () = self .client .request("stop_runner", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(()) } /// Add a runner to the supervisor pub async fn add_runner(&self, secret: &str, config: RunnerConfig) -> ClientResult<()> { let params = serde_json::json!({ "secret": secret, "config": config }); let _: () = self .client .request("add_runner", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(()) } /// Get status of a specific runner pub async fn get_runner_status(&self, secret: &str, actor_id: &str) -> ClientResult { let params = serde_json::json!({ "secret": secret, "actor_id": actor_id }); let status: RunnerStatus = self .client .request("get_runner_status", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(status) } /// Get logs for a specific runner pub async fn get_runner_logs( &self, actor_id: &str, lines: Option, follow: bool, ) -> ClientResult> { let logs: Vec = self .client .request("get_runner_logs", rpc_params![actor_id, lines, follow]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(logs) } /// Queue a job to a specific runner pub async fn queue_job_to_runner(&self, runner: &str, job: Job) -> ClientResult<()> { let params = serde_json::json!({ "runner": runner, "job": job }); let _: () = self .client .request("queue_job_to_runner", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(()) } /// Queue a job and wait for completion pub async fn queue_and_wait(&self, runner: &str, job: Job, timeout_secs: u64) -> ClientResult> { let params = serde_json::json!({ "runner": runner, "job": job, "timeout_secs": timeout_secs }); let result: Option = self .client .request("queue_and_wait", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(result) } /// Run a job on a specific runner pub async fn run_job(&self, secret: &str, job: Job) -> ClientResult { let params = serde_json::json!({ "secret": secret, "job": job }); let result: JobResult = self .client .request("job.run", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(result) } /// Get job result by job ID pub async fn get_job_result(&self, job_id: &str) -> ClientResult> { let result: Option = self .client .request("get_job_result", rpc_params![job_id]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(result) } /// Get status of all runners pub async fn get_all_runner_status(&self) -> ClientResult> { let statuses: Vec<(String, RunnerStatus)> = self .client .request("get_all_runner_status", rpc_params![]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(statuses) } /// Start all runners pub async fn start_all(&self) -> ClientResult> { let results: Vec<(String, bool)> = self .client .request("start_all", rpc_params![]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(results) } /// Stop all runners pub async fn stop_all(&self, force: bool) -> ClientResult> { let results: Vec<(String, bool)> = self .client .request("stop_all", rpc_params![force]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(results) } /// Get status of all runners (alternative method) pub async fn get_all_status(&self) -> ClientResult> { let statuses: Vec<(String, RunnerStatus)> = self .client .request("get_all_status", rpc_params![]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(statuses) } /// Add a secret to the supervisor pub async fn add_secret( &self, admin_secret: &str, secret_type: &str, secret_value: &str, ) -> ClientResult<()> { let params = serde_json::json!({ "admin_secret": admin_secret, "secret_type": secret_type, "secret_value": secret_value }); let _: () = self .client .request("add_secret", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(()) } /// Remove a secret from the supervisor pub async fn remove_secret( &self, admin_secret: &str, secret_type: &str, secret_value: &str, ) -> ClientResult<()> { let params = serde_json::json!({ "admin_secret": admin_secret, "secret_type": secret_type, "secret_value": secret_value }); let _: () = self .client .request("remove_secret", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(()) } /// List secrets (returns supervisor info including secret counts) pub async fn list_secrets(&self, admin_secret: &str) -> ClientResult { let params = serde_json::json!({ "admin_secret": admin_secret }); let info: SupervisorInfo = self .client .request("list_secrets", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(info) } /// Stop a running job pub async fn job_stop(&self, secret: &str, job_id: &str) -> ClientResult<()> { let params = serde_json::json!({ "secret": secret, "job_id": job_id }); self.client .request("job.stop", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e)) } /// Delete a job from the system pub async fn job_delete(&self, secret: &str, job_id: &str) -> ClientResult<()> { let params = serde_json::json!({ "secret": secret, "job_id": job_id }); self.client .request("job.delete", rpc_params![params]) .await.map_err(|e| ClientError::JsonRpc(e)) } /// Get supervisor information including secret counts pub async fn get_supervisor_info(&self, admin_secret: &str) -> ClientResult { let info: SupervisorInfo = self .client .request("get_supervisor_info", rpc_params![admin_secret]) .await.map_err(|e| ClientError::JsonRpc(e))?; Ok(info) } }