use rmcp::model::ResourceUpdatedNotificationParam;
use rmcp::service::Peer;
use rmcp::RoleServer;
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Debug, Default)]
pub struct SubscriptionRegistry {
inner: Mutex<HashMap<String, Vec<Peer<RoleServer>>>>,
}
impl SubscriptionRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn subscribe(&self, uri: &str, peer: Peer<RoleServer>) {
let mut guard = self.lock();
guard.entry(uri.to_string()).or_default().push(peer);
}
pub fn unsubscribe(&self, uri: &str, _peer: &Peer<RoleServer>) -> usize {
let mut guard = self.lock();
guard.remove(uri).map_or(0, |v| v.len())
}
pub fn subscribers_for(&self, uri: &str) -> Vec<Peer<RoleServer>> {
let guard = self.lock();
guard.get(uri).cloned().unwrap_or_default()
}
pub fn subscribed_uris(&self) -> Vec<String> {
let guard = self.lock();
guard.keys().cloned().collect()
}
pub fn clear(&self) {
let mut guard = self.lock();
guard.clear();
}
pub fn notify_updated(&self, uri: &str) {
let peers = self.subscribers_for(uri);
if peers.is_empty() {
return;
}
for peer in peers {
let uri = uri.to_string();
let Ok(handle) = tokio::runtime::Handle::try_current() else {
return;
};
let params = ResourceUpdatedNotificationParam { uri: uri.clone() };
handle.spawn(async move {
if let Err(e) = peer.notify_resource_updated(params).await {
tracing::debug!(uri = %uri, error = ?e, "resource update notify failed");
}
});
}
}
pub fn notify_list_changed(&self) {
let peers: Vec<Peer<RoleServer>> = {
let guard = self.lock();
guard.values().flat_map(|v| v.iter().cloned()).collect()
};
if peers.is_empty() {
return;
}
let Ok(handle) = tokio::runtime::Handle::try_current() else {
return;
};
for peer in peers {
handle.spawn(async move {
if let Err(e) = peer.notify_resource_list_changed().await {
tracing::debug!(error = ?e, "resource list changed notify failed");
}
});
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Vec<Peer<RoleServer>>>> {
self.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
#[must_use]
pub fn uris_for_table_change(table: &str) -> Vec<String> {
vec![
"hyper://workspace".into(),
"hyper://tables".into(),
"hyper://readme".into(),
format!("hyper://tables/{table}/schema"),
format!("hyper://tables/{table}/sample"),
format!("hyper://tables/{table}/csv-sample"),
]
}
#[must_use]
pub fn uris_for_workspace_change() -> Vec<&'static str> {
vec!["hyper://workspace", "hyper://tables", "hyper://readme"]
}