use std::collections::HashMap;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use std::time::Duration;
use ahp_types::actions::{ActionEnvelope, StateAction};
use ahp_types::commands::{
DispatchActionParams, InitializeParams, InitializeResult, ReconnectParams, ReconnectResult,
SubscribeParams, SubscribeResult, UnsubscribeParams,
};
use ahp_types::common::{Uri, ROOT_RESOURCE_URI};
use ahp_types::messages::{
ActionNotificationParams, JsonRpcError, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest,
JsonRpcVersion,
};
use ahp_types::notifications::{
AuthRequiredParams, SessionAddedParams, SessionRemovedParams, SessionSummaryChangedParams,
};
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value;
use tokio::sync::{broadcast, mpsc, oneshot, Mutex};
use tokio::task::JoinHandle;
use crate::error::ClientError;
use crate::transport::{Transport, TransportMessage};
const DEFAULT_SUBSCRIPTION_BUFFER: usize = 256;
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub default_request_timeout: Option<Duration>,
pub subscription_buffer: usize,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
default_request_timeout: Some(Duration::from_secs(30)),
subscription_buffer: DEFAULT_SUBSCRIPTION_BUFFER,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SubscriptionEvent {
Action(ActionEnvelope),
SessionAdded(SessionAddedParams),
SessionRemoved(SessionRemovedParams),
SessionSummaryChanged(SessionSummaryChangedParams),
AuthRequired(AuthRequiredParams),
}
#[derive(Debug, Clone)]
pub struct ClientEvent {
pub channel: Uri,
pub event: SubscriptionEvent,
}
pub struct ClientEventStream {
rx: broadcast::Receiver<ClientEvent>,
}
impl ClientEventStream {
pub async fn recv(&mut self) -> Option<ClientEvent> {
loop {
match self.rx.recv().await {
Ok(ev) => return Some(ev),
Err(broadcast::error::RecvError::Closed) => return None,
Err(broadcast::error::RecvError::Lagged(_)) => continue,
}
}
}
}
pub struct SessionSubscription {
rx: broadcast::Receiver<SubscriptionEvent>,
uri: String,
}
impl SessionSubscription {
pub fn uri(&self) -> &str {
&self.uri
}
pub async fn recv(&mut self) -> Option<SubscriptionEvent> {
loop {
match self.rx.recv().await {
Ok(ev) => return Some(ev),
Err(broadcast::error::RecvError::Closed) => return None,
Err(broadcast::error::RecvError::Lagged(_)) => continue,
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct DispatchHandle {
pub client_seq: i64,
}
type PendingMap = HashMap<u64, oneshot::Sender<Result<Value, JsonRpcError>>>;
struct Shared {
pending: Mutex<PendingMap>,
subscriptions: Mutex<HashMap<String, broadcast::Sender<SubscriptionEvent>>>,
all_events: std::sync::Mutex<Option<broadcast::Sender<ClientEvent>>>,
outbound: mpsc::Sender<Outbound>,
next_id: AtomicU64,
next_client_seq: AtomicU64,
config: ClientConfig,
}
enum Outbound {
Message(JsonRpcMessage),
Shutdown,
}
#[derive(Clone)]
pub struct Client {
shared: Arc<Shared>,
_reader: Arc<DriveHandle>,
}
struct DriveHandle {
handle: Mutex<Option<JoinHandle<()>>>,
}
impl Drop for DriveHandle {
fn drop(&mut self) {
if let Ok(mut guard) = self.handle.try_lock() {
if let Some(h) = guard.take() {
h.abort();
}
}
}
}
impl Client {
pub async fn connect<T: Transport>(
transport: T,
config: ClientConfig,
) -> Result<Self, ClientError> {
let (outbound_tx, outbound_rx) = mpsc::channel::<Outbound>(64);
let (all_events_tx, _) = broadcast::channel::<ClientEvent>(config.subscription_buffer);
let shared = Arc::new(Shared {
pending: Mutex::new(HashMap::new()),
subscriptions: Mutex::new(HashMap::new()),
all_events: std::sync::Mutex::new(Some(all_events_tx)),
outbound: outbound_tx,
next_id: AtomicU64::new(1),
next_client_seq: AtomicU64::new(1),
config,
});
let handle = tokio::spawn(drive_transport(transport, shared.clone(), outbound_rx));
Ok(Self {
shared,
_reader: Arc::new(DriveHandle {
handle: Mutex::new(Some(handle)),
}),
})
}
pub async fn shutdown(&self) {
let _ = self.shared.outbound.send(Outbound::Shutdown).await;
let mut pending = self.shared.pending.lock().await;
for (_, tx) in pending.drain() {
let _ = tx.send(Err(JsonRpcError {
code: -32000,
message: "client shut down".into(),
data: None,
}));
}
}
pub async fn request<P, R>(&self, method: &str, params: P) -> Result<R, ClientError>
where
P: Serialize,
R: DeserializeOwned,
{
let id = self.shared.next_id.fetch_add(1, Ordering::Relaxed);
let params_val = serde_json::to_value(¶ms)?;
let params_any = if params_val.is_null() {
None
} else {
Some(ahp_types::common::AnyValue::from(params_val))
};
let req = JsonRpcMessage::Request(JsonRpcRequest {
jsonrpc: JsonRpcVersion::V2,
id,
method: method.into(),
params: params_any,
});
let (tx, rx) = oneshot::channel();
{
let mut pending = self.shared.pending.lock().await;
pending.insert(id, tx);
}
if self
.shared
.outbound
.send(Outbound::Message(req))
.await
.is_err()
{
self.shared.pending.lock().await.remove(&id);
return Err(ClientError::Shutdown);
}
let result = match self.shared.config.default_request_timeout {
Some(dur) => match tokio::time::timeout(dur, rx).await {
Ok(r) => r,
Err(_) => {
self.shared.pending.lock().await.remove(&id);
return Err(ClientError::Cancelled);
}
},
None => rx.await,
};
match result {
Ok(Ok(value)) => Ok(serde_json::from_value(value)?),
Ok(Err(e)) => Err(ClientError::Rpc(e)),
Err(_) => Err(ClientError::Shutdown),
}
}
pub async fn notify<P: Serialize>(&self, method: &str, params: P) -> Result<(), ClientError> {
let params_val = serde_json::to_value(¶ms)?;
let params_any = if params_val.is_null() {
None
} else {
Some(ahp_types::common::AnyValue::from(params_val))
};
let msg = JsonRpcMessage::Notification(JsonRpcNotification {
jsonrpc: JsonRpcVersion::V2,
method: method.into(),
params: params_any,
});
self.shared
.outbound
.send(Outbound::Message(msg))
.await
.map_err(|_| ClientError::Shutdown)
}
pub async fn initialize(
&self,
client_id: String,
protocol_versions: Vec<String>,
initial_subscriptions: Vec<String>,
) -> Result<InitializeResult, ClientError> {
let params = InitializeParams {
channel: ROOT_RESOURCE_URI.to_string(),
protocol_versions,
client_id,
initial_subscriptions: if initial_subscriptions.is_empty() {
None
} else {
Some(initial_subscriptions)
},
locale: None,
capabilities: None,
};
self.request("initialize", params).await
}
pub async fn reconnect(
&self,
client_id: String,
last_seen_server_seq: i64,
subscriptions: Vec<String>,
) -> Result<ReconnectResult, ClientError> {
let params = ReconnectParams {
channel: ROOT_RESOURCE_URI.to_string(),
client_id,
last_seen_server_seq,
subscriptions,
};
self.request("reconnect", params).await
}
pub async fn subscribe(
&self,
uri: String,
) -> Result<(SubscribeResult, SessionSubscription), ClientError> {
let sub = self.attach_subscription(&uri).await;
let result: SubscribeResult = self
.request("subscribe", SubscribeParams { channel: uri })
.await?;
Ok((result, sub))
}
pub async fn attach_subscription(&self, uri: &str) -> SessionSubscription {
let mut subs = self.shared.subscriptions.lock().await;
let tx = subs
.entry(uri.to_string())
.or_insert_with(|| broadcast::channel(self.shared.config.subscription_buffer).0);
SessionSubscription {
rx: tx.subscribe(),
uri: uri.to_string(),
}
}
pub async fn unsubscribe(&self, uri: String) -> Result<(), ClientError> {
{
let mut subs = self.shared.subscriptions.lock().await;
subs.remove(&uri);
}
self.notify("unsubscribe", UnsubscribeParams { channel: uri })
.await
}
pub fn events(&self) -> ClientEventStream {
let rx = match self.shared.all_events.lock() {
Ok(guard) => guard.as_ref().map(|s| s.subscribe()),
Err(_) => None,
};
let rx = rx.unwrap_or_else(|| {
let (_, rx) = broadcast::channel(1);
rx
});
ClientEventStream { rx }
}
pub async fn dispatch(
&self,
channel: Uri,
action: StateAction,
) -> Result<DispatchHandle, ClientError> {
let client_seq = self.shared.next_client_seq.fetch_add(1, Ordering::Relaxed) as i64;
self.notify(
"dispatchAction",
DispatchActionParams {
channel,
client_seq,
action,
},
)
.await?;
Ok(DispatchHandle { client_seq })
}
}
async fn drive_transport<T: Transport>(
mut transport: T,
shared: Arc<Shared>,
mut outbound: mpsc::Receiver<Outbound>,
) {
loop {
tokio::select! {
outbound_msg = outbound.recv() => {
match outbound_msg {
Some(Outbound::Message(msg)) => {
if let Ok(wire) = TransportMessage::encode(&msg) {
if let Err(err) = transport.send(wire).await {
tracing::warn!(?err, "transport send failed");
break;
}
}
}
Some(Outbound::Shutdown) | None => {
let _ = transport.close().await;
break;
}
}
}
inbound = transport.recv() => {
match inbound {
Ok(Some(wire)) => {
match wire.into_parsed() {
Ok(msg) => dispatch_inbound(&shared, msg).await,
Err(err) => tracing::warn!(?err, "malformed frame"),
}
}
Ok(None) => break,
Err(err) => {
tracing::warn!(?err, "transport recv error");
break;
}
}
}
}
}
let mut pending = shared.pending.lock().await;
for (_, tx) in pending.drain() {
let _ = tx.send(Err(JsonRpcError {
code: -32000,
message: "transport closed".into(),
data: None,
}));
}
let mut subs = shared.subscriptions.lock().await;
subs.clear();
if let Ok(mut guard) = shared.all_events.lock() {
guard.take();
}
}
async fn dispatch_inbound(shared: &Shared, msg: JsonRpcMessage) {
match msg {
JsonRpcMessage::SuccessResponse(r) => {
if let Some(tx) = shared.pending.lock().await.remove(&r.id) {
let _ = tx.send(Ok(r.result));
}
}
JsonRpcMessage::ErrorResponse(r) => {
if let Some(tx) = shared.pending.lock().await.remove(&r.id) {
let _ = tx.send(Err(r.error));
}
}
JsonRpcMessage::Notification(n) => {
handle_notification(shared, n).await;
}
JsonRpcMessage::Request(r) => {
tracing::debug!(method = %r.method, "ignoring unexpected server request");
}
}
}
async fn handle_notification(shared: &Shared, n: JsonRpcNotification) {
let params_val: Value = n.params.unwrap_or(Value::Null);
match n.method.as_str() {
"action" => {
if let Ok(envelope) = serde_json::from_value::<ActionNotificationParams>(params_val) {
let channel = envelope.channel.clone();
fan_out(shared, &channel, SubscriptionEvent::Action(envelope)).await;
}
}
"root/sessionAdded" => {
if let Ok(params) = serde_json::from_value::<SessionAddedParams>(params_val) {
let channel = params.channel.clone();
fan_out(shared, &channel, SubscriptionEvent::SessionAdded(params)).await;
}
}
"root/sessionRemoved" => {
if let Ok(params) = serde_json::from_value::<SessionRemovedParams>(params_val) {
let channel = params.channel.clone();
fan_out(shared, &channel, SubscriptionEvent::SessionRemoved(params)).await;
}
}
"root/sessionSummaryChanged" => {
if let Ok(params) = serde_json::from_value::<SessionSummaryChangedParams>(params_val) {
let channel = params.channel.clone();
fan_out(
shared,
&channel,
SubscriptionEvent::SessionSummaryChanged(params),
)
.await;
}
}
"auth/required" => {
if let Ok(params) = serde_json::from_value::<AuthRequiredParams>(params_val) {
let channel = params.channel.clone();
fan_out(shared, &channel, SubscriptionEvent::AuthRequired(params)).await;
}
}
other => {
tracing::debug!(method = %other, "unhandled notification");
}
}
}
async fn fan_out(shared: &Shared, channel: &Uri, event: SubscriptionEvent) {
{
let subs = shared.subscriptions.lock().await;
if let Some(tx) = subs.get(channel) {
let _ = tx.send(event.clone());
}
}
if let Ok(guard) = shared.all_events.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.send(ClientEvent {
channel: channel.clone(),
event,
});
}
}
}