use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::MutexGuard;
use std::sync::PoisonError;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI64;
use std::sync::atomic::Ordering;
use serde_json::Value;
use tokio::sync::oneshot;
use crate::adapters::primary::transport::http::stream::EventTail;
use crate::application::server::context::{CallbackExchange, ExchangeError};
use crate::domain::ir::JsonObject;
type Tail = EventTail<Value>;
#[derive(Debug, Clone, Default)]
pub struct ExchangeTable {
inner: Arc<Mutex<Table>>,
readable: Arc<AtomicBool>,
}
const ID_ORIGIN: i64 = 1 << 40;
#[derive(Debug)]
struct Table {
waiters: HashMap<i64, oneshot::Sender<Answer>>,
next_id: AtomicI64,
}
pub type Answer = Result<JsonObject, ExchangeError>;
impl Default for Table {
fn default() -> Self {
Self {
waiters: HashMap::new(),
next_id: AtomicI64::new(ID_ORIGIN),
}
}
}
impl ExchangeTable {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn readable() -> Self {
Self {
readable: Arc::new(AtomicBool::new(true)),
..Self::default()
}
}
#[must_use]
pub fn is_readable(&self) -> bool {
self.readable.load(Ordering::Relaxed)
}
fn register(&self, sender: oneshot::Sender<Answer>) -> i64 {
let mut table = self.lock();
let id = table.next_id.fetch_add(1, Ordering::Relaxed);
table.waiters.insert(id, sender);
id
}
pub fn resolve(&self, id: i64, answer: Answer) -> bool {
let Some(waiter) = self.lock().waiters.remove(&id) else {
return false;
};
waiter.send(answer).is_ok()
}
#[must_use]
pub fn outstanding(&self) -> usize {
self.lock().waiters.len()
}
fn lock(&self) -> MutexGuard<'_, Table> {
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
}
}
#[derive(Clone)]
pub struct HttpExchange {
table: ExchangeTable,
publish: Publisher,
}
type Publisher = std::sync::Arc<dyn Fn(Value) + Send + Sync>;
impl core::fmt::Debug for HttpExchange {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("HttpExchange")
.field("table", &self.table)
.finish_non_exhaustive()
}
}
impl HttpExchange {
#[must_use]
pub fn new(table: ExchangeTable, tail: Tail) -> Self {
Self::publishing(table, move |request| {
tail.push(request);
})
}
#[must_use]
pub fn publishing(
table: ExchangeTable,
publish: impl Fn(Value) + Send + Sync + 'static,
) -> Self {
Self {
table,
publish: std::sync::Arc::new(publish),
}
}
#[must_use]
pub const fn table(&self) -> &ExchangeTable {
&self.table
}
}
impl CallbackExchange for HttpExchange {
async fn exchange(
&self,
method: &str,
params: &crate::domain::ir::JsonObject,
) -> Result<crate::domain::ir::JsonObject, ExchangeError> {
if !self.table.is_readable() {
return Err(ExchangeError::new(
"this server answers with a single JSON document, so a tool \
cannot ask the peer a question and wait: set \
Streaming::EventStream to offer elicitation or sampling",
));
}
let (sender, receiver) = oneshot::channel();
let id = self.table.register(sender);
let mut request = serde_json::Map::new();
request.insert("jsonrpc".to_owned(), Value::from("2.0"));
request.insert("id".to_owned(), Value::from(id));
request.insert("method".to_owned(), Value::from(method));
request.insert(
"params".to_owned(),
serde_json::from_str::<Value>(params.as_str())
.map_err(|_| ExchangeError::new("the request's params were not JSON"))?,
);
(self.publish)(Value::Object(request));
receiver
.await
.map_err(|_| ExchangeError::new("the session ended before the peer answered"))?
}
}
#[cfg(test)]
mod tests {
use super::{ExchangeTable, HttpExchange};
use crate::adapters::primary::transport::http::stream::EventTail;
use crate::application::server::context::CallbackExchange;
use crate::domain::ir::JsonObject;
type TestError = Box<dyn std::error::Error>;
fn capacity() -> Result<std::num::NonZeroUsize, TestError> {
Ok(4.try_into()?)
}
#[tokio::test]
async fn an_unreadable_exchange_names_the_setting() -> Result<(), TestError> {
let exchange = HttpExchange::new(ExchangeTable::new(), EventTail::new(capacity()?));
let params = JsonObject::new("{}")?;
let Err(error) = exchange.exchange("elicitation/create", ¶ms).await else {
return Err("an exchange with no reader was answered".into());
};
let message = error.to_string();
if !message.contains("EventStream") {
return Err(format!("the refusal does not name the setting: {message}").into());
}
Ok(())
}
#[tokio::test]
async fn a_refused_exchange_leaves_nothing_in_flight() -> Result<(), TestError> {
let table = ExchangeTable::new();
let exchange = HttpExchange::new(table.clone(), EventTail::new(capacity()?));
let params = JsonObject::new("{}")?;
let _ = exchange.exchange("elicitation/create", ¶ms).await;
if table.outstanding() != 0 {
return Err("a refused exchange left a waiter behind".into());
}
Ok(())
}
#[tokio::test]
async fn a_readable_exchange_registers_a_waiter() -> Result<(), TestError> {
let table = ExchangeTable::readable();
let exchange = HttpExchange::new(table.clone(), EventTail::new(capacity()?));
let params = JsonObject::new("{}")?;
let pending = exchange.exchange("elicitation/create", ¶ms);
tokio::pin!(pending);
let _ = tokio::time::timeout(std::time::Duration::from_millis(50), &mut pending).await;
if table.outstanding() != 1 {
return Err("a readable exchange registered no waiter".into());
}
let second = exchange.exchange("elicitation/create", ¶ms);
tokio::pin!(second);
let answered =
tokio::time::timeout(std::time::Duration::from_millis(50), &mut second).await;
if answered.is_ok() {
return Err("an exchange answered without the peer".into());
}
if table.outstanding() != 2 {
return Err("the second exchange registered no waiter of its own".into());
}
Ok(())
}
#[test]
fn the_default_table_is_unreadable() {
assert!(!ExchangeTable::new().is_readable());
assert!(ExchangeTable::readable().is_readable());
}
}