use std::sync::Arc;
use bacnet_transport::port::TransportPort;
use rmcp::model::ResourceUpdatedNotificationParam;
use rmcp::service::Peer;
use rmcp::{ErrorData, RoleServer};
use crate::context::Ctx;
use crate::errors::ToolError;
use crate::follow::{Follow, Wake};
use crate::listen::SHUTTING_DOWN;
use crate::resources::uri::{self, Uri};
pub(crate) fn max_uris<T: TransportPort + 'static>(ctx: &Ctx<T>) -> usize {
ctx.policy.caps.watches.saturating_mul(2)
}
fn refusal(error: &ToolError) -> ErrorData {
ErrorData::invalid_params(error.summary(), Some(error.json()))
}
fn over_cap(reason: &'static str) -> ToolError {
let message = match reason {
"process-cap" => "the server watches as many points as it may",
SHUTTING_DOWN => "the server is shutting down",
"passive" => "the server is passive: it builds no client and sends nothing",
_ => "this principal watches as many points as it may; unsubscribe from some first",
};
ToolError::refused(reason, message)
}
pub(crate) fn subscribe<T: TransportPort + 'static>(
ctx: &Ctx<T>,
text: &str,
) -> Result<(), ErrorData> {
let parsed = uri::parse(text).map_err(|why| refusal(&ToolError::invalid("uri", why)))?;
if ctx.subs.legacy_has(text) {
return Ok(());
}
if ctx.hub.is_none() && !matches!(parsed, Uri::Session | Uri::Ledger) {
return Err(refusal(&over_cap("passive")));
}
if ctx.subs.uris() >= max_uris(ctx) {
return Err(refusal(&too_many(ctx)));
}
let hold = match parsed {
Uri::Point(point) => {
ctx.reach_check(point.device).map_err(|e| refusal(&e))?;
let hold = ctx
.subs
.hold(ctx, point)
.map_err(|reason| refusal(&over_cap(reason)))?;
Some(hold)
}
Uri::DeviceAlarms(device) => {
ctx.reach_check(device).map_err(|e| refusal(&e))?;
None
}
Uri::Devices | Uri::DevicesAfter(_) | Uri::Alarms | Uri::Session | Uri::Ledger => None,
Uri::Device(_) => {
return Err(refusal(&ToolError::invalid(
"uri",
"a device summary has no updates; subscribe to its points or its alarms",
)));
}
};
ctx.subs
.legacy_add(text, hold, max_uris(ctx))
.map_err(|reason| match reason {
SHUTTING_DOWN => refusal(&over_cap(reason)),
_ => refusal(&too_many(ctx)),
})
}
fn too_many<T: TransportPort + 'static>(ctx: &Ctx<T>) -> ToolError {
ToolError::refused(
"principal-cap",
format!(
"this session subscribes to {} resources, its listen streams' device alarms \
included, as many as it may; unsubscribe from some first",
max_uris(ctx)
),
)
}
pub(crate) fn unsubscribe<T: TransportPort + 'static>(ctx: &Ctx<T>, text: &str) {
ctx.subs.legacy_remove(text);
}
pub(crate) async fn notify<T: TransportPort + 'static>(ctx: Arc<Ctx<T>>, peer: Peer<RoleServer>) {
let mut changed = ctx.subs.legacy_watch();
let mut follow = Follow::new();
follow.set(ctx.engine.as_deref(), ctx.subs.legacy_snapshot());
loop {
let wake = tokio::select! {
biased;
() = ctx.closing.cancelled() => return,
seen = changed.changed() => {
if seen.is_err() {
return;
}
follow.set(ctx.engine.as_deref(), ctx.subs.legacy_snapshot());
continue;
}
wake = follow.next() => wake,
};
match wake {
Wake::Updated(name) => {
if send(&peer, name).await.is_err() {
return;
}
}
Wake::Devices => {
for name in follow.device_uris() {
if send(&peer, name.clone()).await.is_err() {
return;
}
}
}
Wake::Alarms => {
for name in follow.alarm_uris() {
if crate::resources::refresh_alarms(&ctx, name).await
&& send(&peer, name.clone()).await.is_err()
{
return;
}
}
}
}
}
}
async fn send(peer: &Peer<RoleServer>, uri: String) -> Result<(), rmcp::service::ServiceError> {
peer.notify_resource_updated(ResourceUpdatedNotificationParam::new(uri))
.await
}