use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc;
use cdk_common::subscription::Params;
use tokio::sync::{mpsc, RwLock};
use tokio::task::JoinHandle;
use tracing::error;
use super::Wallet;
use crate::mint_url::MintUrl;
use crate::pub_sub::SubId;
use crate::wallet::MintConnector;
mod http;
#[cfg(all(
not(feature = "http_subscription"),
feature = "mint",
not(target_arch = "wasm32")
))]
mod ws;
type WsSubscriptionBody = (mpsc::Sender<NotificationPayload>, Params);
#[derive(Debug, Clone)]
pub struct SubscriptionManager {
all_connections: Arc<RwLock<HashMap<MintUrl, SubscriptionClient>>>,
http_client: Arc<dyn MintConnector + Send + Sync>,
}
impl SubscriptionManager {
pub fn new(http_client: Arc<dyn MintConnector + Send + Sync>) -> Self {
Self {
all_connections: Arc::new(RwLock::new(HashMap::new())),
http_client,
}
}
pub async fn subscribe(
&self,
mint_url: MintUrl,
filter: Params,
wallet: Arc<Wallet>,
) -> ActiveSubscription {
let subscription_clients = self.all_connections.read().await;
let id = filter.id.clone();
if let Some(subscription_client) = subscription_clients.get(&mint_url) {
let (on_drop_notif, receiver) = subscription_client.subscribe(filter).await;
ActiveSubscription::new(receiver, id, on_drop_notif)
} else {
drop(subscription_clients);
#[cfg(all(
not(feature = "http_subscription"),
feature = "mint",
not(target_arch = "wasm32")
))]
let is_ws_support = self
.http_client
.get_mint_info()
.await
.map(|info| !info.nuts.nut17.supported.is_empty())
.unwrap_or_default();
#[cfg(any(
feature = "http_subscription",
not(feature = "mint"),
target_arch = "wasm32"
))]
let is_ws_support = false;
tracing::debug!(
"Connect to {:?} to subscribe. WebSocket is supported ({})",
mint_url,
is_ws_support
);
let mut subscription_clients = self.all_connections.write().await;
let subscription_client = SubscriptionClient::new(
mint_url.clone(),
self.http_client.clone(),
is_ws_support,
wallet,
);
let (on_drop_notif, receiver) = subscription_client.subscribe(filter).await;
subscription_clients.insert(mint_url, subscription_client);
ActiveSubscription::new(receiver, id, on_drop_notif)
}
}
}
#[derive(Debug)]
pub struct SubscriptionClient {
new_subscription_notif: mpsc::Sender<SubId>,
on_drop_notif: mpsc::Sender<SubId>,
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
worker: Option<JoinHandle<()>>,
}
type NotificationPayload = crate::nuts::NotificationPayload<String>;
pub struct ActiveSubscription {
sub_id: Option<SubId>,
on_drop_notif: mpsc::Sender<SubId>,
receiver: mpsc::Receiver<NotificationPayload>,
}
impl ActiveSubscription {
fn new(
receiver: mpsc::Receiver<NotificationPayload>,
sub_id: SubId,
on_drop_notif: mpsc::Sender<SubId>,
) -> Self {
Self {
sub_id: Some(sub_id),
on_drop_notif,
receiver,
}
}
pub fn try_recv(&mut self) -> Result<Option<NotificationPayload>, Error> {
match self.receiver.try_recv() {
Ok(payload) => Ok(Some(payload)),
Err(mpsc::error::TryRecvError::Empty) => Ok(None),
Err(mpsc::error::TryRecvError::Disconnected) => Err(Error::Disconnected),
}
}
pub async fn recv(&mut self) -> Option<NotificationPayload> {
self.receiver.recv().await
}
}
impl Drop for ActiveSubscription {
fn drop(&mut self) {
if let Some(sub_id) = self.sub_id.take() {
let _ = self.on_drop_notif.try_send(sub_id);
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Could not join paths: {0}")]
Url(#[from] crate::mint_url::Error),
#[error("Disconnected from the notification channel")]
Disconnected,
}
impl SubscriptionClient {
pub fn new(
url: MintUrl,
http_client: Arc<dyn MintConnector + Send + Sync>,
prefer_ws_method: bool,
wallet: Arc<Wallet>,
) -> Self {
let subscriptions = Arc::new(RwLock::new(HashMap::new()));
let (new_subscription_notif, new_subscription_recv) = mpsc::channel(100);
let (on_drop_notif, on_drop_recv) = mpsc::channel(1000);
Self {
new_subscription_notif,
on_drop_notif,
subscriptions: subscriptions.clone(),
worker: Some(Self::start_worker(
prefer_ws_method,
http_client,
url,
subscriptions,
new_subscription_recv,
on_drop_recv,
wallet,
)),
}
}
#[allow(unused_variables)]
fn start_worker(
prefer_ws_method: bool,
http_client: Arc<dyn MintConnector + Send + Sync>,
url: MintUrl,
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
new_subscription_recv: mpsc::Receiver<SubId>,
on_drop_recv: mpsc::Receiver<SubId>,
wallet: Arc<Wallet>,
) -> JoinHandle<()> {
#[cfg(any(
feature = "http_subscription",
not(feature = "mint"),
target_arch = "wasm32"
))]
return Self::http_worker(
http_client,
subscriptions,
new_subscription_recv,
on_drop_recv,
wallet,
);
#[cfg(all(
not(feature = "http_subscription"),
feature = "mint",
not(target_arch = "wasm32")
))]
if prefer_ws_method {
Self::ws_worker(
http_client,
url,
subscriptions,
new_subscription_recv,
on_drop_recv,
wallet,
)
} else {
Self::http_worker(
http_client,
subscriptions,
new_subscription_recv,
on_drop_recv,
wallet,
)
}
}
pub async fn subscribe(
&self,
filter: Params,
) -> (mpsc::Sender<SubId>, mpsc::Receiver<NotificationPayload>) {
let mut subscriptions = self.subscriptions.write().await;
let id = filter.id.clone();
let (sender, receiver) = mpsc::channel(10_000);
subscriptions.insert(id.clone(), (sender, filter));
drop(subscriptions);
let _ = self.new_subscription_notif.send(id).await;
(self.on_drop_notif.clone(), receiver)
}
fn http_worker(
http_client: Arc<dyn MintConnector + Send + Sync>,
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
new_subscription_recv: mpsc::Receiver<SubId>,
on_drop: mpsc::Receiver<SubId>,
wallet: Arc<Wallet>,
) -> JoinHandle<()> {
let http_worker = http::http_main(
vec![],
http_client,
subscriptions,
new_subscription_recv,
on_drop,
wallet,
);
#[cfg(target_arch = "wasm32")]
let ret = tokio::task::spawn_local(http_worker);
#[cfg(not(target_arch = "wasm32"))]
let ret = tokio::spawn(http_worker);
ret
}
#[cfg(all(
not(feature = "http_subscription"),
feature = "mint",
not(target_arch = "wasm32")
))]
fn ws_worker(
http_client: Arc<dyn MintConnector + Send + Sync>,
url: MintUrl,
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
new_subscription_recv: mpsc::Receiver<SubId>,
on_drop: mpsc::Receiver<SubId>,
wallet: Arc<Wallet>,
) -> JoinHandle<()> {
tokio::spawn(ws::ws_main(
http_client,
url,
subscriptions,
new_subscription_recv,
on_drop,
wallet,
))
}
}
impl Drop for SubscriptionClient {
fn drop(&mut self) {
if let Some(sender) = self.worker.take() {
sender.abort();
}
}
}