use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use serde_json::{json, Value};
#[derive(Clone, Default)]
pub struct ProgressReporter {
token: Option<Value>,
tenant: String,
agent_id: i64,
}
impl ProgressReporter {
#[must_use]
pub fn disabled() -> Self {
Self::default()
}
pub(crate) fn for_agent(token: Option<Value>, tenant: String, agent_id: i64) -> Self {
Self {
token,
tenant,
agent_id,
}
}
#[must_use]
pub fn is_active(&self) -> bool {
self.token.is_some()
}
pub fn report(&self, progress: f64, total: Option<f64>, message: Option<&str>) {
let Some(token) = &self.token else { return };
let mut params = json!({ "progressToken": token, "progress": progress });
if let Some(t) = total {
params["total"] = json!(t);
}
if let Some(m) = message {
params["message"] = json!(m);
}
let body = json!({
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": params,
})
.to_string();
super::notifications::bus().send(super::notifications::ScopedFrame {
tenant: self.tenant.clone(),
agent_id: Some(self.agent_id),
body,
});
}
}
#[derive(Clone)]
pub struct CancelToken {
flag: Arc<AtomicBool>,
}
impl Default for CancelToken {
fn default() -> Self {
Self::never()
}
}
impl CancelToken {
#[must_use]
pub fn never() -> Self {
Self {
flag: Arc::new(AtomicBool::new(false)),
}
}
#[must_use]
pub fn cancelled() -> Self {
let t = Self::never();
t.trip();
t
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.flag.load(Ordering::SeqCst)
}
fn trip(&self) {
self.flag.store(true, Ordering::SeqCst);
}
}
type CancelKey = (String, i64, String);
fn registry() -> &'static Mutex<HashMap<CancelKey, CancelToken>> {
static R: OnceLock<Mutex<HashMap<CancelKey, CancelToken>>> = OnceLock::new();
R.get_or_init(|| Mutex::new(HashMap::new()))
}
fn registry_lock() -> std::sync::MutexGuard<'static, HashMap<CancelKey, CancelToken>> {
registry().lock().unwrap_or_else(|e| e.into_inner())
}
pub(crate) struct CancelGuard {
key: CancelKey,
token: CancelToken,
}
impl CancelGuard {
pub(crate) fn register(tenant: &str, agent_id: i64, request_id: &str) -> Self {
let key = (tenant.to_owned(), agent_id, request_id.to_owned());
let token = CancelToken::never();
registry_lock().insert(key.clone(), token.clone());
Self { key, token }
}
pub(crate) fn token(&self) -> CancelToken {
self.token.clone()
}
}
impl Drop for CancelGuard {
fn drop(&mut self) {
registry_lock().remove(&self.key);
}
}
pub fn cancel(tenant: &str, agent_id: i64, request_id: &str) {
let key = (tenant.to_owned(), agent_id, request_id.to_owned());
if let Some(token) = registry_lock().get(&key) {
token.trip();
}
}
pub(crate) fn progress_token(params: &Value) -> Option<Value> {
params
.get("_meta")
.and_then(|m| m.get("progressToken"))
.cloned()
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn progress_emits_when_token_present_and_silent_otherwise() {
let mut rx = super::super::notifications::bus().subscribe();
ProgressReporter::disabled().report(0.5, Some(1.0), Some("half"));
ProgressReporter::for_agent(Some(json!("p1-unit")), "acme".into(), 5).report(
0.5,
Some(1.0),
Some("half"),
);
for _ in 0..200 {
let Ok(frame) = rx.recv().await else { continue };
let v: Value = serde_json::from_str(&frame.body).unwrap();
if v["method"] == "notifications/progress" && v["params"]["progressToken"] == "p1-unit"
{
assert_eq!(frame.tenant, "acme");
assert_eq!(frame.agent_id, Some(5));
assert_eq!(v["params"]["progress"], 0.5);
return;
}
}
panic!("did not observe the progress notification");
}
#[test]
fn cancel_is_agent_scoped_and_guard_deregisters_on_drop() {
let guard = CancelGuard::register("acme", 1, "req-42");
let token = guard.token();
assert!(!token.is_cancelled());
cancel("acme", 2, "req-42");
cancel("evil", 1, "req-42");
assert!(!token.is_cancelled());
cancel("acme", 1, "req-42");
assert!(token.is_cancelled());
drop(guard);
cancel("acme", 1, "req-42");
}
}