use crate::error::{Error, ErrorCode};
use crate::shared::PendingResponse;
use crate::transport::{Sender as _, TransportProtoSender};
use crate::types::{
RequestId, SubscriptionFilter, SubscriptionsListenResult,
notification::{CancelledNotificationParams, Notification},
};
use dashmap::DashMap;
use std::sync::Arc;
use tokio::sync::oneshot;
pub(super) type AckWaiters = Arc<DashMap<RequestId, oneshot::Sender<SubscriptionFilter>>>;
pub(super) type SubscriptionStates = Arc<DashMap<RequestId, SubscriptionState>>;
#[derive(Debug, Clone)]
pub(super) enum SubscriptionState {
Pending(SubscriptionFilter),
Established(SubscriptionFilter),
}
impl SubscriptionState {
pub(super) fn established(&self) -> Option<&SubscriptionFilter> {
match self {
Self::Established(filter) => Some(filter),
Self::Pending(_) => None,
}
}
pub(super) fn acknowledge(&mut self, acknowledged: &SubscriptionFilter) -> bool {
let Self::Pending(requested) = self else {
return false;
};
if !acknowledged.is_subset_of(requested) {
return false;
}
*self = Self::Established(requested.intersection(acknowledged));
true
}
}
#[derive(Debug)]
pub enum SubscriptionEnd {
Graceful(SubscriptionsListenResult),
Abrupt,
Cancelled,
}
pub struct Subscription {
id: RequestId,
requested: SubscriptionFilter,
acknowledged: SubscriptionFilter,
response: oneshot::Receiver<PendingResponse>,
sender: TransportProtoSender,
release: SubscriptionRelease,
cancelled: bool,
settled: bool,
}
impl std::fmt::Debug for Subscription {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Subscription")
.field("id", &self.id)
.field("requested", &self.requested)
.field("acknowledged", &self.acknowledged)
.finish_non_exhaustive()
}
}
impl Subscription {
pub(super) fn new(
id: RequestId,
requested: SubscriptionFilter,
acknowledged: SubscriptionFilter,
response: oneshot::Receiver<PendingResponse>,
sender: TransportProtoSender,
release: SubscriptionRelease,
) -> Self {
Self {
id,
requested,
acknowledged,
response,
sender,
release,
cancelled: false,
settled: false,
}
}
#[inline]
pub fn id(&self) -> &RequestId {
&self.id
}
#[inline]
pub fn requested(&self) -> &SubscriptionFilter {
&self.requested
}
#[inline]
pub fn acknowledged(&self) -> &SubscriptionFilter {
&self.acknowledged
}
#[inline]
pub fn is_fully_honored(&self) -> bool {
self.requested.is_subset_of(&self.acknowledged)
}
pub async fn cancel(&mut self) -> Result<(), Error> {
let sent = self.sender.send(cancelled(&self.id).into()).await;
self.settled = true;
self.cancelled = sent.is_ok();
self.release.release(&self.id);
sent
}
pub async fn closed(mut self) -> SubscriptionEnd {
if self.cancelled {
return SubscriptionEnd::Cancelled;
}
let end = match (&mut self.response).await {
Ok(PendingResponse::Response(resp)) => match resp.into_result() {
Ok(result) if self.closes_this(&result) => SubscriptionEnd::Graceful(result),
_ => SubscriptionEnd::Abrupt,
},
_ => SubscriptionEnd::Abrupt,
};
self.settled = true;
self.release.release(&self.id);
end
}
#[inline]
fn closes_this(&self, result: &SubscriptionsListenResult) -> bool {
let matches = result.meta.subscription_id == self.id;
#[cfg(feature = "tracing")]
if !matches {
tracing::warn!(
logger = "neva",
subscription = %self.id,
closed = %result.meta.subscription_id,
"the closing result names a different subscription"
);
}
matches
}
}
impl Drop for Subscription {
fn drop(&mut self) {
if self.settled {
return;
}
self.release.release(&self.id);
let Ok(runtime) = tokio::runtime::Handle::try_current() else {
return;
};
let mut sender = self.sender.clone();
let notification = cancelled(&self.id);
runtime.spawn(async move {
let _ = sender.send(notification.into()).await;
});
}
}
#[derive(Clone)]
pub(super) struct SubscriptionRelease {
pending: crate::shared::RequestQueue,
ack_waiters: AckWaiters,
filters: SubscriptionStates,
}
impl SubscriptionRelease {
pub(super) fn new(
pending: crate::shared::RequestQueue,
ack_waiters: AckWaiters,
filters: SubscriptionStates,
) -> Self {
Self {
pending,
ack_waiters,
filters,
}
}
pub(super) fn release(&self, id: &RequestId) {
let _ = self.pending.pop(id);
self.ack_waiters.remove(id);
self.filters.remove(id);
}
}
impl std::fmt::Debug for SubscriptionRelease {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SubscriptionRelease")
.finish_non_exhaustive()
}
}
pub(super) struct EstablishmentGuard {
id: RequestId,
release: SubscriptionRelease,
sender: TransportProtoSender,
armed: bool,
}
impl EstablishmentGuard {
pub(super) fn new(
id: RequestId,
release: SubscriptionRelease,
sender: TransportProtoSender,
) -> Self {
Self {
id,
release,
sender,
armed: true,
}
}
pub(super) fn disarm(mut self) {
self.armed = false;
}
pub(super) fn forget(mut self) {
self.armed = false;
self.release.release(&self.id);
}
pub(super) async fn abandon(mut self) {
self.armed = false;
self.release.release(&self.id);
let _ = self.sender.send(cancelled(&self.id).into()).await;
}
}
impl Drop for EstablishmentGuard {
fn drop(&mut self) {
if !self.armed {
return;
}
self.release.release(&self.id);
let Ok(runtime) = tokio::runtime::Handle::try_current() else {
return;
};
let mut sender = self.sender.clone();
let notification = cancelled(&self.id);
runtime.spawn(async move {
let _ = sender.send(notification.into()).await;
});
}
}
impl std::fmt::Debug for EstablishmentGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EstablishmentGuard")
.field("id", &self.id)
.field("armed", &self.armed)
.finish_non_exhaustive()
}
}
pub(super) fn cancelled(id: &RequestId) -> Notification {
let params = CancelledNotificationParams {
request_id: id.clone(),
reason: Some("subscription cancelled by the client".into()),
};
Notification::new(
crate::types::notification::commands::CANCELLED,
serde_json::to_value(params).ok(),
)
}
pub(super) fn parse_ack(
notification: &Notification,
) -> Result<(RequestId, SubscriptionFilter), Error> {
let params = notification
.params
.clone()
.ok_or_else(|| Error::new(ErrorCode::InvalidParams, "Acknowledgment has no params"))?;
serde_json::from_value::<crate::types::SubscriptionsAcknowledgedNotificationParams>(params)
.map(|p| (p.meta.subscription_id, p.notifications))
.map_err(|e| Error::new(ErrorCode::InvalidParams, e))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Response, SUBSCRIPTION_ID_KEY};
fn release() -> SubscriptionRelease {
SubscriptionRelease::new(
crate::shared::RequestQueue::new(std::time::Duration::from_secs(5)),
Default::default(),
Default::default(),
)
}
fn ack(id: serde_json::Value, filter: serde_json::Value) -> Notification {
Notification::new(
crate::types::subscription::commands::ACKNOWLEDGED,
Some(serde_json::json!({
"notifications": filter,
"_meta": { SUBSCRIPTION_ID_KEY: id },
})),
)
}
#[test]
fn it_parses_an_acknowledgment() {
let notification = ack(
serde_json::json!(7),
serde_json::json!({ "toolsListChanged": true }),
);
let (id, filter) = parse_ack(¬ification).unwrap();
assert_eq!(id, RequestId::Number(7));
assert!(filter.tools_list_changed);
assert!(!filter.prompts_list_changed);
}
#[test]
fn it_rejects_an_acknowledgment_without_a_subscription_id() {
let notification = Notification::new(
crate::types::subscription::commands::ACKNOWLEDGED,
Some(serde_json::json!({ "notifications": {} })),
);
assert!(parse_ack(¬ification).is_err());
}
#[tokio::test]
async fn it_reports_a_graceful_close() {
let (tx, rx) = oneshot::channel();
let subscription = Subscription::new(
RequestId::Number(1),
SubscriptionFilter::new().with_tools_changed(),
SubscriptionFilter::new().with_tools_changed(),
rx,
TransportProtoSender::None,
release(),
);
let result = serde_json::json!({ "_meta": { SUBSCRIPTION_ID_KEY: 1 } });
tx.send(PendingResponse::Response(Response::success(
RequestId::Number(1),
result,
)))
.unwrap();
let end = subscription.closed().await;
assert!(matches!(end, SubscriptionEnd::Graceful(r)
if r.meta.subscription_id == RequestId::Number(1)));
}
#[tokio::test]
async fn it_refuses_a_closing_result_for_another_subscription() {
let (tx, rx) = oneshot::channel();
let subscription = Subscription::new(
RequestId::Number(1),
SubscriptionFilter::new().with_tools_changed(),
SubscriptionFilter::new().with_tools_changed(),
rx,
TransportProtoSender::None,
release(),
);
let result = serde_json::json!({ "_meta": { SUBSCRIPTION_ID_KEY: 2 } });
tx.send(PendingResponse::Response(Response::success(
RequestId::Number(1),
result,
)))
.unwrap();
assert!(matches!(
subscription.closed().await,
SubscriptionEnd::Abrupt
));
}
#[tokio::test]
async fn it_reports_an_abrupt_close_when_the_stream_drops() {
let (tx, rx) = oneshot::channel();
let subscription = Subscription::new(
RequestId::Number(1),
SubscriptionFilter::new(),
SubscriptionFilter::new(),
rx,
TransportProtoSender::None,
release(),
);
drop(tx);
assert!(matches!(
subscription.closed().await,
SubscriptionEnd::Abrupt
));
}
#[test]
fn it_reports_a_narrowed_acknowledgment() {
let (_tx, rx) = oneshot::channel();
let subscription = Subscription::new(
RequestId::Number(1),
SubscriptionFilter::new()
.with_tools_changed()
.with_prompts_changed(),
SubscriptionFilter::new().with_tools_changed(),
rx,
TransportProtoSender::None,
release(),
);
assert!(!subscription.is_fully_honored());
}
}