Skip to main content

soothe_client/
helpers.rs

1//! Scripting helpers for one-shot RPCs and daemon probes.
2
3use std::time::Duration;
4
5use serde_json::{json, Map, Value};
6
7use crate::client::Client;
8use crate::errors::{Error, Result};
9use crate::session::connect_with_retries;
10
11/// Resolve WebSocket URL from env (`SOOTHE_WS_URL` / `SOOTHE_DAEMON_URL`).
12pub fn websocket_url_from_env() -> String {
13    crate::config::load_config_from_env().daemon_url
14}
15
16/// Check daemon status via an already-connected client.
17pub async fn check_daemon_status(client: &Client) -> Result<Map<String, Value>> {
18    client.fetch_daemon_status().await
19}
20
21/// True when daemon answers with `readiness_state == ready`.
22pub async fn is_daemon_live(
23    ws_url: &str,
24    connect_timeout: Duration,
25    wait_for_ready: bool,
26    ready_timeout: Duration,
27) -> bool {
28    let client = Client::new(ws_url);
29    let connect = async { connect_with_retries(&client, 5, Duration::from_millis(200)).await };
30    if tokio::time::timeout(connect_timeout, connect)
31        .await
32        .ok()
33        .and_then(|r| r.ok())
34        .is_none()
35    {
36        return false;
37    }
38    if !wait_for_ready {
39        let _ = client.close().await;
40        return true;
41    }
42    let deadline = tokio::time::Instant::now() + ready_timeout;
43    let mut ok = false;
44    while tokio::time::Instant::now() < deadline {
45        if let Ok(status) = client.fetch_daemon_status().await {
46            let ready = status.get("readiness_state").and_then(|v| v.as_str()) == Some("ready")
47                || status.get("running").and_then(|v| v.as_bool()) == Some(true);
48            if ready {
49                ok = true;
50                break;
51            }
52        }
53        tokio::time::sleep(Duration::from_millis(100)).await;
54    }
55    let _ = client.close().await;
56    ok
57}
58
59/// One-shot protocol-1 RPC; errors returned as `{"error": "..."}`.
60pub async fn protocol1_rpc(
61    ws_url: &str,
62    method: &str,
63    params: Map<String, Value>,
64    mode: &str,
65    rpc_timeout: Duration,
66) -> Map<String, Value> {
67    let client = Client::new(ws_url);
68    if let Err(e) = connect_with_retries(&client, 5, Duration::from_millis(200)).await {
69        let mut m = Map::new();
70        m.insert("error".into(), json!(e.to_string()));
71        return m;
72    }
73    let out = match mode {
74        "notify" => match client.notify(method, params).await {
75            Ok(()) => Map::new(),
76            Err(e) => {
77                let mut m = Map::new();
78                m.insert("error".into(), json!(e.to_string()));
79                m
80            }
81        },
82        "subscribe" => match client.subscribe(method, params, rpc_timeout).await {
83            Ok(id) => {
84                let mut m = Map::new();
85                m.insert("subscription_id".into(), json!(id));
86                m
87            }
88            Err(e) => {
89                let mut m = Map::new();
90                m.insert("error".into(), json!(e.to_string()));
91                m
92            }
93        },
94        _ => match client.request(method, params, rpc_timeout).await {
95            Ok(r) => r,
96            Err(e) => {
97                let mut m = Map::new();
98                m.insert("error".into(), json!(e.to_string()));
99                m
100            }
101        },
102    };
103    let _ = client.close().await;
104    out
105}
106
107/// Request daemon shutdown.
108pub async fn request_daemon_shutdown(client: &Client) -> Result<()> {
109    let resp = client
110        .request("daemon_shutdown", Map::new(), Duration::from_secs(10))
111        .await?;
112    let status = resp.get("status").and_then(|v| v.as_str()).unwrap_or("");
113    if status != "acknowledged" && !status.is_empty() {
114        return Err(Error::msg(format!("unexpected shutdown status: {status}")));
115    }
116    Ok(())
117}
118
119/// Request config reload.
120pub async fn request_daemon_config_reload(client: &Client) -> Result<Map<String, Value>> {
121    client
122        .request("config_reload", Map::new(), Duration::from_secs(5))
123        .await
124}
125
126/// Fetch skills catalog.
127pub async fn fetch_skills_catalog(client: &Client) -> Result<Map<String, Value>> {
128    client.list_skills().await
129}
130
131/// Fetch a config section.
132pub async fn fetch_config_section(client: &Client, section: &str) -> Result<Map<String, Value>> {
133    let mut params = Map::new();
134    if !section.is_empty() {
135        params.insert("section".into(), json!(section));
136    }
137    client
138        .request("config_get", params, Duration::from_secs(10))
139        .await
140}
141
142/// Fetch loop history.
143pub async fn fetch_loop_history(client: &Client, loop_id: &str) -> Result<Map<String, Value>> {
144    client.loop_history_fetch(loop_id).await
145}
146
147/// Fetch loop cards.
148pub async fn fetch_loop_cards(client: &Client, loop_id: &str) -> Result<Map<String, Value>> {
149    client.loop_cards_fetch(loop_id).await
150}
151
152/// Submit access_key/secret_key credentials via RPC.
153pub async fn request_auth(
154    client: &Client,
155    access_key: &str,
156    secret_key: &str,
157) -> Result<Map<String, Value>> {
158    client.authenticate(access_key, secret_key).await
159}
160
161/// Submit a refresh_token via RPC.
162pub async fn request_auth_refresh(
163    client: &Client,
164    refresh_token: &str,
165) -> Result<Map<String, Value>> {
166    client.refresh_auth_token(refresh_token).await
167}