use std::sync::Arc;
use arc_swap::ArcSwap;
use crate::application::session::{DynFrameSink, SinkError};
use crate::domain::registry::{PrimitiveKind, Snapshot};
use crate::domain::{Feature, FeatureProfile};
#[derive(Clone)]
pub struct Subscriber {
kind: PrimitiveKind,
also: Option<PrimitiveKind>,
feature: Feature,
method: &'static str,
frame: Arc<str>,
profile: FeatureProfile,
sink: Arc<dyn DynFrameSink>,
}
impl Subscriber {
#[must_use]
pub fn new(
kind: PrimitiveKind,
feature: Feature,
method: &'static str,
frame: &str,
profile: FeatureProfile,
sink: Arc<dyn DynFrameSink>,
) -> Self {
Self {
kind,
also: None,
feature,
method,
frame: Arc::from(frame),
profile,
sink,
}
}
#[must_use]
pub const fn also_for(mut self, kind: PrimitiveKind) -> Self {
self.also = Some(kind);
self
}
#[must_use]
pub const fn kind(&self) -> PrimitiveKind {
self.kind
}
#[must_use]
pub fn covers(&self, kinds: &[PrimitiveKind]) -> bool {
kinds
.iter()
.any(|kind| *kind == self.kind || Some(*kind) == self.also)
}
#[must_use]
pub const fn method(&self) -> &'static str {
self.method
}
#[must_use]
pub fn is_eligible(&self) -> bool {
self.profile.allows(self.feature)
}
}
impl core::fmt::Debug for Subscriber {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Subscriber")
.field("kind", &self.kind)
.field("feature", &self.feature)
.field("method", &self.method)
.field("profile", &self.profile)
.field("eligible", &self.is_eligible())
.finish_non_exhaustive()
}
}
#[derive(Debug, Default)]
pub struct Fanout {
pub sent: usize,
pub gated: usize,
pub failed: usize,
}
impl Fanout {
#[must_use]
pub const fn is_silent(&self) -> bool {
self.sent == 0
}
}
#[derive(Default)]
pub struct SubscriberSet {
entries: ArcSwap<Vec<Arc<Subscriber>>>,
}
impl SubscriberSet {
#[must_use]
pub fn new() -> Self {
Self {
entries: ArcSwap::from_pointee(Vec::new()),
}
}
#[must_use]
pub fn all(&self) -> Vec<Arc<Subscriber>> {
self.entries.load_full().as_ref().clone()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.load().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.load().is_empty()
}
pub fn add(&self, subscriber: Subscriber) {
let mut next: Vec<Arc<Subscriber>> = self.all();
next.push(Arc::new(subscriber));
self.entries.store(Arc::new(next));
}
pub fn remove_by_sink(&self, sink: &Arc<dyn DynFrameSink>) -> usize {
let current = self.all();
let next: Vec<Arc<Subscriber>> = current
.iter()
.filter(|entry| !Arc::ptr_eq(&entry.sink, sink))
.map(Arc::clone)
.collect();
let removed = current.len() - next.len();
if removed > 0 {
self.entries.store(Arc::new(next));
}
removed
}
pub async fn announce(&self, snapshot: &Snapshot, kinds: &[PrimitiveKind]) -> Fanout {
let mut report = Fanout::default();
if kinds.is_empty() {
return report;
}
let generation = snapshot.generation();
for entry in self.all() {
if !entry.covers(kinds) {
continue;
}
if !entry.is_eligible() {
report.gated += 1;
continue;
}
match entry.sink.send(&entry.frame).await {
Ok(()) => report.sent += 1,
Err(SinkError) => report.failed += 1,
}
}
debug_assert!(
generation > 0 || snapshot.is_blank(),
"a published snapshot is numbered"
);
report
}
}
impl core::fmt::Debug for SubscriberSet {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SubscriberSet")
.field("len", &self.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::{Subscriber, SubscriberSet};
use crate::application::session::{DynFrameSink, FrameSink, SinkError};
use crate::domain::registry::PrimitiveKind;
use crate::domain::registry::TestError;
use crate::domain::{Feature, FeatureProfile};
const FRAME: &str = r#"{"jsonrpc":"2.0","method":"notifications/tools/list_changed"}"#;
#[derive(Default)]
struct Counter(AtomicUsize);
impl FrameSink for Counter {
#[expect(
clippy::unused_async_trait_impl,
reason = "FrameSink is a native async fn trait; the signature is not ours to change"
)]
async fn send(&self, _frame: &str) -> Result<(), SinkError> {
self.0.fetch_add(1, Ordering::Relaxed);
Ok(())
}
}
fn watch(profile: FeatureProfile, sink: &Arc<Counter>) -> Subscriber {
Subscriber::new(
PrimitiveKind::Tool,
Feature::ToolsListChanged,
"notifications/tools/list_changed",
FRAME,
profile,
Arc::clone(sink) as Arc<dyn DynFrameSink>,
)
}
fn snapshot() -> crate::domain::registry::Snapshot {
crate::domain::registry::Snapshot::empty()
}
#[test]
fn eligibility_is_the_negotiated_profile() {
let sink = Arc::new(Counter::default());
let opted_in = watch(
FeatureProfile::empty().with(Feature::ToolsListChanged),
&sink,
);
let opted_out = watch(FeatureProfile::empty().with(Feature::Tools), &sink);
assert!(opted_in.is_eligible());
assert!(!opted_out.is_eligible());
}
#[test]
fn a_subscriber_carries_its_method() {
let sink = Arc::new(Counter::default());
let subscriber = watch(
FeatureProfile::empty().with(Feature::ToolsListChanged),
&sink,
);
assert_eq!(subscriber.kind(), PrimitiveKind::Tool);
assert_eq!(subscriber.method(), "notifications/tools/list_changed");
}
#[tokio::test]
async fn an_empty_fan_out_reports_silence() {
let set = SubscriberSet::new();
let report = set.announce(&snapshot(), &[]).await;
assert!(report.is_silent());
assert_eq!(report.sent, 0);
assert_eq!(report.gated, 0);
assert_eq!(report.failed, 0);
}
#[tokio::test]
async fn a_gated_subscriber_is_counted_not_skipped() {
let set = SubscriberSet::new();
let sink = Arc::new(Counter::default());
set.add(watch(FeatureProfile::empty().with(Feature::Tools), &sink));
let report = set.announce(&snapshot(), &[PrimitiveKind::Tool]).await;
assert_eq!(report.gated, 1);
assert_eq!(report.sent, 0);
assert_eq!(sink.0.load(Ordering::Relaxed), 0);
}
#[tokio::test]
async fn an_unwatched_kind_reaches_nobody() {
let set = SubscriberSet::new();
let sink = Arc::new(Counter::default());
set.add(watch(
FeatureProfile::empty().with(Feature::ToolsListChanged),
&sink,
));
let report = set.announce(&snapshot(), &[PrimitiveKind::Resource]).await;
assert!(report.is_silent());
assert_eq!(sink.0.load(Ordering::Relaxed), 0);
}
#[tokio::test]
async fn a_disconnect_removes_exactly_its_own_subscriber() -> Result<(), TestError> {
let set = SubscriberSet::new();
let staying = Arc::new(Counter::default());
let leaving = Arc::new(Counter::default());
let profile = FeatureProfile::empty().with(Feature::ToolsListChanged);
set.add(watch(profile, &staying));
set.add(watch(profile, &leaving));
assert_eq!(set.len(), 2);
let removed = set.remove_by_sink(&(Arc::clone(&leaving) as Arc<dyn DynFrameSink>));
assert_eq!(removed, 1);
assert_eq!(set.len(), 1);
set.announce(&snapshot(), &[PrimitiveKind::Tool]).await;
assert_eq!(staying.0.load(Ordering::Relaxed), 1);
assert_eq!(leaving.0.load(Ordering::Relaxed), 0);
Ok(())
}
#[test]
fn removing_an_unknown_sink_removes_nothing() {
let set = SubscriberSet::new();
let unknown: Arc<dyn DynFrameSink> = Arc::new(Counter::default());
assert_eq!(set.remove_by_sink(&unknown), 0);
assert!(set.is_empty());
}
}