end to end job management support
This commit is contained in:
		@@ -17,6 +17,7 @@ futures-util = { workspace = true, features = ["sink"] }
 | 
			
		||||
thiserror = { workspace = true }
 | 
			
		||||
url = { workspace = true }
 | 
			
		||||
http = "0.2"
 | 
			
		||||
hero_job = { path = "../../../core/job" }
 | 
			
		||||
 | 
			
		||||
# Authentication dependencies
 | 
			
		||||
hex = { workspace = true }
 | 
			
		||||
 
 | 
			
		||||
@@ -148,7 +148,7 @@ async fn run_interactive_mode(client: hero_websocket_client::CircleWsClient) ->
 | 
			
		||||
    // Execute the script
 | 
			
		||||
    match client.play(input).await {
 | 
			
		||||
        Ok(result) => {
 | 
			
		||||
            console::log_1(&format!("📤 Result: {}", result.output).into());
 | 
			
		||||
            console::log_1(&format!("📤 Result: {}", result).into());
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            console::log_1(&format!("❌ Script execution failed: {}", e).into());
 | 
			
		||||
@@ -164,7 +164,7 @@ async fn execute_script(client: hero_websocket_client::CircleWsClient, script: S
 | 
			
		||||
    
 | 
			
		||||
    match client.play(script).await {
 | 
			
		||||
        Ok(result) => {
 | 
			
		||||
            console::log_1(&result.output.into());
 | 
			
		||||
            console::log_1(&result.into());
 | 
			
		||||
            Ok(())
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
@@ -209,7 +209,7 @@ async fn execute_script(client: hero_websocket_client::CircleWsClient, script: S
 | 
			
		||||
    
 | 
			
		||||
    match client.play(script).await {
 | 
			
		||||
        Ok(result) => {
 | 
			
		||||
            println!("{}", result.output);
 | 
			
		||||
            println!("{}", result);
 | 
			
		||||
            Ok(())
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
@@ -244,7 +244,7 @@ async fn run_interactive_mode(client: hero_websocket_client::CircleWsClient) ->
 | 
			
		||||
 | 
			
		||||
        match client.play(input).await {
 | 
			
		||||
            Ok(result) => {
 | 
			
		||||
                println!("\n📤 Result: {}", result.output);
 | 
			
		||||
                println!("\n📤 Result: {}", result);
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                error!("❌ Script execution failed: {}", e);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										34
									
								
								interfaces/websocket/client/src/builder.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								interfaces/websocket/client/src/builder.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
use std::sync::{Arc, Mutex};
 | 
			
		||||
use crate::CircleWsClient;
 | 
			
		||||
 | 
			
		||||
// Platform-specific imports removed - not needed in builder.rs
 | 
			
		||||
 | 
			
		||||
pub struct CircleWsClientBuilder {
 | 
			
		||||
    ws_url: String,
 | 
			
		||||
    private_key: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CircleWsClientBuilder {
 | 
			
		||||
    pub fn new(ws_url: String) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            ws_url,
 | 
			
		||||
            private_key: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn with_keypair(mut self, private_key: String) -> Self {
 | 
			
		||||
        self.private_key = Some(private_key);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn build(self) -> CircleWsClient {
 | 
			
		||||
        CircleWsClient {
 | 
			
		||||
            ws_url: self.ws_url,
 | 
			
		||||
            internal_tx: None,
 | 
			
		||||
            #[cfg(not(target_arch = "wasm32"))]
 | 
			
		||||
            task_handle: None,
 | 
			
		||||
            private_key: self.private_key,
 | 
			
		||||
            is_connected: Arc::new(Mutex::new(false)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								interfaces/websocket/client/src/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								interfaces/websocket/client/src/error.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
use thiserror::Error;
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Debug, Clone)]
 | 
			
		||||
pub struct JsonRpcErrorClient {
 | 
			
		||||
    pub code: i32,
 | 
			
		||||
    pub message: String,
 | 
			
		||||
    pub data: Option<Value>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Error, Debug)]
 | 
			
		||||
pub enum CircleWsClientError {
 | 
			
		||||
    #[error("WebSocket connection error: {0}")]
 | 
			
		||||
    ConnectionError(String),
 | 
			
		||||
    #[error("WebSocket send error: {0}")]
 | 
			
		||||
    SendError(String),
 | 
			
		||||
    #[error("WebSocket receive error: {0}")]
 | 
			
		||||
    ReceiveError(String),
 | 
			
		||||
    #[error("JSON serialization/deserialization error: {0}")]
 | 
			
		||||
    JsonError(#[from] serde_json::Error),
 | 
			
		||||
    #[error("Request timed out for request ID: {0}")]
 | 
			
		||||
    Timeout(String),
 | 
			
		||||
    #[error("JSON-RPC error response: {code} - {message}")]
 | 
			
		||||
    JsonRpcError {
 | 
			
		||||
        code: i32,
 | 
			
		||||
        message: String,
 | 
			
		||||
        data: Option<Value>,
 | 
			
		||||
    },
 | 
			
		||||
    #[error("No response received for request ID: {0}")]
 | 
			
		||||
    NoResponse(String),
 | 
			
		||||
    #[error("Client is not connected")]
 | 
			
		||||
    NotConnected,
 | 
			
		||||
    #[error("Internal channel error: {0}")]
 | 
			
		||||
    ChannelError(String),
 | 
			
		||||
    #[error("Authentication error: {0}")]
 | 
			
		||||
    Auth(#[from] crate::auth::AuthError),
 | 
			
		||||
    #[error("Authentication requires a keypair, but none was provided.")]
 | 
			
		||||
    AuthNoKeyPair,
 | 
			
		||||
}
 | 
			
		||||
@@ -5,12 +5,15 @@ use serde::{Deserialize, Serialize};
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::sync::{Arc, Mutex};
 | 
			
		||||
use thiserror::Error;
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
// Authentication module
 | 
			
		||||
pub mod auth;
 | 
			
		||||
pub mod builder;
 | 
			
		||||
pub mod methods;
 | 
			
		||||
pub mod error;
 | 
			
		||||
 | 
			
		||||
pub use error::{JsonRpcErrorClient, CircleWsClientError};
 | 
			
		||||
pub use auth::{AuthCredentials, AuthError, AuthResult};
 | 
			
		||||
 | 
			
		||||
// Platform-specific WebSocket imports and spawn function
 | 
			
		||||
@@ -51,69 +54,6 @@ pub struct JsonRpcResponseClient {
 | 
			
		||||
    pub id: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Debug, Clone)]
 | 
			
		||||
pub struct JsonRpcErrorClient {
 | 
			
		||||
    pub code: i32,
 | 
			
		||||
    pub message: String,
 | 
			
		||||
    pub data: Option<Value>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Debug, Clone)]
 | 
			
		||||
pub struct PlayParamsClient {
 | 
			
		||||
    pub script: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Debug, Clone)]
 | 
			
		||||
pub struct PlayResultClient {
 | 
			
		||||
    pub output: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Debug, Clone)]
 | 
			
		||||
pub struct AuthCredentialsParams {
 | 
			
		||||
    pub pubkey: String,
 | 
			
		||||
    pub signature: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Debug, Clone)]
 | 
			
		||||
pub struct FetchNonceParams {
 | 
			
		||||
    pub pubkey: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Debug, Clone)]
 | 
			
		||||
pub struct FetchNonceResponse {
 | 
			
		||||
    pub nonce: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Error, Debug)]
 | 
			
		||||
pub enum CircleWsClientError {
 | 
			
		||||
    #[error("WebSocket connection error: {0}")]
 | 
			
		||||
    ConnectionError(String),
 | 
			
		||||
    #[error("WebSocket send error: {0}")]
 | 
			
		||||
    SendError(String),
 | 
			
		||||
    #[error("WebSocket receive error: {0}")]
 | 
			
		||||
    ReceiveError(String),
 | 
			
		||||
    #[error("JSON serialization/deserialization error: {0}")]
 | 
			
		||||
    JsonError(#[from] serde_json::Error),
 | 
			
		||||
    #[error("Request timed out for request ID: {0}")]
 | 
			
		||||
    Timeout(String),
 | 
			
		||||
    #[error("JSON-RPC error response: {code} - {message}")]
 | 
			
		||||
    JsonRpcError {
 | 
			
		||||
        code: i32,
 | 
			
		||||
        message: String,
 | 
			
		||||
        data: Option<Value>,
 | 
			
		||||
    },
 | 
			
		||||
    #[error("No response received for request ID: {0}")]
 | 
			
		||||
    NoResponse(String),
 | 
			
		||||
    #[error("Client is not connected")]
 | 
			
		||||
    NotConnected,
 | 
			
		||||
    #[error("Internal channel error: {0}")]
 | 
			
		||||
    ChannelError(String),
 | 
			
		||||
    #[error("Authentication error: {0}")]
 | 
			
		||||
    Auth(#[from] auth::AuthError),
 | 
			
		||||
    #[error("Authentication requires a keypair, but none was provided.")]
 | 
			
		||||
    AuthNoKeyPair,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Wrapper for messages sent to the WebSocket task
 | 
			
		||||
enum InternalWsMessage {
 | 
			
		||||
    SendJsonRpc(
 | 
			
		||||
@@ -183,116 +123,6 @@ impl CircleWsClient {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CircleWsClient {
 | 
			
		||||
    pub async fn authenticate(&mut self) -> Result<bool, CircleWsClientError> {
 | 
			
		||||
        info!("🔐 [{}] Starting authentication process...", self.ws_url);
 | 
			
		||||
        
 | 
			
		||||
        let private_key = self
 | 
			
		||||
            .private_key
 | 
			
		||||
            .as_ref()
 | 
			
		||||
            .ok_or(CircleWsClientError::AuthNoKeyPair)?;
 | 
			
		||||
        
 | 
			
		||||
        info!("🔑 [{}] Deriving public key from private key...", self.ws_url);
 | 
			
		||||
        let public_key = auth::derive_public_key(private_key)?;
 | 
			
		||||
        info!("✅ [{}] Public key derived: {}...", self.ws_url, &public_key[..8]);
 | 
			
		||||
 | 
			
		||||
        info!("🎫 [{}] Fetching authentication nonce...", self.ws_url);
 | 
			
		||||
        let nonce = self.fetch_nonce(&public_key).await?;
 | 
			
		||||
        info!("✅ [{}] Nonce received: {}...", self.ws_url, &nonce[..8]);
 | 
			
		||||
 | 
			
		||||
        info!("✍️  [{}] Signing nonce with private key...", self.ws_url);
 | 
			
		||||
        let signature = auth::sign_message(private_key, &nonce)?;
 | 
			
		||||
        info!("✅ [{}] Signature created: {}...", self.ws_url, &signature[..8]);
 | 
			
		||||
 | 
			
		||||
        info!("🔒 [{}] Submitting authentication credentials...", self.ws_url);
 | 
			
		||||
        let result = self.authenticate_with_signature(&public_key, &signature).await?;
 | 
			
		||||
        
 | 
			
		||||
        if result {
 | 
			
		||||
            info!("🎉 [{}] Authentication successful!", self.ws_url);
 | 
			
		||||
        } else {
 | 
			
		||||
            error!("❌ [{}] Authentication failed - server rejected credentials", self.ws_url);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Ok(result)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn fetch_nonce(&self, pubkey: &str) -> Result<String, CircleWsClientError> {
 | 
			
		||||
        info!("📡 [{}] Sending fetch_nonce request for pubkey: {}...", self.ws_url, &pubkey[..8]);
 | 
			
		||||
        
 | 
			
		||||
        let params = FetchNonceParams {
 | 
			
		||||
            pubkey: pubkey.to_string(),
 | 
			
		||||
        };
 | 
			
		||||
        let req = self.create_request("fetch_nonce", params)?;
 | 
			
		||||
        let res = self.send_request(req).await?;
 | 
			
		||||
 | 
			
		||||
        if let Some(err) = res.error {
 | 
			
		||||
            error!("❌ [{}] fetch_nonce failed: {} (code: {})", self.ws_url, err.message, err.code);
 | 
			
		||||
            return Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: err.code,
 | 
			
		||||
                message: err.message,
 | 
			
		||||
                data: err.data,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let nonce_res: FetchNonceResponse = serde_json::from_value(res.result.unwrap_or_default())?;
 | 
			
		||||
        info!("✅ [{}] fetch_nonce successful, nonce length: {}", self.ws_url, nonce_res.nonce.len());
 | 
			
		||||
        Ok(nonce_res.nonce)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn authenticate_with_signature(
 | 
			
		||||
        &self,
 | 
			
		||||
        pubkey: &str,
 | 
			
		||||
        signature: &str,
 | 
			
		||||
    ) -> Result<bool, CircleWsClientError> {
 | 
			
		||||
        info!("📡 [{}] Sending authenticate request with signature...", self.ws_url);
 | 
			
		||||
        
 | 
			
		||||
        let params = AuthCredentialsParams {
 | 
			
		||||
            pubkey: pubkey.to_string(),
 | 
			
		||||
            signature: signature.to_string(),
 | 
			
		||||
        };
 | 
			
		||||
        let req = self.create_request("authenticate", params)?;
 | 
			
		||||
        let res = self.send_request(req).await?;
 | 
			
		||||
 | 
			
		||||
        if let Some(err) = res.error {
 | 
			
		||||
            error!("❌ [{}] authenticate failed: {} (code: {})", self.ws_url, err.message, err.code);
 | 
			
		||||
            return Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: err.code,
 | 
			
		||||
                message: err.message,
 | 
			
		||||
                data: err.data,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let authenticated = res
 | 
			
		||||
            .result
 | 
			
		||||
            .and_then(|v| v.get("authenticated").and_then(|v| v.as_bool()))
 | 
			
		||||
            .unwrap_or(false);
 | 
			
		||||
            
 | 
			
		||||
        if authenticated {
 | 
			
		||||
            info!("✅ [{}] authenticate request successful - server confirmed authentication", self.ws_url);
 | 
			
		||||
        } else {
 | 
			
		||||
            error!("❌ [{}] authenticate request failed - server returned false", self.ws_url);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Ok(authenticated)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Call the whoami method to get authentication status and user information
 | 
			
		||||
    pub async fn whoami(&self) -> Result<Value, CircleWsClientError> {
 | 
			
		||||
        let req = self.create_request("whoami", serde_json::json!({}))?;
 | 
			
		||||
        let response = self.send_request(req).await?;
 | 
			
		||||
        
 | 
			
		||||
        if let Some(result) = response.result {
 | 
			
		||||
            Ok(result)
 | 
			
		||||
        } else if let Some(error) = response.error {
 | 
			
		||||
            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: error.code,
 | 
			
		||||
                message: error.message,
 | 
			
		||||
                data: error.data,
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(CircleWsClientError::NoResponse("whoami".to_string()))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn create_request<T: Serialize>(
 | 
			
		||||
        &self,
 | 
			
		||||
        method: &str,
 | 
			
		||||
@@ -676,7 +506,7 @@ impl CircleWsClient {
 | 
			
		||||
        ws_conn: tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
 | 
			
		||||
        mut internal_rx: mpsc::Receiver<InternalWsMessage>,
 | 
			
		||||
        pending_requests: &Arc<Mutex<HashMap<String, oneshot::Sender<Result<JsonRpcResponseClient, CircleWsClientError>>>>>,
 | 
			
		||||
        log_url: &str,
 | 
			
		||||
        _log_url: &str,
 | 
			
		||||
        _is_connected: &Arc<Mutex<bool>>,
 | 
			
		||||
    ) -> String {
 | 
			
		||||
        let (mut ws_tx, mut ws_rx) = ws_conn.split();
 | 
			
		||||
@@ -811,141 +641,6 @@ impl CircleWsClient {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn play(
 | 
			
		||||
        &self,
 | 
			
		||||
        script: String,
 | 
			
		||||
    ) -> impl std::future::Future<Output = Result<PlayResultClient, CircleWsClientError>> + Send + 'static
 | 
			
		||||
    {
 | 
			
		||||
        let req_id_outer = Uuid::new_v4().to_string();
 | 
			
		||||
 | 
			
		||||
        // Clone the sender option. The sender itself (mpsc::Sender) is also Clone.
 | 
			
		||||
        let internal_tx_clone_opt = self.internal_tx.clone();
 | 
			
		||||
 | 
			
		||||
        async move {
 | 
			
		||||
            let req_id = req_id_outer; // Move req_id into the async block
 | 
			
		||||
            let params = PlayParamsClient { script }; // script is moved in
 | 
			
		||||
 | 
			
		||||
            let request = match serde_json::to_value(params) {
 | 
			
		||||
                Ok(p_val) => JsonRpcRequestClient {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    method: "play".to_string(),
 | 
			
		||||
                    params: p_val,
 | 
			
		||||
                    id: req_id.clone(),
 | 
			
		||||
                },
 | 
			
		||||
                Err(e) => return Err(CircleWsClientError::JsonError(e)),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let (response_tx, response_rx) = oneshot::channel();
 | 
			
		||||
 | 
			
		||||
            if let Some(mut internal_tx) = internal_tx_clone_opt {
 | 
			
		||||
                internal_tx
 | 
			
		||||
                    .send(InternalWsMessage::SendJsonRpc(request, response_tx))
 | 
			
		||||
                    .await
 | 
			
		||||
                    .map_err(|e| {
 | 
			
		||||
                        CircleWsClientError::ChannelError(format!(
 | 
			
		||||
                            "Failed to send request to internal task: {}",
 | 
			
		||||
                            e
 | 
			
		||||
                        ))
 | 
			
		||||
                    })?;
 | 
			
		||||
            } else {
 | 
			
		||||
                return Err(CircleWsClientError::NotConnected);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Add a timeout for waiting for the response
 | 
			
		||||
            // For simplicity, using a fixed timeout here. Could be configurable.
 | 
			
		||||
            #[cfg(target_arch = "wasm32")]
 | 
			
		||||
            {
 | 
			
		||||
                match response_rx.await {
 | 
			
		||||
                    Ok(Ok(rpc_response)) => {
 | 
			
		||||
                        if let Some(json_rpc_error) = rpc_response.error {
 | 
			
		||||
                            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                                code: json_rpc_error.code,
 | 
			
		||||
                                message: json_rpc_error.message,
 | 
			
		||||
                                data: json_rpc_error.data,
 | 
			
		||||
                            })
 | 
			
		||||
                        } else if let Some(result_value) = rpc_response.result {
 | 
			
		||||
                            serde_json::from_value(result_value)
 | 
			
		||||
                                .map_err(CircleWsClientError::JsonError)
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Err(CircleWsClientError::NoResponse(req_id.clone()))
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    Ok(Err(e)) => Err(e), // Error propagated from the ws task
 | 
			
		||||
                    Err(_) => Err(CircleWsClientError::Timeout(req_id.clone())), // oneshot channel cancelled
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            #[cfg(not(target_arch = "wasm32"))]
 | 
			
		||||
            {
 | 
			
		||||
                use tokio::time::timeout as tokio_timeout;
 | 
			
		||||
                match tokio_timeout(std::time::Duration::from_secs(10), response_rx).await {
 | 
			
		||||
                    Ok(Ok(Ok(rpc_response))) => {
 | 
			
		||||
                        // Timeout -> Result<ChannelRecvResult, Error>
 | 
			
		||||
                        if let Some(json_rpc_error) = rpc_response.error {
 | 
			
		||||
                            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                                code: json_rpc_error.code,
 | 
			
		||||
                                message: json_rpc_error.message,
 | 
			
		||||
                                data: json_rpc_error.data,
 | 
			
		||||
                            })
 | 
			
		||||
                        } else if let Some(result_value) = rpc_response.result {
 | 
			
		||||
                            serde_json::from_value(result_value)
 | 
			
		||||
                                .map_err(CircleWsClientError::JsonError)
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Err(CircleWsClientError::NoResponse(req_id.clone()))
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    Ok(Ok(Err(e))) => Err(e), // Error propagated from the ws task
 | 
			
		||||
                    Ok(Err(_)) => Err(CircleWsClientError::ChannelError(
 | 
			
		||||
                        "Response channel cancelled".to_string(),
 | 
			
		||||
                    )), // oneshot cancelled
 | 
			
		||||
                    Err(_) => Err(CircleWsClientError::Timeout(req_id.clone())), // tokio_timeout expired
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Send a plaintext ping message and wait for pong response
 | 
			
		||||
    pub async fn ping(&mut self) -> Result<String, CircleWsClientError> {
 | 
			
		||||
        if let Some(mut tx) = self.internal_tx.clone() {
 | 
			
		||||
            let (response_tx, response_rx) = oneshot::channel();
 | 
			
		||||
            
 | 
			
		||||
            // Send plaintext ping message
 | 
			
		||||
            tx.send(InternalWsMessage::SendPlaintext("ping".to_string(), response_tx))
 | 
			
		||||
                .await
 | 
			
		||||
                .map_err(|e| {
 | 
			
		||||
                    CircleWsClientError::ChannelError(format!(
 | 
			
		||||
                        "Failed to send ping request to internal task: {}",
 | 
			
		||||
                        e
 | 
			
		||||
                    ))
 | 
			
		||||
                })?;
 | 
			
		||||
            
 | 
			
		||||
            // Wait for pong response with timeout
 | 
			
		||||
            #[cfg(target_arch = "wasm32")]
 | 
			
		||||
            {
 | 
			
		||||
                match response_rx.await {
 | 
			
		||||
                    Ok(Ok(response)) => Ok(response),
 | 
			
		||||
                    Ok(Err(e)) => Err(e),
 | 
			
		||||
                    Err(_) => Err(CircleWsClientError::ChannelError(
 | 
			
		||||
                        "Ping response channel cancelled".to_string(),
 | 
			
		||||
                    )),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            #[cfg(not(target_arch = "wasm32"))]
 | 
			
		||||
            {
 | 
			
		||||
                use tokio::time::timeout as tokio_timeout;
 | 
			
		||||
                match tokio_timeout(std::time::Duration::from_secs(10), response_rx).await {
 | 
			
		||||
                    Ok(Ok(Ok(response))) => Ok(response),
 | 
			
		||||
                    Ok(Ok(Err(e))) => Err(e),
 | 
			
		||||
                    Ok(Err(_)) => Err(CircleWsClientError::ChannelError(
 | 
			
		||||
                        "Ping response channel cancelled".to_string(),
 | 
			
		||||
                    )),
 | 
			
		||||
                    Err(_) => Err(CircleWsClientError::Timeout("ping".to_string())),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(CircleWsClientError::NotConnected)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn disconnect(&mut self) {
 | 
			
		||||
        if let Some(mut tx) = self.internal_tx.take() {
 | 
			
		||||
            info!("Sending close signal to internal WebSocket task.");
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										500
									
								
								interfaces/websocket/client/src/methods.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										500
									
								
								interfaces/websocket/client/src/methods.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,500 @@
 | 
			
		||||
use futures_channel::oneshot;
 | 
			
		||||
use futures_util::SinkExt;
 | 
			
		||||
use log::{error, info};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
use hero_job::{Job, JobStatus};
 | 
			
		||||
 | 
			
		||||
use crate::{CircleWsClient, CircleWsClientError, InternalWsMessage, JsonRpcRequestClient, auth};
 | 
			
		||||
 | 
			
		||||
// Platform-specific WebSocket imports removed - not needed in methods.rs
 | 
			
		||||
 | 
			
		||||
// JSON-RPC structures are imported from crate root
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Debug, Clone)]
 | 
			
		||||
pub struct PlayParamsClient {
 | 
			
		||||
    pub script: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Debug, Clone)]
 | 
			
		||||
pub struct PlayResultClient {
 | 
			
		||||
    pub output: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Debug, Clone)]
 | 
			
		||||
pub struct AuthCredentialsParams {
 | 
			
		||||
    pub pubkey: String,
 | 
			
		||||
    pub signature: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
pub struct JobLogsResult {
 | 
			
		||||
    pub logs: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
pub struct ClearJobsResult {
 | 
			
		||||
    pub deleted_count: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CircleWsClient {
 | 
			
		||||
    pub async fn authenticate(&mut self) -> Result<bool, CircleWsClientError> {
 | 
			
		||||
        info!("🔐 [{}] Starting authentication process...", self.ws_url);
 | 
			
		||||
        
 | 
			
		||||
        let private_key = self
 | 
			
		||||
            .private_key
 | 
			
		||||
            .as_ref()
 | 
			
		||||
            .ok_or(CircleWsClientError::AuthNoKeyPair)?;
 | 
			
		||||
        
 | 
			
		||||
        info!("🔑 [{}] Deriving public key from private key...", self.ws_url);
 | 
			
		||||
        let public_key = auth::derive_public_key(private_key)?;
 | 
			
		||||
        info!("✅ [{}] Public key derived: {}...", self.ws_url, &public_key[..8]);
 | 
			
		||||
 | 
			
		||||
        info!("🎫 [{}] Fetching authentication nonce...", self.ws_url);
 | 
			
		||||
        let nonce = self.fetch_nonce(&public_key).await?;
 | 
			
		||||
        info!("✅ [{}] Nonce received: {}...", self.ws_url, &nonce[..8]);
 | 
			
		||||
 | 
			
		||||
        info!("✍️  [{}] Signing nonce with private key...", self.ws_url);
 | 
			
		||||
        let signature = auth::sign_message(private_key, &nonce)?;
 | 
			
		||||
        info!("✅ [{}] Signature created: {}...", self.ws_url, &signature[..8]);
 | 
			
		||||
 | 
			
		||||
        info!("🔒 [{}] Submitting authentication credentials...", self.ws_url);
 | 
			
		||||
        let result = self.authenticate_with_signature(&public_key, &signature).await?;
 | 
			
		||||
        
 | 
			
		||||
        if result {
 | 
			
		||||
            info!("🎉 [{}] Authentication successful!", self.ws_url);
 | 
			
		||||
        } else {
 | 
			
		||||
            error!("❌ [{}] Authentication failed - server rejected credentials", self.ws_url);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Ok(result)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn fetch_nonce(&self, pubkey: &str) -> Result<String, CircleWsClientError> {
 | 
			
		||||
        info!("📡 [{}] Sending fetch_nonce request for pubkey: {}...", self.ws_url, &pubkey[..8]);
 | 
			
		||||
        let req = self.create_request("fetch_nonce", pubkey.to_string())?;
 | 
			
		||||
        let res = self.send_request(req).await?;
 | 
			
		||||
 | 
			
		||||
        if let Some(err) = res.error {
 | 
			
		||||
            error!("❌ [{}] fetch_nonce failed: {} (code: {})", self.ws_url, err.message, err.code);
 | 
			
		||||
            return Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: err.code,
 | 
			
		||||
                message: err.message,
 | 
			
		||||
                data: err.data,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let nonce = res.result
 | 
			
		||||
            .and_then(|v| v.as_str().map(|s| s.to_string()))
 | 
			
		||||
            .ok_or_else(|| CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: -32603,
 | 
			
		||||
                message: "Invalid nonce response format".to_string(),
 | 
			
		||||
                data: None,
 | 
			
		||||
            })?;
 | 
			
		||||
        
 | 
			
		||||
        info!("✅ [{}] fetch_nonce successful, nonce length: {}", self.ws_url, nonce.len());
 | 
			
		||||
        Ok(nonce)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn authenticate_with_signature(
 | 
			
		||||
        &self,
 | 
			
		||||
        pubkey: &str,
 | 
			
		||||
        signature: &str,
 | 
			
		||||
    ) -> Result<bool, CircleWsClientError> {
 | 
			
		||||
        info!("📡 [{}] Sending authenticate request with signature...", self.ws_url);
 | 
			
		||||
        
 | 
			
		||||
        let params = AuthCredentialsParams {
 | 
			
		||||
            pubkey: pubkey.to_string(),
 | 
			
		||||
            signature: signature.to_string(),
 | 
			
		||||
        };
 | 
			
		||||
        let req = self.create_request("authenticate", params)?;
 | 
			
		||||
        let res = self.send_request(req).await?;
 | 
			
		||||
 | 
			
		||||
        if let Some(err) = res.error {
 | 
			
		||||
            error!("❌ [{}] authenticate failed: {} (code: {})", self.ws_url, err.message, err.code);
 | 
			
		||||
            return Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: err.code,
 | 
			
		||||
                message: err.message,
 | 
			
		||||
                data: err.data,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let authenticated = res
 | 
			
		||||
            .result
 | 
			
		||||
            .and_then(|v| v.get("authenticated").and_then(|v| v.as_bool()))
 | 
			
		||||
            .unwrap_or(false);
 | 
			
		||||
            
 | 
			
		||||
        if authenticated {
 | 
			
		||||
            info!("✅ [{}] authenticate request successful - server confirmed authentication", self.ws_url);
 | 
			
		||||
        } else {
 | 
			
		||||
            error!("❌ [{}] authenticate request failed - server returned false", self.ws_url);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Ok(authenticated)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Call the whoami method to get authentication status and user information
 | 
			
		||||
    pub async fn whoami(&self) -> Result<Value, CircleWsClientError> {
 | 
			
		||||
        let req = self.create_request("whoami", serde_json::json!({}))?;
 | 
			
		||||
        let response = self.send_request(req).await?;
 | 
			
		||||
        
 | 
			
		||||
        if let Some(result) = response.result {
 | 
			
		||||
            Ok(result)
 | 
			
		||||
        } else if let Some(error) = response.error {
 | 
			
		||||
            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: error.code,
 | 
			
		||||
                message: error.message,
 | 
			
		||||
                data: error.data,
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(CircleWsClientError::NoResponse("whoami".to_string()))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn play(
 | 
			
		||||
        &self,
 | 
			
		||||
        script: String,
 | 
			
		||||
    ) -> impl std::future::Future<Output = Result<String, CircleWsClientError>> + Send + 'static
 | 
			
		||||
    {
 | 
			
		||||
        let req_id_outer = Uuid::new_v4().to_string();
 | 
			
		||||
 | 
			
		||||
        // Clone the sender option. The sender itself (mpsc::Sender) is also Clone.
 | 
			
		||||
        let internal_tx_clone_opt = self.internal_tx.clone();
 | 
			
		||||
 | 
			
		||||
        async move {
 | 
			
		||||
            let req_id = req_id_outer; // Move req_id into the async block
 | 
			
		||||
            let params = PlayParamsClient { script }; // script is moved in
 | 
			
		||||
 | 
			
		||||
            let request = match serde_json::to_value(params) {
 | 
			
		||||
                Ok(p_val) => JsonRpcRequestClient {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    method: "play".to_string(),
 | 
			
		||||
                    params: p_val,
 | 
			
		||||
                    id: req_id.clone(),
 | 
			
		||||
                },
 | 
			
		||||
                Err(e) => return Err(CircleWsClientError::JsonError(e)),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let (response_tx, response_rx) = oneshot::channel();
 | 
			
		||||
 | 
			
		||||
            if let Some(mut internal_tx) = internal_tx_clone_opt {
 | 
			
		||||
                internal_tx
 | 
			
		||||
                    .send(InternalWsMessage::SendJsonRpc(request, response_tx))
 | 
			
		||||
                    .await
 | 
			
		||||
                    .map_err(|e| {
 | 
			
		||||
                        CircleWsClientError::ChannelError(format!(
 | 
			
		||||
                            "Failed to send request to internal task: {}",
 | 
			
		||||
                            e
 | 
			
		||||
                        ))
 | 
			
		||||
                    })?;
 | 
			
		||||
            } else {
 | 
			
		||||
                return Err(CircleWsClientError::NotConnected);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Add a timeout for waiting for the response
 | 
			
		||||
            // For simplicity, using a fixed timeout here. Could be configurable.
 | 
			
		||||
            #[cfg(target_arch = "wasm32")]
 | 
			
		||||
            {
 | 
			
		||||
                match response_rx.await {
 | 
			
		||||
                    Ok(Ok(rpc_response)) => {
 | 
			
		||||
                        if let Some(json_rpc_error) = rpc_response.error {
 | 
			
		||||
                            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                                code: json_rpc_error.code,
 | 
			
		||||
                                message: json_rpc_error.message,
 | 
			
		||||
                                data: json_rpc_error.data,
 | 
			
		||||
                            })
 | 
			
		||||
                        } else if let Some(result_value) = rpc_response.result {
 | 
			
		||||
                            // Extract output string directly from the result
 | 
			
		||||
                            if let Some(output) = result_value.get("output").and_then(|v| v.as_str()) {
 | 
			
		||||
                                Ok(output.to_string())
 | 
			
		||||
                            } else {
 | 
			
		||||
                                Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                                    code: -32603,
 | 
			
		||||
                                    message: "Invalid play response format - missing output field".to_string(),
 | 
			
		||||
                                    data: None,
 | 
			
		||||
                                })
 | 
			
		||||
                            }
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Err(CircleWsClientError::NoResponse(req_id.clone()))
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    Ok(Err(e)) => Err(e), // Error propagated from the ws task
 | 
			
		||||
                    Err(_) => Err(CircleWsClientError::Timeout(req_id.clone())), // oneshot channel cancelled
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            #[cfg(not(target_arch = "wasm32"))]
 | 
			
		||||
            {
 | 
			
		||||
                use tokio::time::timeout as tokio_timeout;
 | 
			
		||||
                match tokio_timeout(std::time::Duration::from_secs(10), response_rx).await {
 | 
			
		||||
                    Ok(Ok(Ok(rpc_response))) => {
 | 
			
		||||
                        // Timeout -> Result<ChannelRecvResult, Error>
 | 
			
		||||
                        if let Some(json_rpc_error) = rpc_response.error {
 | 
			
		||||
                            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                                code: json_rpc_error.code,
 | 
			
		||||
                                message: json_rpc_error.message,
 | 
			
		||||
                                data: json_rpc_error.data,
 | 
			
		||||
                            })
 | 
			
		||||
                        } else if let Some(result_value) = rpc_response.result {
 | 
			
		||||
                            // Extract output string directly from the result
 | 
			
		||||
                            if let Some(output) = result_value.get("output").and_then(|v| v.as_str()) {
 | 
			
		||||
                                Ok(output.to_string())
 | 
			
		||||
                            } else {
 | 
			
		||||
                                Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                                    code: -32603,
 | 
			
		||||
                                    message: "Invalid play response format - missing output field".to_string(),
 | 
			
		||||
                                    data: None,
 | 
			
		||||
                                })
 | 
			
		||||
                            }
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Err(CircleWsClientError::NoResponse(req_id.clone()))
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    Ok(Ok(Err(e))) => Err(e), // Error propagated from the ws task
 | 
			
		||||
                    Ok(Err(_)) => Err(CircleWsClientError::ChannelError(
 | 
			
		||||
                        "Response channel cancelled".to_string(),
 | 
			
		||||
                    )), // oneshot cancelled
 | 
			
		||||
                    Err(_) => Err(CircleWsClientError::Timeout(req_id.clone())), // tokio_timeout expired
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Send a plaintext ping message and wait for pong response
 | 
			
		||||
    pub async fn ping(&mut self) -> Result<String, CircleWsClientError> {
 | 
			
		||||
        if let Some(mut tx) = self.internal_tx.clone() {
 | 
			
		||||
            let (response_tx, response_rx) = oneshot::channel();
 | 
			
		||||
            
 | 
			
		||||
            // Send plaintext ping message
 | 
			
		||||
            tx.send(InternalWsMessage::SendPlaintext("ping".to_string(), response_tx))
 | 
			
		||||
                .await
 | 
			
		||||
                .map_err(|e| {
 | 
			
		||||
                    CircleWsClientError::ChannelError(format!(
 | 
			
		||||
                        "Failed to send ping request to internal task: {}",
 | 
			
		||||
                        e
 | 
			
		||||
                    ))
 | 
			
		||||
                })?;
 | 
			
		||||
            
 | 
			
		||||
            // Wait for pong response with timeout
 | 
			
		||||
            #[cfg(target_arch = "wasm32")]
 | 
			
		||||
            {
 | 
			
		||||
                match response_rx.await {
 | 
			
		||||
                    Ok(Ok(response)) => Ok(response),
 | 
			
		||||
                    Ok(Err(e)) => Err(e),
 | 
			
		||||
                    Err(_) => Err(CircleWsClientError::ChannelError(
 | 
			
		||||
                        "Ping response channel cancelled".to_string(),
 | 
			
		||||
                    )),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            #[cfg(not(target_arch = "wasm32"))]
 | 
			
		||||
            {
 | 
			
		||||
                use tokio::time::timeout as tokio_timeout;
 | 
			
		||||
                match tokio_timeout(std::time::Duration::from_secs(10), response_rx).await {
 | 
			
		||||
                    Ok(Ok(Ok(response))) => Ok(response),
 | 
			
		||||
                    Ok(Ok(Err(e))) => Err(e),
 | 
			
		||||
                    Ok(Err(_)) => Err(CircleWsClientError::ChannelError(
 | 
			
		||||
                        "Ping response channel cancelled".to_string(),
 | 
			
		||||
                    )),
 | 
			
		||||
                    Err(_) => Err(CircleWsClientError::Timeout("ping".to_string())),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(CircleWsClientError::NotConnected)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Create a new job without starting it
 | 
			
		||||
    pub async fn create_job(
 | 
			
		||||
        &self,
 | 
			
		||||
        job: Job,
 | 
			
		||||
    ) -> Result<String, CircleWsClientError> {
 | 
			
		||||
        
 | 
			
		||||
        let req = self.create_request("create_job", job)?;
 | 
			
		||||
        let response = self.send_request(req).await?;
 | 
			
		||||
        
 | 
			
		||||
        if let Some(result) = response.result {
 | 
			
		||||
            serde_json::from_value(result).map_err(CircleWsClientError::JsonError)
 | 
			
		||||
        } else if let Some(error) = response.error {
 | 
			
		||||
            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: error.code,
 | 
			
		||||
                message: error.message,
 | 
			
		||||
                data: error.data,
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(CircleWsClientError::ReceiveError(
 | 
			
		||||
                "No result or error in response".to_string(),
 | 
			
		||||
            ))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Create and run a job, returning the result when complete
 | 
			
		||||
    pub async fn run_job(
 | 
			
		||||
        &self,
 | 
			
		||||
        job: &Job,
 | 
			
		||||
    ) -> Result<String, CircleWsClientError> {
 | 
			
		||||
        let req = self.create_request("run_job", job)?;
 | 
			
		||||
        let response = self.send_request(req).await?;
 | 
			
		||||
        
 | 
			
		||||
        if let Some(result) = response.result {
 | 
			
		||||
            serde_json::from_value(result).map_err(CircleWsClientError::JsonError)
 | 
			
		||||
        } else if let Some(error) = response.error {
 | 
			
		||||
            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: error.code,
 | 
			
		||||
                message: error.message,
 | 
			
		||||
                data: error.data,
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(CircleWsClientError::ReceiveError(
 | 
			
		||||
                "No result or error in response".to_string(),
 | 
			
		||||
            ))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the status of a job
 | 
			
		||||
    pub async fn get_job_status(&self, job_id: String) -> Result<JobStatus, CircleWsClientError> {
 | 
			
		||||
        let params = job_id;
 | 
			
		||||
        
 | 
			
		||||
        let req = self.create_request("get_job_status", params)?;
 | 
			
		||||
        let response = self.send_request(req).await?;
 | 
			
		||||
        
 | 
			
		||||
        if let Some(result) = response.result {
 | 
			
		||||
            serde_json::from_value(result).map_err(CircleWsClientError::JsonError)
 | 
			
		||||
        } else if let Some(error) = response.error {
 | 
			
		||||
            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: error.code,
 | 
			
		||||
                message: error.message,
 | 
			
		||||
                data: error.data,
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(CircleWsClientError::ReceiveError(
 | 
			
		||||
                "No result or error in response".to_string(),
 | 
			
		||||
            ))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the output of a job
 | 
			
		||||
    pub async fn get_job_output(&self, job_id: String) -> Result<String, CircleWsClientError> {
 | 
			
		||||
        let params = job_id;
 | 
			
		||||
        
 | 
			
		||||
        let req = self.create_request("get_job_output", params)?;
 | 
			
		||||
        let response = self.send_request(req).await?;
 | 
			
		||||
        
 | 
			
		||||
        if let Some(result) = response.result {
 | 
			
		||||
            serde_json::from_value(result).map_err(CircleWsClientError::JsonError)
 | 
			
		||||
        } else if let Some(error) = response.error {
 | 
			
		||||
            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: error.code,
 | 
			
		||||
                message: error.message,
 | 
			
		||||
                data: error.data,
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(CircleWsClientError::ReceiveError(
 | 
			
		||||
                "No result or error in response".to_string(),
 | 
			
		||||
            ))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the logs of a job
 | 
			
		||||
    pub async fn get_job_logs(&self, job_id: String) -> Result<JobLogsResult, CircleWsClientError> {
 | 
			
		||||
        let params = job_id;
 | 
			
		||||
        
 | 
			
		||||
        let req = self.create_request("get_job_logs", params)?;
 | 
			
		||||
        let response = self.send_request(req).await?;
 | 
			
		||||
        
 | 
			
		||||
        if let Some(result) = response.result {
 | 
			
		||||
            serde_json::from_value(result).map_err(CircleWsClientError::JsonError)
 | 
			
		||||
        } else if let Some(error) = response.error {
 | 
			
		||||
            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: error.code,
 | 
			
		||||
                message: error.message,
 | 
			
		||||
                data: error.data,
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(CircleWsClientError::ReceiveError(
 | 
			
		||||
                "No result or error in response".to_string(),
 | 
			
		||||
            ))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// List all job IDs in the system
 | 
			
		||||
    pub async fn list_jobs(&self) -> Result<Vec<Job>, CircleWsClientError> {
 | 
			
		||||
        let req = self.create_request("list_jobs", serde_json::Value::Null)?;
 | 
			
		||||
        let response = self.send_request(req).await?;
 | 
			
		||||
        
 | 
			
		||||
        if let Some(result) = response.result {
 | 
			
		||||
            serde_json::from_value(result).map_err(CircleWsClientError::JsonError)
 | 
			
		||||
        } else if let Some(error) = response.error {
 | 
			
		||||
            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: error.code,
 | 
			
		||||
                message: error.message,
 | 
			
		||||
                data: error.data,
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(CircleWsClientError::ReceiveError(
 | 
			
		||||
                "No result or error in response".to_string(),
 | 
			
		||||
            ))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Stop a running job
 | 
			
		||||
    pub async fn stop_job(&self, job_id: String) -> Result<(), CircleWsClientError> {
 | 
			
		||||
        let req = self.create_request("stop_job", job_id)?;
 | 
			
		||||
        let response = self.send_request(req).await?;
 | 
			
		||||
        
 | 
			
		||||
        if let Some(result) = response.result {
 | 
			
		||||
            serde_json::from_value(result).map_err(CircleWsClientError::JsonError)
 | 
			
		||||
        } else if let Some(error) = response.error {
 | 
			
		||||
            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: error.code,
 | 
			
		||||
                message: error.message,
 | 
			
		||||
                data: error.data,
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(CircleWsClientError::ReceiveError(
 | 
			
		||||
                "No result or error in response".to_string(),
 | 
			
		||||
            ))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delete a job from the system
 | 
			
		||||
    pub async fn delete_job(&self, job_id: String) -> Result<(), CircleWsClientError> {        
 | 
			
		||||
        let req = self.create_request("delete_job", job_id)?;
 | 
			
		||||
        let response = self.send_request(req).await?;
 | 
			
		||||
        
 | 
			
		||||
        if let Some(error) = response.error {
 | 
			
		||||
            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: error.code,
 | 
			
		||||
                message: error.message,
 | 
			
		||||
                data: error.data,
 | 
			
		||||
            })
 | 
			
		||||
        } else if response.result.is_some() {
 | 
			
		||||
            // Success - return void
 | 
			
		||||
            Ok(())
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(CircleWsClientError::ReceiveError(
 | 
			
		||||
                "No result or error in response".to_string(),
 | 
			
		||||
            ))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Clear all jobs from the system
 | 
			
		||||
    pub async fn clear_all_jobs(&self) -> Result<(), CircleWsClientError> {
 | 
			
		||||
        let req = self.create_request("clear_all_jobs", serde_json::Value::Null)?;
 | 
			
		||||
        let response = self.send_request(req).await?;
 | 
			
		||||
        
 | 
			
		||||
        if let Some(error) = response.error {
 | 
			
		||||
            Err(CircleWsClientError::JsonRpcError {
 | 
			
		||||
                code: error.code,
 | 
			
		||||
                message: error.message,
 | 
			
		||||
                data: error.data,
 | 
			
		||||
            })
 | 
			
		||||
        } else if response.result.is_some() {
 | 
			
		||||
            // Success - return void
 | 
			
		||||
            Ok(())
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(CircleWsClientError::ReceiveError(
 | 
			
		||||
                "No result or error in response".to_string(),
 | 
			
		||||
            ))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user