use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use serde_json::{json, Value as JsonValue};
use tokio::sync::{mpsc, oneshot};
use crate::value::{VmError, VmValue};
pub type OutboundSender = mpsc::UnboundedSender<JsonValue>;
#[derive(Clone)]
pub struct ClientRequestBus {
outbound: OutboundSender,
pending: Arc<Mutex<HashMap<String, oneshot::Sender<JsonValue>>>>,
next_id: Arc<AtomicU64>,
}
impl ClientRequestBus {
pub fn new(outbound: OutboundSender) -> Self {
Self {
outbound,
pending: Arc::new(Mutex::new(HashMap::new())),
next_id: Arc::new(AtomicU64::new(1)),
}
}
pub fn route_response(&self, msg: &JsonValue) -> bool {
if msg.get("method").is_some() {
return false;
}
if msg.get("result").is_none() && msg.get("error").is_none() {
return false;
}
let Some(id) = msg.get("id") else {
return false;
};
let id_key = canonical_id(id);
let mut pending = self
.pending
.lock()
.expect("client-request pending poisoned");
if let Some(tx) = pending.remove(&id_key) {
let _ = tx.send(msg.clone());
true
} else {
false
}
}
pub async fn request(
&self,
id_prefix: &str,
method: &str,
params: JsonValue,
error_prefix: &str,
) -> Result<JsonValue, VmError> {
let id_seq = self.next_id.fetch_add(1, Ordering::Relaxed);
let id = format!("harn-{id_prefix}-{id_seq}");
let (tx, rx) = oneshot::channel();
self.pending
.lock()
.expect("client-request pending poisoned")
.insert(id.clone(), tx);
let request = json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params,
});
if self.outbound.send(request).is_err() {
self.pending
.lock()
.expect("client-request pending poisoned")
.remove(&id);
return Err(VmError::Runtime(format!(
"{error_prefix}: transport closed before request could be sent"
)));
}
let response = match rx.await {
Ok(value) => value,
Err(_) => {
return Err(VmError::Runtime(format!(
"{error_prefix}: transport dropped before client responded"
)));
}
};
if let Some(error) = response.get("error") {
let message = error
.get("message")
.and_then(|value| value.as_str())
.unwrap_or("unknown error");
let code = error
.get("code")
.and_then(|value| value.as_i64())
.unwrap_or(-1);
return Err(VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
format!("{error_prefix}: client error ({code}): {message}"),
))));
}
Ok(response.get("result").cloned().unwrap_or(JsonValue::Null))
}
}
fn canonical_id(value: &JsonValue) -> String {
if let Some(s) = value.as_str() {
return s.to_string();
}
if let Some(n) = value.as_i64() {
return n.to_string();
}
if let Some(n) = value.as_u64() {
return n.to_string();
}
value.to_string()
}
thread_local! {
static CURRENT_BUS: RefCell<Option<ClientRequestBus>> = const { RefCell::new(None) };
}
pub fn install_bus(bus: Option<ClientRequestBus>) -> Option<ClientRequestBus> {
CURRENT_BUS.with(|cell| std::mem::replace(&mut *cell.borrow_mut(), bus))
}
pub fn current_bus() -> Option<ClientRequestBus> {
CURRENT_BUS.with(|cell| cell.borrow().clone())
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn canonical_id_handles_strings_numbers_and_other() {
assert_eq!(canonical_id(&json!("a")), "a");
assert_eq!(canonical_id(&json!(42)), "42");
assert_eq!(canonical_id(&json!(true)), "true");
}
}