use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use std::time::Duration;
use ahp_types::actions::{ActionEnvelope, StateAction};
use ahp_types::commands::{
CreateResourceWatchParams, CreateResourceWatchResult, DispatchActionParams, InitializeParams,
InitializeResult, ReconnectParams, ReconnectResult, ResourceCopyParams, ResourceCopyResult,
ResourceDeleteParams, ResourceDeleteResult, ResourceListParams, ResourceListResult,
ResourceMkdirParams, ResourceMkdirResult, ResourceMoveParams, ResourceMoveResult,
ResourceReadParams, ResourceReadResult, ResourceRequestParams, ResourceRequestResult,
ResourceResolveParams, ResourceResolveResult, ResourceWriteParams, ResourceWriteResult,
SubscribeParams, SubscribeResult, SubscribeView, SubscriptionDeliveryOptions,
UnsubscribeParams,
};
use ahp_types::common::{Uri, ROOT_RESOURCE_URI};
use ahp_types::errors::json_rpc_error_codes;
use ahp_types::messages::{
ActionNotificationParams, JsonRpcError, JsonRpcErrorResponse, JsonRpcMessage,
JsonRpcNotification, JsonRpcRequest, JsonRpcSuccessResponse, 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,
server_request_handler: std::sync::Mutex<Option<ServerRequestHandler>>,
}
enum Outbound {
Message(JsonRpcMessage),
Shutdown,
}
pub type ServerRequestFuture = Pin<Box<dyn Future<Output = Result<Value, JsonRpcError>> + Send>>;
pub type ServerRequestHandler = Arc<dyn Fn(String, Value) -> ServerRequestFuture + Send + Sync>;
type ResourceHandler<P, R> =
Arc<dyn Fn(P) -> Pin<Box<dyn Future<Output = Result<R, JsonRpcError>> + Send>> + Send + Sync>;
fn method_not_found(method: &str) -> JsonRpcError {
JsonRpcError {
code: json_rpc_error_codes::METHOD_NOT_FOUND,
message: format!("no handler for server method \"{method}\""),
data: None,
}
}
fn run_resource_handler<P, R>(
handler: Option<ResourceHandler<P, R>>,
method: String,
params: Value,
) -> ServerRequestFuture
where
P: DeserializeOwned + Send + 'static,
R: Serialize + Send + 'static,
{
Box::pin(async move {
let Some(handler) = handler else {
return Err(method_not_found(&method));
};
let parsed: P = serde_json::from_value(params).map_err(|e| JsonRpcError {
code: json_rpc_error_codes::INVALID_PARAMS,
message: e.to_string(),
data: None,
})?;
let result = handler(parsed).await?;
serde_json::to_value(result).map_err(|e| JsonRpcError {
code: json_rpc_error_codes::INTERNAL_ERROR,
message: e.to_string(),
data: None,
})
})
}
macro_rules! resource_request_handlers {
($( $(#[$doc:meta])* $field:ident : $setter:ident => $method:literal ($params:ty) -> $result:ty ),+ $(,)?) => {
#[derive(Clone, Default)]
pub struct ResourceRequestHandlers {
$( $(#[$doc])* pub $field: Option<ResourceHandler<$params, $result>>, )+
}
impl ResourceRequestHandlers {
pub fn new() -> Self {
Self::default()
}
$(
$(#[$doc])*
pub fn $setter<F, Fut>(mut self, handler: F) -> Self
where
F: Fn($params) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<$result, JsonRpcError>> + Send + 'static,
{
self.$field = Some(Arc::new(move |p| Box::pin(handler(p))));
self
}
)+
pub fn into_handler(self) -> ServerRequestHandler {
Arc::new(move |method: String, params: Value| match method.as_str() {
$( $method => run_resource_handler(self.$field.clone(), method.clone(), params), )+
_ => {
let unknown = method.clone();
Box::pin(async move { Err(method_not_found(&unknown)) })
}
})
}
}
};
}
resource_request_handlers! {
resource_read: on_resource_read => "resourceRead" (ResourceReadParams) -> ResourceReadResult,
resource_write: on_resource_write => "resourceWrite" (ResourceWriteParams) -> ResourceWriteResult,
resource_list: on_resource_list => "resourceList" (ResourceListParams) -> ResourceListResult,
resource_copy: on_resource_copy => "resourceCopy" (ResourceCopyParams) -> ResourceCopyResult,
resource_delete: on_resource_delete => "resourceDelete" (ResourceDeleteParams) -> ResourceDeleteResult,
resource_move: on_resource_move => "resourceMove" (ResourceMoveParams) -> ResourceMoveResult,
resource_resolve: on_resource_resolve => "resourceResolve" (ResourceResolveParams) -> ResourceResolveResult,
resource_mkdir: on_resource_mkdir => "resourceMkdir" (ResourceMkdirParams) -> ResourceMkdirResult,
resource_request: on_resource_request => "resourceRequest" (ResourceRequestParams) -> ResourceRequestResult,
create_resource_watch: on_create_resource_watch => "createResourceWatch" (CreateResourceWatchParams) -> CreateResourceWatchResult,
}
#[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,
server_request_handler: std::sync::Mutex::new(None),
});
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,
client_info: 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> {
self.subscribe_with_delivery(uri, None).await
}
pub async fn subscribe_with_delivery(
&self,
uri: String,
delivery: Option<SubscriptionDeliveryOptions>,
) -> Result<(SubscribeResult, SessionSubscription), ClientError> {
self.subscribe_with_options(uri, delivery, None).await
}
pub async fn subscribe_with_options(
&self,
uri: String,
delivery: Option<SubscriptionDeliveryOptions>,
view: Option<SubscribeView>,
) -> Result<(SubscribeResult, SessionSubscription), ClientError> {
let sub = self.attach_subscription(&uri).await;
let result: SubscribeResult = self
.request(
"subscribe",
SubscribeParams {
channel: uri,
delivery,
view,
},
)
.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 })
}
pub fn set_server_request_handler<F, Fut>(&self, handler: F)
where
F: Fn(String, Value) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Value, JsonRpcError>> + Send + 'static,
{
let handler: ServerRequestHandler =
Arc::new(move |method, params| Box::pin(handler(method, params)));
if let Ok(mut guard) = self.shared.server_request_handler.lock() {
*guard = Some(handler);
}
}
pub fn set_resource_request_handlers(&self, handlers: ResourceRequestHandlers) {
if let Ok(mut guard) = self.shared.server_request_handler.lock() {
*guard = Some(handlers.into_handler());
}
}
pub fn clear_server_request_handler(&self) {
if let Ok(mut guard) = self.shared.server_request_handler.lock() {
*guard = None;
}
}
pub async fn resource_read(
&self,
mut params: ResourceReadParams,
) -> Result<ResourceReadResult, ClientError> {
params.channel = ROOT_RESOURCE_URI.to_string();
self.request("resourceRead", params).await
}
pub async fn resource_write(
&self,
mut params: ResourceWriteParams,
) -> Result<ResourceWriteResult, ClientError> {
params.channel = ROOT_RESOURCE_URI.to_string();
self.request("resourceWrite", params).await
}
pub async fn resource_list(
&self,
mut params: ResourceListParams,
) -> Result<ResourceListResult, ClientError> {
params.channel = ROOT_RESOURCE_URI.to_string();
self.request("resourceList", params).await
}
pub async fn resource_copy(
&self,
mut params: ResourceCopyParams,
) -> Result<ResourceCopyResult, ClientError> {
params.channel = ROOT_RESOURCE_URI.to_string();
self.request("resourceCopy", params).await
}
pub async fn resource_delete(
&self,
mut params: ResourceDeleteParams,
) -> Result<ResourceDeleteResult, ClientError> {
params.channel = ROOT_RESOURCE_URI.to_string();
self.request("resourceDelete", params).await
}
pub async fn resource_move(
&self,
mut params: ResourceMoveParams,
) -> Result<ResourceMoveResult, ClientError> {
params.channel = ROOT_RESOURCE_URI.to_string();
self.request("resourceMove", params).await
}
pub async fn resource_resolve(
&self,
mut params: ResourceResolveParams,
) -> Result<ResourceResolveResult, ClientError> {
params.channel = ROOT_RESOURCE_URI.to_string();
self.request("resourceResolve", params).await
}
pub async fn resource_mkdir(
&self,
mut params: ResourceMkdirParams,
) -> Result<ResourceMkdirResult, ClientError> {
params.channel = ROOT_RESOURCE_URI.to_string();
self.request("resourceMkdir", params).await
}
pub async fn resource_request(
&self,
mut params: ResourceRequestParams,
) -> Result<ResourceRequestResult, ClientError> {
params.channel = ROOT_RESOURCE_URI.to_string();
self.request("resourceRequest", params).await
}
pub async fn create_resource_watch(
&self,
mut params: CreateResourceWatchParams,
) -> Result<CreateResourceWatchResult, ClientError> {
params.channel = ROOT_RESOURCE_URI.to_string();
self.request("createResourceWatch", params).await
}
}
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: &Arc<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) => {
let shared = shared.clone();
tokio::spawn(async move {
handle_server_request(&shared, r).await;
});
}
}
}
async fn handle_server_request(shared: &Shared, req: JsonRpcRequest) {
let handler = shared
.server_request_handler
.lock()
.ok()
.and_then(|guard| guard.clone());
let id = req.id;
let params = req.params.unwrap_or(Value::Null);
let response = match handler {
Some(handler) => match handler(req.method, params).await {
Ok(result) => JsonRpcMessage::SuccessResponse(JsonRpcSuccessResponse {
jsonrpc: JsonRpcVersion::V2,
id,
result,
}),
Err(error) => JsonRpcMessage::ErrorResponse(JsonRpcErrorResponse {
jsonrpc: JsonRpcVersion::V2,
id,
error,
}),
},
None => JsonRpcMessage::ErrorResponse(JsonRpcErrorResponse {
jsonrpc: JsonRpcVersion::V2,
id,
error: method_not_found(&req.method),
}),
};
let _ = shared.outbound.send(Outbound::Message(response)).await;
}
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,
});
}
}
}