initial commit
This commit is contained in:
2
clients/openrpc/.gitignore
vendored
Normal file
2
clients/openrpc/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
pkg
|
||||
target
|
59
clients/openrpc/Cargo-wasm.toml
Normal file
59
clients/openrpc/Cargo-wasm.toml
Normal file
@@ -0,0 +1,59 @@
|
||||
[package]
|
||||
name = "hero-supervisor-openrpc-client-wasm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "WASM-compatible OpenRPC client for Hero Supervisor"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
# WASM bindings
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
|
||||
# Web APIs
|
||||
web-sys = { version = "0.3", features = [
|
||||
"console",
|
||||
"Request",
|
||||
"RequestInit",
|
||||
"RequestMode",
|
||||
"Response",
|
||||
"Window",
|
||||
"Headers",
|
||||
"AbortController",
|
||||
"AbortSignal",
|
||||
] }
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde-wasm-bindgen = "0.6"
|
||||
|
||||
# Error handling
|
||||
thiserror = "1.0"
|
||||
|
||||
# UUID for job IDs
|
||||
uuid = { version = "1.0", features = ["v4", "serde", "js"] }
|
||||
|
||||
# Time handling
|
||||
chrono = { version = "0.4", features = ["serde", "wasmbind"] }
|
||||
|
||||
# Collections
|
||||
indexmap = "2.0"
|
||||
|
||||
# Logging for WASM
|
||||
log = "0.4"
|
||||
console_log = "1.0"
|
||||
|
||||
# Async utilities
|
||||
futures = "0.3"
|
||||
|
||||
[dependencies.getrandom]
|
||||
version = "0.2"
|
||||
features = ["js"]
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
2710
clients/openrpc/Cargo.lock
generated
Normal file
2710
clients/openrpc/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
82
clients/openrpc/Cargo.toml
Normal file
82
clients/openrpc/Cargo.toml
Normal file
@@ -0,0 +1,82 @@
|
||||
[package]
|
||||
name = "hero-supervisor-openrpc-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "OpenRPC client for Hero Supervisor"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
# Common dependencies for both native and WASM
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
log = "0.4"
|
||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||
|
||||
# Native JSON-RPC client (not WASM compatible)
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
jsonrpsee = { version = "0.24", features = ["http-client", "macros"] }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
hero-supervisor = { path = "../.." }
|
||||
env_logger = "0.11"
|
||||
|
||||
# WASM-specific dependencies
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
web-sys = { version = "0.3", features = [
|
||||
"console",
|
||||
"Request",
|
||||
"RequestInit",
|
||||
"RequestMode",
|
||||
"Response",
|
||||
"Headers",
|
||||
"Window",
|
||||
] }
|
||||
console_log = "1.0"
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
||||
# UUID for job IDs (native)
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.uuid]
|
||||
version = "1.0"
|
||||
features = ["v4", "serde"]
|
||||
|
||||
# Time handling (native)
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.chrono]
|
||||
version = "0.4"
|
||||
features = ["serde"]
|
||||
|
||||
# WASM-compatible dependencies (already defined above)
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies.chrono]
|
||||
version = "0.4"
|
||||
features = ["serde", "wasmbind"]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies.uuid]
|
||||
version = "1.0"
|
||||
features = ["v4", "serde", "js"]
|
||||
|
||||
# Collections
|
||||
indexmap = "2.0"
|
||||
|
||||
# Interactive CLI
|
||||
crossterm = "0.27"
|
||||
ratatui = "0.28"
|
||||
|
||||
# Command line parsing
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
|
||||
[[bin]]
|
||||
name = "openrpc-cli"
|
||||
path = "cmd/main.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
# Testing utilities
|
||||
tokio-test = "0.4"
|
196
clients/openrpc/README.md
Normal file
196
clients/openrpc/README.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Hero Supervisor OpenRPC Client
|
||||
|
||||
A Rust client library for interacting with the Hero Supervisor OpenRPC server. This crate provides a simple, async interface for managing actors and jobs remotely.
|
||||
|
||||
## Features
|
||||
|
||||
- **Async API**: Built on `tokio` and `jsonrpsee` for high-performance async operations
|
||||
- **Type Safety**: Full Rust type safety with serde serialization/deserialization
|
||||
- **Job Builder**: Fluent API for creating jobs with validation
|
||||
- **Comprehensive Coverage**: All supervisor operations available via client
|
||||
- **Error Handling**: Detailed error types with proper error propagation
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
hero-supervisor-openrpc-client = "0.1.0"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```rust
|
||||
use hero_supervisor_openrpc_client::{
|
||||
SupervisorClient, RunnerConfig, RunnerType, ProcessManagerType, JobBuilder, JobType
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a client
|
||||
let client = SupervisorClient::new("http://127.0.0.1:3030")?;
|
||||
|
||||
// Add a runner
|
||||
let config = RunnerConfig {
|
||||
actor_id: "my_actor".to_string(),
|
||||
runner_type: RunnerType::OSISRunner,
|
||||
binary_path: PathBuf::from("/path/to/actor/binary"),
|
||||
db_path: "/path/to/db".to_string(),
|
||||
redis_url: "redis://localhost:6379".to_string(),
|
||||
};
|
||||
|
||||
client.add_runner(config, ProcessManagerType::Simple).await?;
|
||||
|
||||
// Start the runner
|
||||
client.start_runner("my_actor").await?;
|
||||
|
||||
// Create and queue a job
|
||||
let job = JobBuilder::new()
|
||||
.caller_id("my_client")
|
||||
.context_id("example_context")
|
||||
.payload("print('Hello from Hero Supervisor!');")
|
||||
.job_type(JobType::OSIS)
|
||||
.runner_name("my_actor")
|
||||
.timeout(Duration::from_secs(60))
|
||||
.build()?;
|
||||
|
||||
client.queue_job_to_runner("my_actor", job).await?;
|
||||
|
||||
// Check runner status
|
||||
let status = client.get_runner_status("my_actor").await?;
|
||||
println!("Runner status: {:?}", status);
|
||||
|
||||
// List all runners
|
||||
let runners = client.list_runners().await?;
|
||||
println!("Active runners: {:?}", runners);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Client Creation
|
||||
|
||||
```rust
|
||||
let client = SupervisorClient::new("http://127.0.0.1:3030")?;
|
||||
```
|
||||
|
||||
### Runner Management
|
||||
|
||||
```rust
|
||||
// Add a runner
|
||||
client.add_runner(config, ProcessManagerType::Simple).await?;
|
||||
|
||||
// Remove a runner
|
||||
client.remove_runner("actor_id").await?;
|
||||
|
||||
// List all runners
|
||||
let runners = client.list_runners().await?;
|
||||
|
||||
// Start/stop runners
|
||||
client.start_runner("actor_id").await?;
|
||||
client.stop_runner("actor_id", false).await?; // force = false
|
||||
|
||||
// Get runner status
|
||||
let status = client.get_runner_status("actor_id").await?;
|
||||
|
||||
// Get runner logs
|
||||
let logs = client.get_runner_logs("actor_id", Some(100), false).await?;
|
||||
```
|
||||
|
||||
### Job Management
|
||||
|
||||
```rust
|
||||
// Create a job using the builder
|
||||
let job = JobBuilder::new()
|
||||
.caller_id("client_id")
|
||||
.context_id("context_id")
|
||||
.payload("script_content")
|
||||
.job_type(JobType::OSIS)
|
||||
.runner_name("target_actor")
|
||||
.timeout(Duration::from_secs(300))
|
||||
.env_var("KEY", "value")
|
||||
.build()?;
|
||||
|
||||
// Queue the job
|
||||
client.queue_job_to_runner("actor_id", job).await?;
|
||||
```
|
||||
|
||||
### Bulk Operations
|
||||
|
||||
```rust
|
||||
// Start all runners
|
||||
let results = client.start_all().await?;
|
||||
|
||||
// Stop all runners
|
||||
let results = client.stop_all(false).await?; // force = false
|
||||
|
||||
// Get status of all runners
|
||||
let statuses = client.get_all_runner_status().await?;
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
### RunnerType
|
||||
|
||||
- `SALRunner` - System abstraction layer operations
|
||||
- `OSISRunner` - Operating system interface operations
|
||||
- `VRunner` - Virtualization operations
|
||||
- `PyRunner` - Python-based actors
|
||||
|
||||
### JobType
|
||||
|
||||
- `SAL` - SAL job type
|
||||
- `OSIS` - OSIS job type
|
||||
- `V` - V job type
|
||||
- `Python` - Python job type
|
||||
|
||||
### ProcessManagerType
|
||||
|
||||
- `Simple` - Direct process spawning
|
||||
- `Tmux(String)` - Tmux session-based management
|
||||
|
||||
### ProcessStatus
|
||||
|
||||
- `Running` - Process is active
|
||||
- `Stopped` - Process is stopped
|
||||
- `Failed` - Process failed
|
||||
- `Unknown` - Status unknown
|
||||
|
||||
## Error Handling
|
||||
|
||||
The client uses the `ClientError` enum for error handling:
|
||||
|
||||
```rust
|
||||
use hero_supervisor_openrpc_client::ClientError;
|
||||
|
||||
match client.start_runner("actor_id").await {
|
||||
Ok(()) => println!("Runner started successfully"),
|
||||
Err(ClientError::JsonRpc(e)) => println!("JSON-RPC error: {}", e),
|
||||
Err(ClientError::Server { message }) => println!("Server error: {}", message),
|
||||
Err(e) => println!("Other error: {}", e),
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See the `examples/` directory for complete usage examples:
|
||||
|
||||
- `basic_client.rs` - Basic client usage
|
||||
- `job_management.rs` - Job creation and management
|
||||
- `runner_lifecycle.rs` - Complete runner lifecycle management
|
||||
|
||||
## Requirements
|
||||
|
||||
- Rust 1.70+
|
||||
- Hero Supervisor server running with OpenRPC feature enabled
|
||||
- Network access to the supervisor server
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
|
29
clients/openrpc/build-wasm.sh
Executable file
29
clients/openrpc/build-wasm.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build script for WASM-compatible OpenRPC client
|
||||
|
||||
set -e
|
||||
|
||||
echo "Building WASM OpenRPC client..."
|
||||
|
||||
# Check if wasm-pack is installed
|
||||
if ! command -v wasm-pack &> /dev/null; then
|
||||
echo "wasm-pack is not installed. Installing..."
|
||||
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
fi
|
||||
|
||||
# Build the WASM package
|
||||
echo "Building WASM package..."
|
||||
wasm-pack build --target web --out-dir pkg-wasm
|
||||
|
||||
echo "WASM build complete! Package available in pkg-wasm/"
|
||||
echo ""
|
||||
echo "To use in a web project:"
|
||||
echo "1. Copy the pkg-wasm directory to your web project"
|
||||
echo "2. Import the module in your JavaScript:"
|
||||
echo " import init, { WasmSupervisorClient, create_client, create_job } from './pkg-wasm/hero_supervisor_openrpc_client_wasm.js';"
|
||||
echo "3. Initialize the WASM module:"
|
||||
echo " await init();"
|
||||
echo "4. Create and use the client:"
|
||||
echo " const client = create_client('http://localhost:3030');"
|
||||
echo " const runners = await client.list_runners();"
|
872
clients/openrpc/cmd/main.rs
Normal file
872
clients/openrpc/cmd/main.rs
Normal file
@@ -0,0 +1,872 @@
|
||||
//! Interactive CLI for Hero Supervisor OpenRPC Client
|
||||
//!
|
||||
//! This CLI provides an interactive interface to explore and test OpenRPC methods
|
||||
//! with arrow key navigation, parameter input, and response display.
|
||||
|
||||
use clap::Parser;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::io;
|
||||
use chrono;
|
||||
|
||||
use hero_supervisor_openrpc_client::{SupervisorClient, RunnerConfig, RunnerType, ProcessManagerType};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "openrpc-cli")]
|
||||
#[command(about = "Interactive CLI for Hero Supervisor OpenRPC")]
|
||||
struct Cli {
|
||||
/// OpenRPC server URL
|
||||
#[arg(short, long, default_value = "http://127.0.0.1:3030")]
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct RpcMethod {
|
||||
name: String,
|
||||
description: String,
|
||||
params: Vec<RpcParam>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct RpcParam {
|
||||
name: String,
|
||||
param_type: String,
|
||||
required: bool,
|
||||
description: String,
|
||||
}
|
||||
|
||||
struct App {
|
||||
client: SupervisorClient,
|
||||
methods: Vec<RpcMethod>,
|
||||
list_state: ListState,
|
||||
current_screen: Screen,
|
||||
selected_method: Option<RpcMethod>,
|
||||
param_inputs: Vec<String>,
|
||||
current_param_index: usize,
|
||||
response: Option<String>,
|
||||
error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Screen {
|
||||
MethodList,
|
||||
ParamInput,
|
||||
Response,
|
||||
}
|
||||
|
||||
impl App {
|
||||
async fn new(url: String) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let client = SupervisorClient::new(&url)?;
|
||||
|
||||
// Test connection to OpenRPC server using the standard rpc.discover method
|
||||
// This is the proper OpenRPC way to test server connectivity and discover available methods
|
||||
let discovery_result = client.discover().await;
|
||||
match discovery_result {
|
||||
Ok(discovery_info) => {
|
||||
println!("✓ Connected to OpenRPC server at {}", url);
|
||||
if let Some(info) = discovery_info.get("info") {
|
||||
if let Some(title) = info.get("title").and_then(|t| t.as_str()) {
|
||||
println!(" Server: {}", title);
|
||||
}
|
||||
if let Some(version) = info.get("version").and_then(|v| v.as_str()) {
|
||||
println!(" Version: {}", version);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!("Failed to connect to OpenRPC server at {}: {}\nMake sure the supervisor is running with OpenRPC enabled.", url, e).into());
|
||||
}
|
||||
}
|
||||
|
||||
let methods = vec![
|
||||
RpcMethod {
|
||||
name: "list_runners".to_string(),
|
||||
description: "List all registered runners".to_string(),
|
||||
params: vec![],
|
||||
},
|
||||
RpcMethod {
|
||||
name: "register_runner".to_string(),
|
||||
description: "Register a new runner to the supervisor with secret authentication".to_string(),
|
||||
params: vec![
|
||||
RpcParam {
|
||||
name: "secret".to_string(),
|
||||
param_type: "String".to_string(),
|
||||
required: true,
|
||||
description: "Secret required for runner registration".to_string(),
|
||||
},
|
||||
RpcParam {
|
||||
name: "name".to_string(),
|
||||
param_type: "String".to_string(),
|
||||
required: true,
|
||||
description: "Name of the runner".to_string(),
|
||||
},
|
||||
RpcParam {
|
||||
name: "queue".to_string(),
|
||||
param_type: "String".to_string(),
|
||||
required: true,
|
||||
description: "Queue name for the runner to listen to".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
RpcMethod {
|
||||
name: "run_job".to_string(),
|
||||
description: "Run a job on the appropriate runner".to_string(),
|
||||
params: vec![
|
||||
RpcParam {
|
||||
name: "secret".to_string(),
|
||||
param_type: "String".to_string(),
|
||||
required: true,
|
||||
description: "Secret required for job execution".to_string(),
|
||||
},
|
||||
RpcParam {
|
||||
name: "job_id".to_string(),
|
||||
param_type: "String".to_string(),
|
||||
required: true,
|
||||
description: "Job ID".to_string(),
|
||||
},
|
||||
RpcParam {
|
||||
name: "runner_name".to_string(),
|
||||
param_type: "String".to_string(),
|
||||
required: true,
|
||||
description: "Name of the runner to execute the job".to_string(),
|
||||
},
|
||||
RpcParam {
|
||||
name: "payload".to_string(),
|
||||
param_type: "String".to_string(),
|
||||
required: true,
|
||||
description: "Job payload/script content".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
RpcMethod {
|
||||
name: "remove_runner".to_string(),
|
||||
description: "Remove a runner from the supervisor".to_string(),
|
||||
params: vec![
|
||||
RpcParam {
|
||||
name: "actor_id".to_string(),
|
||||
param_type: "String".to_string(),
|
||||
required: true,
|
||||
description: "ID of the runner to remove".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
RpcMethod {
|
||||
name: "start_runner".to_string(),
|
||||
description: "Start a specific runner".to_string(),
|
||||
params: vec![
|
||||
RpcParam {
|
||||
name: "actor_id".to_string(),
|
||||
param_type: "String".to_string(),
|
||||
required: true,
|
||||
description: "ID of the runner to start".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
RpcMethod {
|
||||
name: "stop_runner".to_string(),
|
||||
description: "Stop a specific runner".to_string(),
|
||||
params: vec![
|
||||
RpcParam {
|
||||
name: "actor_id".to_string(),
|
||||
param_type: "String".to_string(),
|
||||
required: true,
|
||||
description: "ID of the runner to stop".to_string(),
|
||||
},
|
||||
RpcParam {
|
||||
name: "force".to_string(),
|
||||
param_type: "bool".to_string(),
|
||||
required: true,
|
||||
description: "Whether to force stop the runner".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
RpcMethod {
|
||||
name: "get_runner_status".to_string(),
|
||||
description: "Get the status of a specific runner".to_string(),
|
||||
params: vec![
|
||||
RpcParam {
|
||||
name: "actor_id".to_string(),
|
||||
param_type: "String".to_string(),
|
||||
required: true,
|
||||
description: "ID of the runner".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
RpcMethod {
|
||||
name: "get_all_runner_status".to_string(),
|
||||
description: "Get status of all runners".to_string(),
|
||||
params: vec![],
|
||||
},
|
||||
RpcMethod {
|
||||
name: "start_all".to_string(),
|
||||
description: "Start all runners".to_string(),
|
||||
params: vec![],
|
||||
},
|
||||
RpcMethod {
|
||||
name: "stop_all".to_string(),
|
||||
description: "Stop all runners".to_string(),
|
||||
params: vec![
|
||||
RpcParam {
|
||||
name: "force".to_string(),
|
||||
param_type: "bool".to_string(),
|
||||
required: true,
|
||||
description: "Whether to force stop all runners".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
RpcMethod {
|
||||
name: "get_all_status".to_string(),
|
||||
description: "Get status of all components".to_string(),
|
||||
params: vec![],
|
||||
},
|
||||
];
|
||||
|
||||
let mut list_state = ListState::default();
|
||||
list_state.select(Some(0));
|
||||
|
||||
Ok(App {
|
||||
client,
|
||||
methods,
|
||||
list_state,
|
||||
current_screen: Screen::MethodList,
|
||||
selected_method: None,
|
||||
param_inputs: vec![],
|
||||
current_param_index: 0,
|
||||
response: None,
|
||||
error_message: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn next_method(&mut self) {
|
||||
let i = match self.list_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.methods.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.list_state.select(Some(i));
|
||||
}
|
||||
|
||||
fn previous_method(&mut self) {
|
||||
let i = match self.list_state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.methods.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.list_state.select(Some(i));
|
||||
}
|
||||
|
||||
fn select_method(&mut self) {
|
||||
if let Some(i) = self.list_state.selected() {
|
||||
let method = self.methods[i].clone();
|
||||
if method.params.is_empty() {
|
||||
// No parameters needed, call directly
|
||||
self.selected_method = Some(method);
|
||||
self.current_screen = Screen::Response;
|
||||
} else {
|
||||
// Parameters needed, go to input screen
|
||||
self.selected_method = Some(method.clone());
|
||||
self.param_inputs = vec!["".to_string(); method.params.len()];
|
||||
self.current_param_index = 0;
|
||||
self.current_screen = Screen::ParamInput;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_param(&mut self) {
|
||||
if let Some(method) = &self.selected_method {
|
||||
if self.current_param_index < method.params.len() - 1 {
|
||||
self.current_param_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn previous_param(&mut self) {
|
||||
if self.current_param_index > 0 {
|
||||
self.current_param_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn add_char_to_current_param(&mut self, c: char) {
|
||||
if self.current_param_index < self.param_inputs.len() {
|
||||
self.param_inputs[self.current_param_index].push(c);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_char_from_current_param(&mut self) {
|
||||
if self.current_param_index < self.param_inputs.len() {
|
||||
self.param_inputs[self.current_param_index].pop();
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_method(&mut self) {
|
||||
if let Some(method) = &self.selected_method {
|
||||
self.error_message = None;
|
||||
self.response = None;
|
||||
|
||||
// Build parameters
|
||||
let mut params = json!({});
|
||||
|
||||
if !method.params.is_empty() {
|
||||
for (i, param) in method.params.iter().enumerate() {
|
||||
let input = &self.param_inputs[i];
|
||||
if input.is_empty() && param.required {
|
||||
self.error_message = Some(format!("Required parameter '{}' is empty", param.name));
|
||||
return;
|
||||
}
|
||||
|
||||
if !input.is_empty() {
|
||||
let value = match param.param_type.as_str() {
|
||||
"bool" => {
|
||||
match input.to_lowercase().as_str() {
|
||||
"true" | "1" | "yes" => json!(true),
|
||||
"false" | "0" | "no" => json!(false),
|
||||
_ => {
|
||||
self.error_message = Some(format!("Invalid boolean value for '{}': {}", param.name, input));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
"i32" | "i64" | "u32" | "u64" => {
|
||||
match input.parse::<i64>() {
|
||||
Ok(n) => json!(n),
|
||||
Err(_) => {
|
||||
self.error_message = Some(format!("Invalid number for '{}': {}", param.name, input));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => json!(input),
|
||||
};
|
||||
|
||||
if method.name == "register_runner" {
|
||||
// Special handling for register_runner method
|
||||
match param.name.as_str() {
|
||||
"secret" => params["secret"] = value,
|
||||
"name" => params["name"] = value,
|
||||
"queue" => params["queue"] = value,
|
||||
_ => {}
|
||||
}
|
||||
} else if method.name == "run_job" {
|
||||
// Special handling for run_job method
|
||||
match param.name.as_str() {
|
||||
"secret" => params["secret"] = value,
|
||||
"job_id" => params["job_id"] = value,
|
||||
"runner_name" => params["runner_name"] = value,
|
||||
"payload" => params["payload"] = value,
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
params[¶m.name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the method
|
||||
let result: Result<serde_json::Value, hero_supervisor_openrpc_client::ClientError> = match method.name.as_str() {
|
||||
"list_runners" => {
|
||||
match self.client.list_runners().await {
|
||||
Ok(response) => {
|
||||
match serde_json::to_value(response) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
"get_all_runner_status" => {
|
||||
match self.client.get_all_runner_status().await {
|
||||
Ok(response) => {
|
||||
match serde_json::to_value(response) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
"start_all" => {
|
||||
match self.client.start_all().await {
|
||||
Ok(response) => {
|
||||
match serde_json::to_value(response) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
"get_all_status" => {
|
||||
match self.client.get_all_status().await {
|
||||
Ok(response) => {
|
||||
match serde_json::to_value(response) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
"stop_all" => {
|
||||
let force = params.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
match self.client.stop_all(force).await {
|
||||
Ok(response) => {
|
||||
match serde_json::to_value(response) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
"start_runner" => {
|
||||
if let Some(actor_id) = params.get("actor_id").and_then(|v| v.as_str()) {
|
||||
match self.client.start_runner(actor_id).await {
|
||||
Ok(response) => {
|
||||
match serde_json::to_value(response) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(hero_supervisor_openrpc_client::ClientError::from(
|
||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing actor_id parameter"))
|
||||
))
|
||||
}
|
||||
}
|
||||
"stop_runner" => {
|
||||
if let (Some(actor_id), Some(force)) = (
|
||||
params.get("actor_id").and_then(|v| v.as_str()),
|
||||
params.get("force").and_then(|v| v.as_bool())
|
||||
) {
|
||||
match self.client.stop_runner(actor_id, force).await {
|
||||
Ok(response) => {
|
||||
match serde_json::to_value(response) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(hero_supervisor_openrpc_client::ClientError::from(
|
||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing parameters"))
|
||||
))
|
||||
}
|
||||
}
|
||||
"remove_runner" => {
|
||||
if let Some(actor_id) = params.get("actor_id").and_then(|v| v.as_str()) {
|
||||
match self.client.remove_runner(actor_id).await {
|
||||
Ok(response) => {
|
||||
match serde_json::to_value(response) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(hero_supervisor_openrpc_client::ClientError::from(
|
||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing actor_id parameter"))
|
||||
))
|
||||
}
|
||||
}
|
||||
"get_runner_status" => {
|
||||
if let Some(actor_id) = params.get("actor_id").and_then(|v| v.as_str()) {
|
||||
match self.client.get_runner_status(actor_id).await {
|
||||
Ok(response) => {
|
||||
match serde_json::to_value(response) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(hero_supervisor_openrpc_client::ClientError::from(
|
||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing actor_id parameter"))
|
||||
))
|
||||
}
|
||||
}
|
||||
"register_runner" => {
|
||||
if let (Some(secret), Some(name), Some(queue)) = (
|
||||
params.get("secret").and_then(|v| v.as_str()),
|
||||
params.get("name").and_then(|v| v.as_str()),
|
||||
params.get("queue").and_then(|v| v.as_str())
|
||||
) {
|
||||
match self.client.register_runner(secret, name, queue).await {
|
||||
Ok(response) => {
|
||||
match serde_json::to_value(response) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(hero_supervisor_openrpc_client::ClientError::from(
|
||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing required parameters: secret, name, queue"))
|
||||
))
|
||||
}
|
||||
}
|
||||
"run_job" => {
|
||||
if let (Some(secret), Some(job_id), Some(runner_name), Some(payload)) = (
|
||||
params.get("secret").and_then(|v| v.as_str()),
|
||||
params.get("job_id").and_then(|v| v.as_str()),
|
||||
params.get("runner_name").and_then(|v| v.as_str()),
|
||||
params.get("payload").and_then(|v| v.as_str())
|
||||
) {
|
||||
// Create a job object
|
||||
let job = serde_json::json!({
|
||||
"id": job_id,
|
||||
"caller_id": "cli_user",
|
||||
"context_id": "cli_context",
|
||||
"payload": payload,
|
||||
"job_type": "SAL",
|
||||
"runner_name": runner_name,
|
||||
"timeout": 30000000000u64, // 30 seconds in nanoseconds
|
||||
"env_vars": {},
|
||||
"created_at": chrono::Utc::now().to_rfc3339(),
|
||||
"updated_at": chrono::Utc::now().to_rfc3339()
|
||||
});
|
||||
|
||||
match self.client.run_job(secret, job).await {
|
||||
Ok(response) => {
|
||||
match serde_json::to_value(response) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(hero_supervisor_openrpc_client::ClientError::from(
|
||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing required parameters: secret, job_id, runner_name, payload"))
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => Err(hero_supervisor_openrpc_client::ClientError::from(
|
||||
serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Method not implemented in CLI"))
|
||||
)),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
self.response = Some(format!("{:#}", response));
|
||||
}
|
||||
Err(e) => {
|
||||
self.error_message = Some(format!("Error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
self.current_screen = Screen::Response;
|
||||
}
|
||||
}
|
||||
|
||||
fn back_to_methods(&mut self) {
|
||||
self.current_screen = Screen::MethodList;
|
||||
self.selected_method = None;
|
||||
self.param_inputs.clear();
|
||||
self.current_param_index = 0;
|
||||
self.response = None;
|
||||
self.error_message = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &mut App) {
|
||||
match app.current_screen {
|
||||
Screen::MethodList => draw_method_list(f, app),
|
||||
Screen::ParamInput => draw_param_input(f, app),
|
||||
Screen::Response => draw_response(f, app),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_method_list(f: &mut Frame, app: &mut App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints([Constraint::Min(0)].as_ref())
|
||||
.split(f.area());
|
||||
|
||||
let items: Vec<ListItem> = app
|
||||
.methods
|
||||
.iter()
|
||||
.map(|method| {
|
||||
let content = vec![Line::from(vec![
|
||||
Span::styled(&method.name, Style::default().fg(Color::Yellow)),
|
||||
Span::raw(" - "),
|
||||
Span::raw(&method.description),
|
||||
])];
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let items = List::new(items)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("OpenRPC Methods (↑↓ to navigate, Enter to select, q to quit)"),
|
||||
)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::LightGreen)
|
||||
.fg(Color::Black)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
f.render_stateful_widget(items, chunks[0], &mut app.list_state);
|
||||
}
|
||||
|
||||
fn draw_param_input(f: &mut Frame, app: &mut App) {
|
||||
if let Some(method) = &app.selected_method {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(3),
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
// Title
|
||||
let title = Paragraph::new(format!("Parameters for: {}", method.name))
|
||||
.block(Block::default().borders(Borders::ALL).title("Method"));
|
||||
f.render_widget(title, chunks[0]);
|
||||
|
||||
// Parameters - create proper form layout with separate label and input areas
|
||||
let param_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(5); method.params.len()])
|
||||
.split(chunks[1]);
|
||||
|
||||
for (i, param) in method.params.iter().enumerate() {
|
||||
let is_current = i == app.current_param_index;
|
||||
|
||||
// Split each parameter into label and input areas
|
||||
let param_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(2), Constraint::Length(3)])
|
||||
.split(param_chunks[i]);
|
||||
|
||||
// Parameter label and description
|
||||
let label_style = if is_current {
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
};
|
||||
|
||||
let label_text = vec![
|
||||
Line::from(vec![
|
||||
Span::styled(¶m.name, label_style),
|
||||
Span::raw(if param.required { " (required)" } else { " (optional)" }),
|
||||
Span::raw(format!(" [{}]", param.param_type)),
|
||||
]),
|
||||
Line::from(Span::styled(¶m.description, Style::default().fg(Color::Gray))),
|
||||
];
|
||||
|
||||
let label_widget = Paragraph::new(label_text)
|
||||
.block(Block::default().borders(Borders::NONE));
|
||||
f.render_widget(label_widget, param_layout[0]);
|
||||
|
||||
// Input field
|
||||
let empty_string = String::new();
|
||||
let input_value = app.param_inputs.get(i).unwrap_or(&empty_string);
|
||||
|
||||
let input_display = if is_current {
|
||||
if input_value.is_empty() {
|
||||
"█".to_string() // Show cursor when active and empty
|
||||
} else {
|
||||
format!("{}█", input_value) // Show cursor at end when active
|
||||
}
|
||||
} else {
|
||||
if input_value.is_empty() {
|
||||
" ".to_string() // Empty space for inactive empty fields
|
||||
} else {
|
||||
input_value.clone()
|
||||
}
|
||||
};
|
||||
|
||||
let input_style = if is_current {
|
||||
Style::default().fg(Color::Black).bg(Color::Cyan)
|
||||
} else {
|
||||
Style::default().fg(Color::White).bg(Color::DarkGray)
|
||||
};
|
||||
|
||||
let border_style = if is_current {
|
||||
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::Gray)
|
||||
};
|
||||
|
||||
let input_widget = Paragraph::new(Line::from(Span::styled(input_display, input_style)))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(border_style)
|
||||
.title(if is_current { " INPUT " } else { "" }),
|
||||
);
|
||||
|
||||
f.render_widget(input_widget, param_layout[1]);
|
||||
}
|
||||
|
||||
// Instructions
|
||||
let instructions = Paragraph::new("↑↓ to navigate params, type to edit, Enter to execute, Esc to go back")
|
||||
.block(Block::default().borders(Borders::ALL).title("Instructions"));
|
||||
f.render_widget(instructions, chunks[2]);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_response(f: &mut Frame, app: &mut App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(3),
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
// Title
|
||||
let method_name = app.selected_method.as_ref().map(|m| m.name.as_str()).unwrap_or("Unknown");
|
||||
let title = Paragraph::new(format!("Response for: {}", method_name))
|
||||
.block(Block::default().borders(Borders::ALL).title("Response"));
|
||||
f.render_widget(title, chunks[0]);
|
||||
|
||||
// Response content
|
||||
let content = if let Some(error) = &app.error_message {
|
||||
Text::from(error.clone()).style(Style::default().fg(Color::Red))
|
||||
} else if let Some(response) = &app.response {
|
||||
Text::from(response.clone()).style(Style::default().fg(Color::Green))
|
||||
} else {
|
||||
Text::from("Executing...").style(Style::default().fg(Color::Yellow))
|
||||
};
|
||||
|
||||
let response_widget = Paragraph::new(content)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(response_widget, chunks[1]);
|
||||
|
||||
// Instructions
|
||||
let instructions = Paragraph::new("Esc to go back to methods")
|
||||
.block(Block::default().borders(Borders::ALL).title("Instructions"));
|
||||
f.render_widget(instructions, chunks[2]);
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Create app
|
||||
let mut app = match App::new(cli.url).await {
|
||||
Ok(app) => app,
|
||||
Err(e) => {
|
||||
// Cleanup terminal before showing error
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
eprintln!("Failed to connect to OpenRPC server: {}", e);
|
||||
eprintln!("Make sure the supervisor is running with OpenRPC enabled.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Main loop
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match app.current_screen {
|
||||
Screen::MethodList => {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => break,
|
||||
KeyCode::Down => app.next_method(),
|
||||
KeyCode::Up => app.previous_method(),
|
||||
KeyCode::Enter => {
|
||||
app.select_method();
|
||||
// If the selected method has no parameters, execute it immediately
|
||||
if let Some(method) = &app.selected_method {
|
||||
if method.params.is_empty() {
|
||||
app.execute_method().await;
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Screen::ParamInput => {
|
||||
match key.code {
|
||||
KeyCode::Esc => app.back_to_methods(),
|
||||
KeyCode::Up => app.previous_param(),
|
||||
KeyCode::Down => app.next_param(),
|
||||
KeyCode::Enter => {
|
||||
app.execute_method().await;
|
||||
}
|
||||
KeyCode::Backspace => app.remove_char_from_current_param(),
|
||||
KeyCode::Char(c) => app.add_char_to_current_param(c),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Screen::Response => {
|
||||
match key.code {
|
||||
KeyCode::Esc => app.back_to_methods(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
Ok(())
|
||||
}
|
202
clients/openrpc/example-wasm.html
Normal file
202
clients/openrpc/example-wasm.html
Normal file
@@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hero Supervisor WASM OpenRPC Client Example</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
button {
|
||||
background: #007cba;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background: #005a87;
|
||||
}
|
||||
button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin: 5px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.output {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.error {
|
||||
color: #d32f2f;
|
||||
}
|
||||
.success {
|
||||
color: #2e7d32;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hero Supervisor WASM OpenRPC Client</h1>
|
||||
|
||||
<div class="container">
|
||||
<h2>Connection</h2>
|
||||
<input type="text" id="serverUrl" placeholder="Server URL" value="http://localhost:3030">
|
||||
<button onclick="testConnection()">Test Connection</button>
|
||||
<div id="connectionOutput" class="output"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>Runner Management</h2>
|
||||
<button onclick="listRunners()">List Runners</button>
|
||||
<div id="runnersOutput" class="output"></div>
|
||||
|
||||
<h3>Register Runner</h3>
|
||||
<input type="text" id="registerSecret" placeholder="Secret" value="admin123">
|
||||
<input type="text" id="runnerName" placeholder="Runner Name" value="wasm_runner">
|
||||
<input type="text" id="runnerQueue" placeholder="Queue Name" value="wasm_queue">
|
||||
<button onclick="registerRunner()">Register Runner</button>
|
||||
<div id="registerOutput" class="output"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>Job Execution</h2>
|
||||
<input type="text" id="jobSecret" placeholder="Secret" value="admin123">
|
||||
<input type="text" id="jobId" placeholder="Job ID" value="">
|
||||
<input type="text" id="jobRunnerName" placeholder="Runner Name" value="wasm_runner">
|
||||
<textarea id="jobPayload" placeholder="Job Payload" rows="3">echo "Hello from WASM client!"</textarea>
|
||||
<button onclick="runJob()">Run Job</button>
|
||||
<div id="jobOutput" class="output"></div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import init, {
|
||||
WasmSupervisorClient,
|
||||
WasmJob,
|
||||
create_client,
|
||||
create_job
|
||||
} from './pkg-wasm/hero_supervisor_openrpc_client_wasm.js';
|
||||
|
||||
let client = null;
|
||||
|
||||
// Initialize WASM module
|
||||
async function initWasm() {
|
||||
try {
|
||||
await init();
|
||||
console.log('WASM module initialized');
|
||||
document.getElementById('connectionOutput').textContent = 'WASM module loaded successfully';
|
||||
document.getElementById('connectionOutput').className = 'output success';
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize WASM:', error);
|
||||
document.getElementById('connectionOutput').textContent = `Failed to initialize WASM: ${error}`;
|
||||
document.getElementById('connectionOutput').className = 'output error';
|
||||
}
|
||||
}
|
||||
|
||||
// Test connection to supervisor
|
||||
window.testConnection = async function() {
|
||||
try {
|
||||
const serverUrl = document.getElementById('serverUrl').value;
|
||||
client = create_client(serverUrl);
|
||||
|
||||
const result = await client.discover();
|
||||
document.getElementById('connectionOutput').textContent = `Connection successful!\n${JSON.stringify(result, null, 2)}`;
|
||||
document.getElementById('connectionOutput').className = 'output success';
|
||||
} catch (error) {
|
||||
document.getElementById('connectionOutput').textContent = `Connection failed: ${error}`;
|
||||
document.getElementById('connectionOutput').className = 'output error';
|
||||
}
|
||||
};
|
||||
|
||||
// List all runners
|
||||
window.listRunners = async function() {
|
||||
try {
|
||||
if (!client) {
|
||||
throw new Error('Client not initialized. Test connection first.');
|
||||
}
|
||||
|
||||
const runners = await client.list_runners();
|
||||
document.getElementById('runnersOutput').textContent = `Runners:\n${JSON.stringify(runners, null, 2)}`;
|
||||
document.getElementById('runnersOutput').className = 'output success';
|
||||
} catch (error) {
|
||||
document.getElementById('runnersOutput').textContent = `Failed to list runners: ${error}`;
|
||||
document.getElementById('runnersOutput').className = 'output error';
|
||||
}
|
||||
};
|
||||
|
||||
// Register a new runner
|
||||
window.registerRunner = async function() {
|
||||
try {
|
||||
if (!client) {
|
||||
throw new Error('Client not initialized. Test connection first.');
|
||||
}
|
||||
|
||||
const secret = document.getElementById('registerSecret').value;
|
||||
const name = document.getElementById('runnerName').value;
|
||||
const queue = document.getElementById('runnerQueue').value;
|
||||
|
||||
await client.register_runner(secret, name, queue);
|
||||
document.getElementById('registerOutput').textContent = `Runner '${name}' registered successfully!`;
|
||||
document.getElementById('registerOutput').className = 'output success';
|
||||
} catch (error) {
|
||||
document.getElementById('registerOutput').textContent = `Failed to register runner: ${error}`;
|
||||
document.getElementById('registerOutput').className = 'output error';
|
||||
}
|
||||
};
|
||||
|
||||
// Run a job
|
||||
window.runJob = async function() {
|
||||
try {
|
||||
if (!client) {
|
||||
throw new Error('Client not initialized. Test connection first.');
|
||||
}
|
||||
|
||||
const secret = document.getElementById('jobSecret').value;
|
||||
let jobId = document.getElementById('jobId').value;
|
||||
const runnerName = document.getElementById('jobRunnerName').value;
|
||||
const payload = document.getElementById('jobPayload').value;
|
||||
|
||||
// Generate job ID if not provided
|
||||
if (!jobId) {
|
||||
jobId = 'job_' + Math.random().toString(36).substr(2, 9);
|
||||
document.getElementById('jobId').value = jobId;
|
||||
}
|
||||
|
||||
const job = create_job(jobId, payload, "SAL", runnerName);
|
||||
const result = await client.run_job(secret, job);
|
||||
|
||||
document.getElementById('jobOutput').textContent = `Job executed successfully!\nJob ID: ${jobId}\nResult: ${result}`;
|
||||
document.getElementById('jobOutput').className = 'output success';
|
||||
} catch (error) {
|
||||
document.getElementById('jobOutput').textContent = `Failed to run job: ${error}`;
|
||||
document.getElementById('jobOutput').className = 'output error';
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on page load
|
||||
initWasm();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
1037
clients/openrpc/src/lib.rs
Normal file
1037
clients/openrpc/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
668
clients/openrpc/src/wasm.rs
Normal file
668
clients/openrpc/src/wasm.rs
Normal file
@@ -0,0 +1,668 @@
|
||||
//! WASM-compatible OpenRPC client for Hero Supervisor
|
||||
//!
|
||||
//! This module provides a WASM-compatible client library for interacting with the Hero Supervisor
|
||||
//! OpenRPC server using browser-native fetch APIs.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{Request, RequestInit, RequestMode, Response, Headers};
|
||||
use serde::{Deserialize, Serialize};
|
||||
// use std::collections::HashMap; // Unused
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
// use js_sys::Promise; // Unused
|
||||
|
||||
/// WASM-compatible client for communicating with Hero Supervisor OpenRPC server
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmSupervisorClient {
|
||||
server_url: String,
|
||||
}
|
||||
|
||||
/// Error types for WASM client operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum WasmClientError {
|
||||
#[error("Network error: {0}")]
|
||||
Network(String),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
#[error("JavaScript error: {0}")]
|
||||
JavaScript(String),
|
||||
|
||||
#[error("Server error: {message}")]
|
||||
Server { message: String },
|
||||
|
||||
#[error("Invalid response format")]
|
||||
InvalidResponse,
|
||||
}
|
||||
|
||||
/// Result type for WASM client operations
|
||||
pub type WasmClientResult<T> = Result<T, WasmClientError>;
|
||||
|
||||
/// JSON-RPC request structure
|
||||
#[derive(Serialize)]
|
||||
struct JsonRpcRequest {
|
||||
jsonrpc: String,
|
||||
method: String,
|
||||
params: serde_json::Value,
|
||||
id: u32,
|
||||
}
|
||||
|
||||
/// JSON-RPC response structure
|
||||
#[derive(Deserialize)]
|
||||
struct JsonRpcResponse {
|
||||
jsonrpc: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
result: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<JsonRpcError>,
|
||||
id: u32,
|
||||
}
|
||||
|
||||
/// JSON-RPC error structure
|
||||
#[derive(Deserialize)]
|
||||
struct JsonRpcError {
|
||||
code: i32,
|
||||
message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Types of runners supported by the supervisor
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[wasm_bindgen]
|
||||
pub enum WasmRunnerType {
|
||||
SALRunner,
|
||||
OSISRunner,
|
||||
VRunner,
|
||||
}
|
||||
|
||||
/// Job type enumeration that maps to runner types
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[wasm_bindgen]
|
||||
pub enum WasmJobType {
|
||||
SAL,
|
||||
OSIS,
|
||||
V,
|
||||
}
|
||||
|
||||
/// Job structure for creating and managing jobs
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmJob {
|
||||
id: String,
|
||||
caller_id: String,
|
||||
context_id: String,
|
||||
payload: String,
|
||||
runner_name: String,
|
||||
executor: String,
|
||||
timeout_secs: u64,
|
||||
env_vars: String, // JSON string of HashMap<String, String>
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmSupervisorClient {
|
||||
/// Create a new WASM supervisor client
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(server_url: String) -> Self {
|
||||
console_log::init_with_level(log::Level::Info).ok();
|
||||
Self { server_url }
|
||||
}
|
||||
|
||||
/// Get the server URL
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn server_url(&self) -> String {
|
||||
self.server_url.clone()
|
||||
}
|
||||
|
||||
/// Test connection using OpenRPC discovery method
|
||||
pub async fn discover(&self) -> Result<JsValue, JsValue> {
|
||||
let result = self.call_method("rpc.discover", serde_json::Value::Null).await;
|
||||
match result {
|
||||
Ok(value) => Ok(wasm_bindgen::JsValue::from_str(&value.to_string())),
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a new runner to the supervisor with secret authentication
|
||||
pub async fn register_runner(&self, secret: &str, name: &str, queue: &str) -> Result<String, JsValue> {
|
||||
let params = serde_json::json!([{
|
||||
"secret": secret,
|
||||
"name": name,
|
||||
"queue": queue
|
||||
}]);
|
||||
|
||||
match self.call_method("register_runner", params).await {
|
||||
Ok(result) => {
|
||||
// Extract the runner name from the result
|
||||
if let Some(runner_name) = result.as_str() {
|
||||
Ok(runner_name.to_string())
|
||||
} else {
|
||||
Err(JsValue::from_str("Invalid response format: expected runner name"))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a job (fire-and-forget, non-blocking)
|
||||
#[wasm_bindgen]
|
||||
pub async fn create_job(&self, secret: String, job: WasmJob) -> Result<String, JsValue> {
|
||||
// Backend expects RunJobParams struct with secret and job fields - wrap in array like register_runner
|
||||
let params = serde_json::json!([{
|
||||
"secret": secret,
|
||||
"job": {
|
||||
"id": job.id,
|
||||
"caller_id": job.caller_id,
|
||||
"context_id": job.context_id,
|
||||
"payload": job.payload,
|
||||
"runner_name": job.runner_name,
|
||||
"executor": job.executor,
|
||||
"timeout": {
|
||||
"secs": job.timeout_secs,
|
||||
"nanos": 0
|
||||
},
|
||||
"env_vars": serde_json::from_str::<serde_json::Value>(&job.env_vars).unwrap_or(serde_json::json!({})),
|
||||
"created_at": job.created_at,
|
||||
"updated_at": job.updated_at
|
||||
}
|
||||
}]);
|
||||
|
||||
match self.call_method("create_job", params).await {
|
||||
Ok(result) => {
|
||||
if let Some(job_id) = result.as_str() {
|
||||
Ok(job_id.to_string())
|
||||
} else {
|
||||
Ok(result.to_string())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to create job: {:?}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a job on a specific runner (blocking, returns result)
|
||||
#[wasm_bindgen]
|
||||
pub async fn run_job(&self, secret: String, job: WasmJob) -> Result<String, JsValue> {
|
||||
// Backend expects RunJobParams struct with secret and job fields - wrap in array like register_runner
|
||||
let params = serde_json::json!([{
|
||||
"secret": secret,
|
||||
"job": {
|
||||
"id": job.id,
|
||||
"caller_id": job.caller_id,
|
||||
"context_id": job.context_id,
|
||||
"payload": job.payload,
|
||||
"runner_name": job.runner_name,
|
||||
"executor": job.executor,
|
||||
"timeout": {
|
||||
"secs": job.timeout_secs,
|
||||
"nanos": 0
|
||||
},
|
||||
"env_vars": serde_json::from_str::<serde_json::Value>(&job.env_vars).unwrap_or(serde_json::json!({})),
|
||||
"created_at": job.created_at,
|
||||
"updated_at": job.updated_at
|
||||
}
|
||||
}]);
|
||||
|
||||
match self.call_method("run_job", params).await {
|
||||
Ok(result) => {
|
||||
if let Some(result_str) = result.as_str() {
|
||||
Ok(result_str.to_string())
|
||||
} else {
|
||||
Ok(result.to_string())
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// List all runner IDs
|
||||
pub async fn list_runners(&self) -> Result<Vec<String>, JsValue> {
|
||||
match self.call_method("list_runners", serde_json::Value::Null).await {
|
||||
Ok(result) => {
|
||||
if let Ok(runners) = serde_json::from_value::<Vec<String>>(result) {
|
||||
Ok(runners)
|
||||
} else {
|
||||
Err(JsValue::from_str("Invalid response format for list_runners"))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// List all job IDs from Redis
|
||||
pub async fn list_jobs(&self) -> Result<Vec<String>, JsValue> {
|
||||
match self.call_method("list_jobs", serde_json::Value::Null).await {
|
||||
Ok(result) => {
|
||||
if let Ok(jobs) = serde_json::from_value::<Vec<String>>(result) {
|
||||
Ok(jobs)
|
||||
} else {
|
||||
Err(JsValue::from_str("Invalid response format for list_jobs"))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a job by job ID
|
||||
pub async fn get_job(&self, job_id: &str) -> Result<WasmJob, JsValue> {
|
||||
let params = serde_json::json!([job_id]);
|
||||
match self.call_method("get_job", params).await {
|
||||
Ok(result) => {
|
||||
// Convert the Job result to WasmJob
|
||||
if let Ok(job_value) = serde_json::from_value::<serde_json::Value>(result) {
|
||||
// Extract fields from the job
|
||||
let id = job_value.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let caller_id = job_value.get("caller_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let context_id = job_value.get("context_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let payload = job_value.get("payload").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let runner_name = job_value.get("runner_name").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let executor = job_value.get("executor").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let timeout_secs = job_value.get("timeout").and_then(|v| v.get("secs")).and_then(|v| v.as_u64()).unwrap_or(30);
|
||||
let env_vars = job_value.get("env_vars").map(|v| v.to_string()).unwrap_or_else(|| "{}".to_string());
|
||||
let created_at = job_value.get("created_at").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let updated_at = job_value.get("updated_at").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
|
||||
Ok(WasmJob {
|
||||
id,
|
||||
caller_id,
|
||||
context_id,
|
||||
payload,
|
||||
runner_name,
|
||||
executor,
|
||||
timeout_secs,
|
||||
env_vars,
|
||||
created_at,
|
||||
updated_at,
|
||||
})
|
||||
} else {
|
||||
Err(JsValue::from_str("Invalid response format for get_job"))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ping a runner by dispatching a ping job to its queue
|
||||
#[wasm_bindgen]
|
||||
pub async fn ping_runner(&self, runner_id: &str) -> Result<String, JsValue> {
|
||||
let params = serde_json::json!([runner_id]);
|
||||
|
||||
match self.call_method("ping_runner", params).await {
|
||||
Ok(result) => {
|
||||
if let Some(job_id) = result.as_str() {
|
||||
Ok(job_id.to_string())
|
||||
} else {
|
||||
Ok(result.to_string())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to ping runner: {:?}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop a job by ID
|
||||
#[wasm_bindgen]
|
||||
pub async fn stop_job(&self, job_id: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([job_id]);
|
||||
|
||||
match self.call_method("stop_job", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to stop job: {:?}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a job by ID
|
||||
#[wasm_bindgen]
|
||||
pub async fn delete_job(&self, job_id: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([job_id]);
|
||||
|
||||
match self.call_method("delete_job", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to delete job: {:?}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a runner from the supervisor
|
||||
pub async fn remove_runner(&self, actor_id: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([actor_id]);
|
||||
match self.call_method("remove_runner", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a specific runner
|
||||
pub async fn start_runner(&self, actor_id: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([actor_id]);
|
||||
match self.call_method("start_runner", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop a specific runner
|
||||
pub async fn stop_runner(&self, actor_id: &str, force: bool) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([actor_id, force]);
|
||||
self.call_method("stop_runner", params)
|
||||
.await
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a specific runner by ID
|
||||
pub async fn get_runner(&self, actor_id: &str) -> Result<JsValue, JsValue> {
|
||||
let params = serde_json::json!([actor_id]);
|
||||
let result = self.call_method("get_runner", params)
|
||||
.await
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
// Convert the serde_json::Value to a JsValue via string serialization
|
||||
let json_string = serde_json::to_string(&result)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
Ok(js_sys::JSON::parse(&json_string)
|
||||
.map_err(|e| JsValue::from_str("Failed to parse JSON"))?)
|
||||
}
|
||||
|
||||
/// Add a secret to the supervisor
|
||||
pub async fn add_secret(&self, admin_secret: &str, secret_type: &str, secret_value: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([{
|
||||
"admin_secret": admin_secret,
|
||||
"secret_type": secret_type,
|
||||
"secret_value": secret_value
|
||||
}]);
|
||||
match self.call_method("add_secret", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a secret from the supervisor
|
||||
pub async fn remove_secret(&self, admin_secret: &str, secret_type: &str, secret_value: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([{
|
||||
"admin_secret": admin_secret,
|
||||
"secret_type": secret_type,
|
||||
"secret_value": secret_value
|
||||
}]);
|
||||
match self.call_method("remove_secret", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// List secrets (returns supervisor info including secret counts)
|
||||
pub async fn list_secrets(&self, admin_secret: &str) -> Result<JsValue, JsValue> {
|
||||
let params = serde_json::json!([{
|
||||
"admin_secret": admin_secret
|
||||
}]);
|
||||
match self.call_method("list_secrets", params).await {
|
||||
Ok(result) => {
|
||||
// Convert serde_json::Value to JsValue
|
||||
let result_str = serde_json::to_string(&result)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
Ok(js_sys::JSON::parse(&result_str)
|
||||
.map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?)
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get supervisor information including secret counts
|
||||
pub async fn get_supervisor_info(&self, admin_secret: &str) -> Result<JsValue, JsValue> {
|
||||
let params = serde_json::json!({
|
||||
"admin_secret": admin_secret
|
||||
});
|
||||
|
||||
match self.call_method("get_supervisor_info", params).await {
|
||||
Ok(result) => {
|
||||
let result_str = serde_json::to_string(&result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Serialization error: {:?}", e)))?;
|
||||
Ok(js_sys::JSON::parse(&result_str)
|
||||
.map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?)
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to get supervisor info: {:?}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// List admin secrets (returns actual secret values)
|
||||
pub async fn list_admin_secrets(&self, admin_secret: &str) -> Result<Vec<String>, JsValue> {
|
||||
let params = serde_json::json!({
|
||||
"admin_secret": admin_secret
|
||||
});
|
||||
|
||||
match self.call_method("list_admin_secrets", params).await {
|
||||
Ok(result) => {
|
||||
let secrets: Vec<String> = serde_json::from_value(result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to parse admin secrets: {:?}", e)))?;
|
||||
Ok(secrets)
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to list admin secrets: {:?}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// List user secrets (returns actual secret values)
|
||||
pub async fn list_user_secrets(&self, admin_secret: &str) -> Result<Vec<String>, JsValue> {
|
||||
let params = serde_json::json!({
|
||||
"admin_secret": admin_secret
|
||||
});
|
||||
|
||||
match self.call_method("list_user_secrets", params).await {
|
||||
Ok(result) => {
|
||||
let secrets: Vec<String> = serde_json::from_value(result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to parse user secrets: {:?}", e)))?;
|
||||
Ok(secrets)
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to list user secrets: {:?}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// List register secrets (returns actual secret values)
|
||||
pub async fn list_register_secrets(&self, admin_secret: &str) -> Result<Vec<String>, JsValue> {
|
||||
let params = serde_json::json!({
|
||||
"admin_secret": admin_secret
|
||||
});
|
||||
|
||||
match self.call_method("list_register_secrets", params).await {
|
||||
Ok(result) => {
|
||||
let secrets: Vec<String> = serde_json::from_value(result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to parse register secrets: {:?}", e)))?;
|
||||
Ok(secrets)
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to list register secrets: {:?}", e))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmJob {
|
||||
/// Create a new job with default values
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: String, payload: String, executor: String, runner_name: String) -> Self {
|
||||
let now = js_sys::Date::new_0().to_iso_string().as_string().unwrap();
|
||||
Self {
|
||||
id,
|
||||
caller_id: "wasm_client".to_string(),
|
||||
context_id: "wasm_context".to_string(),
|
||||
payload,
|
||||
runner_name,
|
||||
executor,
|
||||
timeout_secs: 30,
|
||||
env_vars: "{}".to_string(),
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the caller ID
|
||||
#[wasm_bindgen(setter)]
|
||||
pub fn set_caller_id(&mut self, caller_id: String) {
|
||||
self.caller_id = caller_id;
|
||||
}
|
||||
|
||||
/// Set the context ID
|
||||
#[wasm_bindgen(setter)]
|
||||
pub fn set_context_id(&mut self, context_id: String) {
|
||||
self.context_id = context_id;
|
||||
}
|
||||
|
||||
/// Set the timeout in seconds
|
||||
#[wasm_bindgen(setter)]
|
||||
pub fn set_timeout_secs(&mut self, timeout_secs: u64) {
|
||||
self.timeout_secs = timeout_secs;
|
||||
}
|
||||
|
||||
/// Set environment variables as JSON string
|
||||
#[wasm_bindgen(setter)]
|
||||
pub fn set_env_vars(&mut self, env_vars: String) {
|
||||
self.env_vars = env_vars;
|
||||
}
|
||||
|
||||
/// Generate a new UUID for the job
|
||||
#[wasm_bindgen]
|
||||
pub fn generate_id(&mut self) {
|
||||
self.id = Uuid::new_v4().to_string();
|
||||
}
|
||||
|
||||
/// Get the job ID
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn id(&self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
/// Get the caller ID
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn caller_id(&self) -> String {
|
||||
self.caller_id.clone()
|
||||
}
|
||||
|
||||
/// Get the context ID
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn context_id(&self) -> String {
|
||||
self.context_id.clone()
|
||||
}
|
||||
|
||||
/// Get the payload
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn payload(&self) -> String {
|
||||
self.payload.clone()
|
||||
}
|
||||
|
||||
/// Get the job type
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn executor(&self) -> String {
|
||||
self.executor.clone()
|
||||
}
|
||||
|
||||
/// Get the runner name
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn runner_name(&self) -> String {
|
||||
self.runner_name.clone()
|
||||
}
|
||||
|
||||
/// Get the timeout in seconds
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn timeout_secs(&self) -> u64 {
|
||||
self.timeout_secs
|
||||
}
|
||||
|
||||
/// Get the environment variables as JSON string
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn env_vars(&self) -> String {
|
||||
self.env_vars.clone()
|
||||
}
|
||||
|
||||
/// Get the created timestamp
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn created_at(&self) -> String {
|
||||
self.created_at.clone()
|
||||
}
|
||||
|
||||
/// Get the updated timestamp
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn updated_at(&self) -> String {
|
||||
self.updated_at.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl WasmSupervisorClient {
|
||||
/// Internal method to make JSON-RPC calls
|
||||
async fn call_method(&self, method: &str, params: serde_json::Value) -> WasmClientResult<serde_json::Value> {
|
||||
let request = JsonRpcRequest {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
method: method.to_string(),
|
||||
params,
|
||||
id: 1,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&request)?;
|
||||
|
||||
// Create headers
|
||||
let headers = Headers::new().map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
|
||||
headers.set("Content-Type", "application/json")
|
||||
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
|
||||
|
||||
// Create request init
|
||||
let opts = RequestInit::new();
|
||||
opts.set_method("POST");
|
||||
opts.set_headers(&headers);
|
||||
opts.set_body(&JsValue::from_str(&body));
|
||||
opts.set_mode(RequestMode::Cors);
|
||||
|
||||
// Create request
|
||||
let request = Request::new_with_str_and_init(&self.server_url, &opts)
|
||||
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
|
||||
|
||||
// Get window and fetch
|
||||
let window = web_sys::window().ok_or_else(|| WasmClientError::JavaScript("No window object".to_string()))?;
|
||||
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await
|
||||
.map_err(|e| WasmClientError::Network(format!("{:?}", e)))?;
|
||||
|
||||
// Convert to Response
|
||||
let resp: Response = resp_value.dyn_into()
|
||||
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
|
||||
|
||||
// Check if response is ok
|
||||
if !resp.ok() {
|
||||
return Err(WasmClientError::Network(format!("HTTP {}: {}", resp.status(), resp.status_text())));
|
||||
}
|
||||
|
||||
// Get response text
|
||||
let text_promise = resp.text()
|
||||
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
|
||||
let text_value = JsFuture::from(text_promise).await
|
||||
.map_err(|e| WasmClientError::Network(format!("{:?}", e)))?;
|
||||
let text = text_value.as_string()
|
||||
.ok_or_else(|| WasmClientError::InvalidResponse)?;
|
||||
|
||||
// Parse JSON-RPC response
|
||||
let response: JsonRpcResponse = serde_json::from_str(&text)?;
|
||||
|
||||
if let Some(error) = response.error {
|
||||
return Err(WasmClientError::Server {
|
||||
message: format!("{}: {}", error.code, error.message),
|
||||
});
|
||||
}
|
||||
|
||||
// For void methods, null result is valid
|
||||
Ok(response.result.unwrap_or(serde_json::Value::Null))
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the WASM client library (call manually if needed)
|
||||
pub fn init() {
|
||||
console_log::init_with_level(log::Level::Info).ok();
|
||||
log::info!("Hero Supervisor WASM OpenRPC Client initialized");
|
||||
}
|
||||
|
||||
/// Utility function to create a job from JavaScript
|
||||
/// Create a new job (convenience function for JavaScript)
|
||||
#[wasm_bindgen]
|
||||
pub fn create_job(id: String, payload: String, executor: String, runner_name: String) -> WasmJob {
|
||||
WasmJob::new(id, payload, executor, runner_name)
|
||||
}
|
||||
|
||||
/// Utility function to create a client from JavaScript
|
||||
#[wasm_bindgen]
|
||||
pub fn create_client(server_url: String) -> WasmSupervisorClient {
|
||||
WasmSupervisorClient::new(server_url)
|
||||
}
|
Reference in New Issue
Block a user