use crate::error::Result;
use crate::types::subscriptions::{
subscription_kind_of, tag_notification_with_subscription_id, SubscriptionFilter,
};
use crate::types::{protocol::ResourceUpdatedParams, RequestId, ServerNotification};
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
use tokio::sync::RwLock;
#[derive(Clone)]
pub struct SubscriptionManager {
subscriptions: Arc<RwLock<HashMap<String, HashSet<String>>>>,
notification_sender: Option<Arc<dyn Fn(ServerNotification) + Send + Sync>>,
}
impl Default for SubscriptionManager {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for SubscriptionManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SubscriptionManager")
.field(
"subscriptions",
&self.subscriptions.try_read().map_or(0, |s| s.len()),
)
.finish()
}
}
impl SubscriptionManager {
pub fn new() -> Self {
Self {
subscriptions: Arc::new(RwLock::new(HashMap::new())),
notification_sender: None,
}
}
pub fn set_notification_sender<F>(&mut self, sender: F)
where
F: Fn(ServerNotification) + Send + Sync + 'static,
{
self.notification_sender = Some(Arc::new(sender));
}
pub async fn subscribe(&self, uri: String, subscriber_id: String) -> Result<()> {
self.subscriptions
.write()
.await
.entry(uri)
.or_default()
.insert(subscriber_id);
Ok(())
}
pub async fn unsubscribe(&self, uri: String, subscriber_id: String) -> Result<()> {
let mut subs = self.subscriptions.write().await;
if let Some(subscribers) = subs.get_mut(&uri) {
subscribers.remove(&subscriber_id);
if subscribers.is_empty() {
subs.remove(&uri);
drop(subs);
}
}
Ok(())
}
pub async fn unsubscribe_all(&self, subscriber_id: &str) -> Result<()> {
let mut subs = self.subscriptions.write().await;
let mut empty_uris = Vec::new();
for (uri, subscribers) in subs.iter_mut() {
subscribers.remove(subscriber_id);
if subscribers.is_empty() {
empty_uris.push(uri.clone());
}
}
for uri in empty_uris {
subs.remove(&uri);
}
drop(subs);
Ok(())
}
pub async fn has_subscribers(&self, uri: &str) -> bool {
let subs = self.subscriptions.read().await;
subs.get(uri).is_some_and(|s| !s.is_empty())
}
pub async fn get_subscriptions(&self, subscriber_id: &str) -> Vec<String> {
let subs = self.subscriptions.read().await;
subs.iter()
.filter_map(|(uri, subscribers)| {
if subscribers.contains(subscriber_id) {
Some(uri.clone())
} else {
None
}
})
.collect()
}
pub async fn get_subscribers(&self, uri: &str) -> Vec<String> {
let subs = self.subscriptions.read().await;
subs.get(uri)
.map(|s| s.iter().cloned().collect())
.unwrap_or_default()
}
pub async fn notify_resource_updated(&self, uri: String) -> Result<usize> {
let subs = self.subscriptions.read().await;
if let Some(subscribers) = subs.get(&uri) {
let subscriber_count = subscribers.len();
drop(subs);
if subscriber_count > 0 {
if let Some(sender) = &self.notification_sender {
let notification =
ServerNotification::ResourceUpdated(ResourceUpdatedParams::new(&*uri));
sender(notification);
}
return Ok(subscriber_count);
}
}
Ok(0)
}
pub async fn get_stats(&self) -> SubscriptionStats {
let subs = self.subscriptions.read().await;
let total_resources = subs.len();
let total_subscriptions = subs.values().map(std::collections::HashSet::len).sum();
let mut subscriber_counts = HashMap::new();
for subscribers in subs.values() {
for subscriber in subscribers {
*subscriber_counts.entry(subscriber.clone()).or_insert(0) += 1;
}
}
drop(subs);
SubscriptionStats {
total_resources,
total_subscriptions,
unique_subscribers: subscriber_counts.len(),
subscriptions_per_resource: if total_resources > 0 {
#[allow(clippy::cast_precision_loss)]
{
total_subscriptions as f64 / total_resources as f64
}
} else {
0.0
},
}
}
}
pub(crate) const LISTEN_CHANNEL_CAPACITY: usize = 64;
pub(crate) const MAX_LISTEN_STREAMS_PER_PRINCIPAL: usize = 4;
pub(crate) const MAX_LISTEN_STREAMS_TOTAL: usize = 64;
pub(crate) const LISTEN_OVERFLOW_NOTICE: &str =
"subscription buffer overflow: this stream is closing; re-issue subscriptions/listen";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ListenFrame {
Message(String),
Comment(&'static str),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct ListenKey {
pub principal: String,
pub request_id: RequestId,
}
struct ListenEntry {
sender: tokio::sync::mpsc::Sender<ListenFrame>,
filter: SubscriptionFilter,
terminal: String,
generation: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ListenRejection {
PerPrincipalLimit,
GlobalLimit,
DuplicateSubscriptionId,
}
impl ListenRejection {
pub(crate) fn message(self) -> &'static str {
match self {
Self::PerPrincipalLimit => {
"too many concurrent subscriptions/listen streams for this principal"
},
Self::GlobalLimit => "too many concurrent subscriptions/listen streams on this server",
Self::DuplicateSubscriptionId => {
"a subscriptions/listen stream is already open for this subscription id"
},
}
}
pub(crate) fn code(self) -> i32 {
match self {
Self::PerPrincipalLimit | Self::GlobalLimit | Self::DuplicateSubscriptionId => {
crate::types::protocol::error_codes::RATE_LIMITED
},
}
}
}
pub(crate) fn anonymous_principal() -> String {
static NEXT: AtomicU64 = AtomicU64::new(0);
format!("anon#{}", NEXT.fetch_add(1, Ordering::Relaxed))
}
pub struct ListenRegistry {
entries: parking_lot::RwLock<HashMap<ListenKey, ListenEntry>>,
global: Arc<tokio::sync::Semaphore>,
per_principal: parking_lot::Mutex<HashMap<String, Arc<tokio::sync::Semaphore>>>,
next_generation: AtomicU64,
}
impl Default for ListenRegistry {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for ListenRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ListenRegistry")
.field("entries", &self.live_streams())
.finish()
}
}
pub(crate) struct ListenGuard {
key: ListenKey,
generation: u64,
registry: Arc<ListenRegistry>,
principal_permit: Option<tokio::sync::OwnedSemaphorePermit>,
global_permit: Option<tokio::sync::OwnedSemaphorePermit>,
}
impl std::fmt::Debug for ListenGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ListenGuard")
.field("request_id", &self.key.request_id)
.finish_non_exhaustive()
}
}
impl Drop for ListenGuard {
fn drop(&mut self) {
self.registry.remove_entry(&self.key, self.generation);
drop(self.principal_permit.take());
drop(self.global_permit.take());
self.registry.prune_principal(&self.key.principal);
}
}
impl ListenRegistry {
#[must_use]
pub fn new() -> Self {
Self::with_limits(MAX_LISTEN_STREAMS_TOTAL)
}
fn with_limits(global: usize) -> Self {
Self {
entries: parking_lot::RwLock::new(HashMap::new()),
global: Arc::new(tokio::sync::Semaphore::new(global)),
per_principal: parking_lot::Mutex::new(HashMap::new()),
next_generation: AtomicU64::new(0),
}
}
pub(crate) fn live_streams(&self) -> usize {
self.entries.read().len()
}
pub(crate) fn register(
self: &Arc<Self>,
key: ListenKey,
filter: SubscriptionFilter,
sender: tokio::sync::mpsc::Sender<ListenFrame>,
terminal: String,
) -> std::result::Result<ListenGuard, ListenRejection> {
let global_permit = Arc::clone(&self.global)
.try_acquire_owned()
.map_err(|_| ListenRejection::GlobalLimit)?;
let principal_semaphore = {
let mut per_principal = self.per_principal.lock();
Arc::clone(
per_principal
.entry(key.principal.clone())
.or_insert_with(|| {
Arc::new(tokio::sync::Semaphore::new(
MAX_LISTEN_STREAMS_PER_PRINCIPAL,
))
}),
)
};
let Ok(principal_permit) = principal_semaphore.try_acquire_owned() else {
self.prune_after_rejection(&key.principal, None);
return Err(ListenRejection::PerPrincipalLimit);
};
let generation = self.next_generation.fetch_add(1, Ordering::Relaxed);
let stored_key = key.clone();
let occupied = {
let mut entries = self.entries.write();
let occupied = match entries.entry(stored_key) {
Entry::Occupied(_) => true,
Entry::Vacant(slot) => {
slot.insert(ListenEntry {
sender,
filter,
terminal,
generation,
});
false
},
};
drop(entries);
occupied
};
if occupied {
self.prune_after_rejection(&key.principal, Some(principal_permit));
return Err(ListenRejection::DuplicateSubscriptionId);
}
Ok(ListenGuard {
key,
generation,
registry: Arc::clone(self),
principal_permit: Some(principal_permit),
global_permit: Some(global_permit),
})
}
pub(crate) fn fan_out(&self, notification: &ServerNotification) {
if self.entries.read().is_empty() {
return;
}
let Some(kind) = subscription_kind_of(notification) else {
return;
};
let Ok(mut frame) = serde_json::to_value(notification) else {
tracing::warn!(target: "mcp.subscriptions", "notification did not serialize; not fanned out");
return;
};
if let Some(object) = frame.as_object_mut() {
object.insert(
"jsonrpc".to_string(),
serde_json::Value::String("2.0".into()),
);
}
let mut overflowed: Vec<(ListenKey, u64)> = Vec::new();
{
let entries = self.entries.read();
for (key, entry) in entries.iter() {
if !entry.filter.covers(&kind) {
continue;
}
if entry.sender.capacity() <= 1 {
overflowed.push((key.clone(), entry.generation));
continue;
}
tag_notification_with_subscription_id(&mut frame, &key.request_id);
if entry
.sender
.try_send(ListenFrame::Message(frame.to_string()))
.is_err()
{
tracing::debug!(
target: "mcp.subscriptions",
"listen frame not delivered (stream closed, or the buffer filled \
between the capacity check and the send); skipping"
);
}
}
}
for (key, generation) in overflowed {
self.disconnect_overflowed(&key, generation);
}
}
fn disconnect_overflowed(&self, key: &ListenKey, generation: u64) {
let Some(entry) = self.take_entry(key, generation) else {
return;
};
let _ = entry
.sender
.try_send(ListenFrame::Comment(LISTEN_OVERFLOW_NOTICE));
tracing::warn!(
target: "mcp.subscriptions",
request_id = %key.request_id,
capacity = LISTEN_CHANNEL_CAPACITY,
"subscriptions/listen subscriber fell behind; closing its stream"
);
}
pub(crate) fn close_all(&self) {
let drained: Vec<ListenEntry> = self.entries.write().drain().map(|(_, e)| e).collect();
for entry in drained {
let _ = entry
.sender
.try_send(ListenFrame::Message(entry.terminal.clone()));
}
}
fn take_entry(&self, key: &ListenKey, generation: u64) -> Option<ListenEntry> {
let mut entries = self.entries.write();
if entries.get(key).is_some_and(|e| e.generation == generation) {
entries.remove(key)
} else {
None
}
}
fn remove_entry(&self, key: &ListenKey, generation: u64) {
drop(self.take_entry(key, generation));
}
fn prune_after_rejection(
&self,
principal: &str,
permit: Option<tokio::sync::OwnedSemaphorePermit>,
) {
drop(permit);
self.prune_principal(principal);
}
fn prune_principal(&self, principal: &str) {
let mut per_principal = self.per_principal.lock();
let prune = per_principal
.get(principal)
.is_some_and(|s| Arc::strong_count(s) == 1);
if prune {
per_principal.remove(principal);
}
}
}
#[derive(Debug, Clone)]
pub struct SubscriptionStats {
pub total_resources: usize,
pub total_subscriptions: usize,
pub unique_subscribers: usize,
pub subscriptions_per_resource: f64,
}
#[cfg(test)]
mod tests {
use super::*;
const THIS_MODULE_SOURCE: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/server/subscriptions.rs"
));
fn flattened(text: &str) -> String {
text.lines()
.map(|line| {
line.trim_start()
.trim_start_matches('/')
.trim_start_matches('!')
})
.collect::<Vec<_>>()
.join(" ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}
#[test]
fn d11_rustdoc_must_not_reintroduce_the_retired_false_spec_claims() {
const NO_POLLING_SHAPE_HEAD: &str = "no polling shape for change";
const NO_POLLING_SHAPE_TAIL: &str = " notifications anywhere in the MCP spec";
const ONLY_CONFORMANT_HEAD: &str = "the only spec-conformant delivery";
const ONLY_CONFORMANT_TAIL: &str = " shape for `listChanged`";
let flat = flattened(THIS_MODULE_SOURCE);
for (head, tail) in [
(NO_POLLING_SHAPE_HEAD, NO_POLLING_SHAPE_TAIL),
(ONLY_CONFORMANT_HEAD, ONLY_CONFORMANT_TAIL),
] {
let forbidden = format!("{head}{tail}");
assert!(
forbidden.len() >= 40,
"the assembled phrase must stay long enough to be a real needle; \
a fragment was emptied and this scan would have become vacuous: \
{forbidden:?}"
);
assert!(
!flat.contains(&forbidden),
"src/server/subscriptions.rs asserts something the MCP spec \
contradicts. The spec's polling shape for change notifications \
is TTL-driven re-fetch via `ttlMs` / `cacheScope` (SEP-2549), \
specified in the caching utility, which blesses relying on it \
instead of `listChanged`. pmcp does not implement it (SCHM-03, \
Phase 115) — that is what the rustdoc must say. Offending \
phrase: {forbidden:?}"
);
}
}
#[test]
fn d11_rustdoc_names_the_specs_real_polling_shape_and_its_owner() {
let module_doc: String = THIS_MODULE_SOURCE
.lines()
.take_while(|line| line.starts_with("//!") || line.trim().is_empty())
.collect::<Vec<_>>()
.join("\n");
for needle in ["ttlMs", "cacheScope", "SEP-2549", "SCHM-03"] {
assert!(
module_doc.contains(needle),
"the corrected D-11 block must name {needle} — a reader who is \
told Tasks-polling is not the spec's shape needs to be told \
what the spec's shape IS and who owns it"
);
}
}
#[tokio::test]
async fn test_subscribe_unsubscribe() {
let manager = SubscriptionManager::new();
manager
.subscribe("file://test.txt".to_string(), "client1".to_string())
.await
.unwrap();
assert!(manager.has_subscribers("file://test.txt").await);
let subs = manager.get_subscriptions("client1").await;
assert_eq!(subs.len(), 1);
assert_eq!(subs[0], "file://test.txt");
manager
.unsubscribe("file://test.txt".to_string(), "client1".to_string())
.await
.unwrap();
assert!(!manager.has_subscribers("file://test.txt").await);
let subs = manager.get_subscriptions("client1").await;
assert_eq!(subs.len(), 0);
}
#[tokio::test]
async fn test_multiple_subscribers() {
let manager = SubscriptionManager::new();
manager
.subscribe("file://shared.txt".to_string(), "client1".to_string())
.await
.unwrap();
manager
.subscribe("file://shared.txt".to_string(), "client2".to_string())
.await
.unwrap();
let subscribers = manager.get_subscribers("file://shared.txt").await;
assert_eq!(subscribers.len(), 2);
assert!(subscribers.contains(&"client1".to_string()));
assert!(subscribers.contains(&"client2".to_string()));
manager
.unsubscribe("file://shared.txt".to_string(), "client1".to_string())
.await
.unwrap();
assert!(manager.has_subscribers("file://shared.txt").await);
let subscribers = manager.get_subscribers("file://shared.txt").await;
assert_eq!(subscribers.len(), 1);
assert_eq!(subscribers[0], "client2");
}
#[tokio::test]
async fn test_unsubscribe_all() {
let manager = SubscriptionManager::new();
manager
.subscribe("file://test1.txt".to_string(), "client1".to_string())
.await
.unwrap();
manager
.subscribe("file://test2.txt".to_string(), "client1".to_string())
.await
.unwrap();
manager
.subscribe("file://test3.txt".to_string(), "client1".to_string())
.await
.unwrap();
manager
.subscribe("file://test2.txt".to_string(), "client2".to_string())
.await
.unwrap();
manager.unsubscribe_all("client1").await.unwrap();
let subs = manager.get_subscriptions("client1").await;
assert_eq!(subs.len(), 0);
assert!(manager.has_subscribers("file://test2.txt").await);
assert!(!manager.has_subscribers("file://test1.txt").await);
assert!(!manager.has_subscribers("file://test3.txt").await);
}
#[tokio::test]
async fn test_stats() {
let manager = SubscriptionManager::new();
manager
.subscribe("file://test1.txt".to_string(), "client1".to_string())
.await
.unwrap();
manager
.subscribe("file://test1.txt".to_string(), "client2".to_string())
.await
.unwrap();
manager
.subscribe("file://test2.txt".to_string(), "client1".to_string())
.await
.unwrap();
manager
.subscribe("file://test3.txt".to_string(), "client3".to_string())
.await
.unwrap();
let stats = manager.get_stats().await;
assert_eq!(stats.total_resources, 3);
assert_eq!(stats.total_subscriptions, 4);
assert_eq!(stats.unique_subscribers, 3);
assert!((stats.subscriptions_per_resource - 1.33).abs() < 0.01);
}
#[tokio::test]
async fn test_notify_resource_updated() {
use std::sync::Mutex;
let manager = SubscriptionManager::new();
let notifications = Arc::new(Mutex::new(Vec::new()));
let notifications_clone = notifications.clone();
let mut manager_mut = manager.clone();
manager_mut.set_notification_sender(move |notif| {
notifications_clone.lock().unwrap().push(notif);
});
manager_mut
.subscribe("file://test.txt".to_string(), "client1".to_string())
.await
.unwrap();
let count = manager_mut
.notify_resource_updated("file://test.txt".to_string())
.await
.unwrap();
assert_eq!(count, 1);
let notifs = notifications.lock().unwrap();
assert_eq!(notifs.len(), 1);
match ¬ifs[0] {
ServerNotification::ResourceUpdated(n) => assert_eq!(n.uri, "file://test.txt"),
_ => panic!("Wrong notification type"),
}
}
mod listen_registry {
use super::*;
use crate::types::notifications::{LogMessageParams, LoggingLevel};
fn tools_only() -> SubscriptionFilter {
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
}
}
fn prompts_only() -> SubscriptionFilter {
SubscriptionFilter {
prompts_list_changed: Some(true),
..SubscriptionFilter::default()
}
}
type Opened = (ListenGuard, tokio::sync::mpsc::Receiver<ListenFrame>);
fn key_for(principal: &str, id: i64) -> ListenKey {
ListenKey {
principal: principal.to_string(),
request_id: RequestId::Number(id),
}
}
fn overflow_the_only_subscriber(registry: &Arc<ListenRegistry>) {
for _ in 0..LISTEN_CHANNEL_CAPACITY + 8 {
registry.fan_out(&ServerNotification::ToolsChanged);
}
assert_eq!(
registry.live_streams(),
0,
"the overflow policy evicts the subscriber that fell behind"
);
}
fn open(
registry: &Arc<ListenRegistry>,
principal: &str,
id: i64,
filter: SubscriptionFilter,
) -> std::result::Result<Opened, ListenRejection> {
let (tx, rx) = tokio::sync::mpsc::channel(LISTEN_CHANNEL_CAPACITY + 1);
tx.try_send(ListenFrame::Message("{\"ack\":true}".to_string()))
.expect("a fresh channel has room for the acknowledgement");
let guard = registry.register(
key_for(principal, id),
filter,
tx,
format!("{{\"id\":{id},\"result\":{{}}}}"),
)?;
Ok((guard, rx))
}
fn open_up_to_the_cap(registry: &Arc<ListenRegistry>, principal: &str) -> Vec<Opened> {
(0..MAX_LISTEN_STREAMS_PER_PRINCIPAL)
.map(|id| {
let id = i64::try_from(id).expect("the cap is small");
open(registry, principal, id, tools_only()).expect("within the cap")
})
.collect()
}
fn skip_ack(rx: &mut tokio::sync::mpsc::Receiver<ListenFrame>) {
match rx.try_recv() {
Ok(ListenFrame::Message(_)) => {},
other => panic!("the FIRST frame must be the acknowledgement, got {other:?}"),
}
}
#[tokio::test]
async fn two_principals_sharing_request_id_one_do_not_cross() {
let registry = Arc::new(ListenRegistry::new());
let (_alice, mut alice_rx) =
open(®istry, "alice", 1, tools_only()).expect("alice registers");
let (_bob, mut bob_rx) =
open(®istry, "bob", 1, prompts_only()).expect("bob registers");
assert_eq!(
registry.live_streams(),
2,
"the PAIR key keeps both entries alive"
);
skip_ack(&mut alice_rx);
skip_ack(&mut bob_rx);
registry.fan_out(&ServerNotification::ToolsChanged);
let Ok(ListenFrame::Message(frame)) = alice_rx.try_recv() else {
panic!("alice requested toolsListChanged and must receive it");
};
assert!(frame.contains("notifications/tools/list_changed"));
assert!(
bob_rx.try_recv().is_err(),
"bob requested only promptsListChanged and must receive nothing"
);
}
#[tokio::test]
async fn two_principals_sharing_request_id_one_hold_two_distinct_entries() {
use crate::types::subscriptions::SUBSCRIPTION_ID_META_KEY;
let registry = Arc::new(ListenRegistry::new());
let (_alice, mut alice_rx) =
open(®istry, "alice", 1, tools_only()).expect("alice registers");
let (_bob, mut bob_rx) =
open(®istry, "bob", 1, tools_only()).expect("bob registers under the same id");
{
let entries = registry.entries.read();
assert!(
entries.contains_key(&key_for("alice", 1)),
"alice's EXACT key survived bob's registration"
);
assert!(
entries.contains_key(&key_for("bob", 1)),
"and bob's EXACT key is present too — two entries, not one \
replaced twice"
);
assert_eq!(entries.len(), 2, "and there are exactly those two");
}
skip_ack(&mut alice_rx);
skip_ack(&mut bob_rx);
registry.fan_out(&ServerNotification::ToolsChanged);
for (owner, rx) in [("alice", &mut alice_rx), ("bob", &mut bob_rx)] {
let Ok(ListenFrame::Message(frame)) = rx.try_recv() else {
panic!("{owner} requested toolsListChanged and must receive it");
};
let value: serde_json::Value = serde_json::from_str(&frame).expect("json");
assert_eq!(
value["method"],
serde_json::json!("notifications/tools/list_changed"),
"{owner} receives the fanned-out notification"
);
assert_eq!(
value["params"]["_meta"][SUBSCRIPTION_ID_META_KEY],
serde_json::json!(1),
"{owner}'s frame is tagged with ITS OWN subscriptionId"
);
}
}
#[tokio::test]
async fn an_unrequested_notification_type_is_never_delivered() {
let registry = Arc::new(ListenRegistry::new());
let (_guard, mut rx) = open(®istry, "alice", 1, tools_only()).expect("registers");
skip_ack(&mut rx);
registry.fan_out(&ServerNotification::PromptsChanged);
registry.fan_out(&ServerNotification::ResourcesChanged);
assert!(
rx.try_recv().is_err(),
"only the REQUESTED type may reach the stream"
);
registry.fan_out(&ServerNotification::ToolsChanged);
assert!(rx.try_recv().is_ok(), "the requested type does arrive");
}
#[tokio::test]
async fn request_scoped_notifications_are_excluded_from_fan_out() {
use crate::types::{ProgressNotification, ProgressToken};
let registry = Arc::new(ListenRegistry::new());
let everything = SubscriptionFilter {
tools_list_changed: Some(true),
prompts_list_changed: Some(true),
resources_list_changed: Some(true),
resource_subscriptions: Some(vec!["mem://a".to_string()]),
};
let (_guard, mut rx) = open(®istry, "alice", 1, everything).expect("registers");
skip_ack(&mut rx);
registry.fan_out(&ServerNotification::Progress(ProgressNotification::new(
ProgressToken::String("t".to_string()),
1.0,
None,
)));
registry.fan_out(&ServerNotification::LogMessage(LogMessageParams::new(
LoggingLevel::Info,
"hi",
)));
assert!(
rx.try_recv().is_err(),
"`notifications/progress` and `notifications/message` are excluded by construction"
);
}
#[tokio::test]
async fn every_delivered_frame_carries_its_own_subscription_id() {
use crate::types::subscriptions::SUBSCRIPTION_ID_META_KEY;
let registry = Arc::new(ListenRegistry::new());
let (_a, mut a_rx) = open(®istry, "alice", 41, tools_only()).expect("registers");
let (_b, mut b_rx) = open(®istry, "bob", 42, tools_only()).expect("registers");
skip_ack(&mut a_rx);
skip_ack(&mut b_rx);
registry.fan_out(&ServerNotification::ToolsChanged);
for (rx, expected) in [(&mut a_rx, 41), (&mut b_rx, 42)] {
let Ok(ListenFrame::Message(frame)) = rx.try_recv() else {
panic!("both subscribers requested the type");
};
let value: serde_json::Value = serde_json::from_str(&frame).expect("json");
assert_eq!(value["jsonrpc"], serde_json::json!("2.0"));
assert_eq!(
value["params"]["_meta"][SUBSCRIPTION_ID_META_KEY],
serde_json::json!(expected),
"each entry is tagged with ITS OWN subscriptionId"
);
}
}
#[tokio::test]
async fn the_per_principal_cap_rejects_the_next_stream() {
let registry = Arc::new(ListenRegistry::new());
let held = open_up_to_the_cap(®istry, "alice");
assert_eq!(held.len(), MAX_LISTEN_STREAMS_PER_PRINCIPAL);
assert_eq!(
open(®istry, "alice", 99, tools_only()).err(),
Some(ListenRejection::PerPrincipalLimit),
"the N+1th stream for one principal is rejected"
);
assert!(open(®istry, "bob", 0, tools_only()).is_ok());
}
#[tokio::test]
async fn the_global_cap_rejects_too() {
let registry = Arc::new(ListenRegistry::with_limits(2));
let _a = open(®istry, "a", 1, tools_only()).expect("first");
let _b = open(®istry, "b", 1, tools_only()).expect("second");
assert_eq!(
open(®istry, "c", 1, tools_only()).err(),
Some(ListenRejection::GlobalLimit)
);
}
#[tokio::test]
async fn dropping_the_guard_empties_the_registry_and_releases_the_permit() {
let registry = Arc::new(ListenRegistry::new());
let mut held = open_up_to_the_cap(®istry, "alice");
assert_eq!(registry.live_streams(), MAX_LISTEN_STREAMS_PER_PRINCIPAL);
assert!(open(®istry, "alice", 99, tools_only()).is_err());
drop(held.pop().expect("one open stream"));
assert_eq!(
registry.live_streams(),
MAX_LISTEN_STREAMS_PER_PRINCIPAL - 1,
"Drop removed the registry entry"
);
assert!(
open(®istry, "alice", 99, tools_only()).is_ok(),
"Drop released the concurrency permit too"
);
}
#[tokio::test]
async fn a_full_channel_closes_that_subscriber() {
let registry = Arc::new(ListenRegistry::new());
let (_guard, mut rx) = open(®istry, "slow", 1, tools_only()).expect("registers");
overflow_the_only_subscriber(®istry);
let mut frames = Vec::new();
while let Ok(frame) = rx.try_recv() {
frames.push(frame);
}
assert!(
frames.len() <= LISTEN_CHANNEL_CAPACITY + 1,
"per-subscriber memory is bounded by the constant, got {}",
frames.len()
);
assert_eq!(
frames.last(),
Some(&ListenFrame::Comment(LISTEN_OVERFLOW_NOTICE)),
"the reserved slot carries the terminal overflow notice"
);
assert!(
rx.recv().await.is_none(),
"the sender was dropped, so the stream ends"
);
}
#[tokio::test]
async fn close_all_sends_the_terminal_result_then_ends_each_stream() {
let registry = Arc::new(ListenRegistry::new());
let (_guard, mut rx) = open(®istry, "alice", 5, tools_only()).expect("registers");
skip_ack(&mut rx);
registry.close_all();
assert_eq!(registry.live_streams(), 0);
let Ok(ListenFrame::Message(frame)) = rx.try_recv() else {
panic!("graceful shutdown sends the terminal result first");
};
assert!(frame.contains("\"id\":5"));
assert!(
rx.recv().await.is_none(),
"then the sender drops and the stream ends"
);
}
#[tokio::test]
async fn anonymous_principals_are_never_shared() {
let a = anonymous_principal();
let b = anonymous_principal();
assert_ne!(a, b, "each anonymous stream is its OWN principal");
}
#[tokio::test]
async fn a_dropped_principal_semaphore_is_pruned() {
let registry = Arc::new(ListenRegistry::new());
{
let _held = open(®istry, "ephemeral", 1, tools_only()).expect("registers");
assert_eq!(registry.per_principal.lock().len(), 1);
}
assert_eq!(
registry.per_principal.lock().len(),
0,
"the per-principal semaphore map does not grow without bound"
);
}
#[tokio::test]
async fn the_rejection_path_prunes_a_semaphore_the_incumbent_could_not() {
let registry = Arc::new(ListenRegistry::new());
let (guard_a, _a_rx) = open(®istry, "raced", 1, tools_only()).expect("A registers");
let standin = {
let per_principal = registry.per_principal.lock();
Arc::clone(per_principal.get("raced").expect("A created the entry"))
}
.try_acquire_owned()
.expect("the per-principal cap is 4, so a second permit is available");
drop(guard_a);
assert_eq!(registry.live_streams(), 0, "A's registry entry is gone");
assert_eq!(
registry.per_principal.lock().len(),
1,
"but its semaphore is NOT: the incumbent's prune saw the rejecting \
call's in-flight Arc and declined. THIS is the leaked state WR-06 \
describes, and nothing else would ever remove it."
);
registry.prune_after_rejection("raced", Some(standin));
assert_eq!(
registry.per_principal.lock().len(),
0,
"the rejection path prunes what the incumbent could not"
);
}
#[tokio::test]
async fn concurrent_register_churn_leaves_no_orphaned_semaphores() {
const THREADS: usize = 4;
const ITERATIONS: usize = 30;
let registry = Arc::new(ListenRegistry::new());
std::thread::scope(|scope| {
for thread in 0..THREADS {
let registry = Arc::clone(®istry);
scope.spawn(move || {
for iteration in 0..ITERATIONS {
let principal = if thread % 2 == 0 { "even" } else { "odd" };
let id = i64::try_from(iteration % 2).expect("0 or 1");
drop(open(®istry, principal, id, tools_only()));
}
});
}
});
assert_eq!(registry.live_streams(), 0, "every guard was dropped");
assert_eq!(
registry.per_principal.lock().len(),
0,
"no principal semaphore outlived the churn"
);
}
mod entry_ownership {
use super::*;
#[tokio::test]
async fn duplicate_key_is_rejected_and_the_first_stream_survives() {
let registry = Arc::new(ListenRegistry::new());
let (_first, mut first_rx) =
open(®istry, "alice", 1, tools_only()).expect("the first stream registers");
skip_ack(&mut first_rx);
assert_eq!(
open(®istry, "alice", 1, tools_only()).err(),
Some(ListenRejection::DuplicateSubscriptionId),
"the SECOND registration is refused, never applied"
);
assert_eq!(
registry.live_streams(),
1,
"the incumbent entry was not evicted"
);
registry.fan_out(&ServerNotification::ToolsChanged);
let Ok(ListenFrame::Message(frame)) = first_rx.try_recv() else {
panic!("the FIRST subscriber's stream must still be open and receiving");
};
assert!(frame.contains("notifications/tools/list_changed"));
}
#[tokio::test]
async fn sequential_reuse_of_a_released_key_still_registers() {
let registry = Arc::new(ListenRegistry::new());
let (first, _first_rx) =
open(®istry, "alice", 1, tools_only()).expect("the first stream registers");
drop(first);
assert_eq!(registry.live_streams(), 0);
let (_second, _second_rx) = open(®istry, "alice", 1, tools_only())
.expect("a RELEASED key is free to reuse — only a LIVE one is refused");
assert_eq!(registry.live_streams(), 1);
}
#[tokio::test]
async fn a_guard_drop_cannot_reclaim_a_successor_at_the_same_key() {
let registry = Arc::new(ListenRegistry::new());
let (guard_a, _a_rx) =
open(®istry, "solo", 1, tools_only()).expect("A registers");
overflow_the_only_subscriber(®istry);
let (_guard_b, mut b_rx) =
open(®istry, "solo", 1, tools_only()).expect("B takes the free slot");
assert_eq!(registry.live_streams(), 1);
skip_ack(&mut b_rx);
drop(guard_a);
assert_eq!(
registry.live_streams(),
1,
"a late guard drop removes only ITS OWN generation (CR-02)"
);
registry.fan_out(&ServerNotification::ToolsChanged);
assert!(
matches!(b_rx.try_recv(), Ok(ListenFrame::Message(_))),
"B's stream is still live and still receiving"
);
}
#[tokio::test]
async fn a_stale_overflow_disconnect_cannot_evict_a_successor() {
let registry = Arc::new(ListenRegistry::new());
let (guard_a, _a_rx) =
open(®istry, "solo", 1, tools_only()).expect("A registers");
let stale_generation = guard_a.generation;
overflow_the_only_subscriber(®istry);
let (_guard_b, mut b_rx) =
open(®istry, "solo", 1, tools_only()).expect("B takes the free slot");
skip_ack(&mut b_rx);
registry.disconnect_overflowed(&key_for("solo", 1), stale_generation);
assert_eq!(
registry.live_streams(),
1,
"a stale disconnect removes NOTHING"
);
registry.fan_out(&ServerNotification::ToolsChanged);
assert!(
matches!(b_rx.try_recv(), Ok(ListenFrame::Message(_))),
"B's stream is untouched by the stale disconnect"
);
}
#[tokio::test]
async fn generations_are_strictly_increasing() {
let registry = Arc::new(ListenRegistry::new());
let held: Vec<Opened> = (0..4)
.map(|id| open(®istry, "alice", id, tools_only()).expect("within the cap"))
.collect();
let generations: Vec<u64> =
held.iter().map(|(guard, _)| guard.generation).collect();
for pair in generations.windows(2) {
assert!(
pair[1] > pair[0],
"every registration draws a strictly larger token: {:?}",
generations
);
}
}
#[tokio::test]
async fn every_listen_refusal_is_retryable_and_only_the_message_distinguishes_them() {
use crate::types::protocol::error_codes::RATE_LIMITED;
for rejection in [
ListenRejection::PerPrincipalLimit,
ListenRejection::GlobalLimit,
ListenRejection::DuplicateSubscriptionId,
] {
assert_eq!(
rejection.code(),
RATE_LIMITED,
"{rejection:?} is transient server state, so it is RETRYABLE"
);
}
for capacity in [
ListenRejection::PerPrincipalLimit,
ListenRejection::GlobalLimit,
] {
assert!(
capacity.message().contains("too many concurrent"),
"a CAP refusal is identified by its message alone: {}",
capacity.message()
);
}
assert!(
!ListenRejection::DuplicateSubscriptionId
.message()
.contains("too many concurrent"),
"the duplicate wording must not read as a capacity refusal"
);
assert!(
ListenRejection::DuplicateSubscriptionId
.message()
.contains("already open for this subscription id"),
"and it must name the real reason, which is what the live \
suite asserts on: {}",
ListenRejection::DuplicateSubscriptionId.message()
);
}
}
}
}