use std::sync::OnceLock;
use serde_json::json;
use crate::sse::EventBus;
#[derive(Clone, Debug)]
pub struct ScopedFrame {
pub tenant: String,
pub agent_id: Option<i64>,
pub body: String,
}
#[must_use]
pub fn bus() -> &'static EventBus<ScopedFrame> {
static BUS: OnceLock<EventBus<ScopedFrame>> = OnceLock::new();
BUS.get_or_init(|| EventBus::new(256))
}
#[must_use]
pub fn frame_visible(frame: &ScopedFrame, tenant: &str, agent_id: i64) -> bool {
frame.tenant == tenant && frame.agent_id.map_or(true, |a| a == agent_id)
}
fn notify(tenant: &str, agent_id: Option<i64>, method: &str) {
let body = json!({ "jsonrpc": "2.0", "method": method }).to_string();
bus().send(ScopedFrame {
tenant: tenant.to_owned(),
agent_id,
body,
});
}
pub fn notify_tools_list_changed(tenant: &str, agent_id: Option<i64>) {
notify(tenant, agent_id, "notifications/tools/list_changed");
}
pub fn notify_prompts_list_changed(tenant: &str, agent_id: Option<i64>) {
notify(tenant, agent_id, "notifications/prompts/list_changed");
}
pub fn notify_resources_list_changed(tenant: &str, agent_id: Option<i64>) {
notify(tenant, agent_id, "notifications/resources/list_changed");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn visibility_is_tenant_and_agent_scoped() {
let per_agent = ScopedFrame {
tenant: "acme".into(),
agent_id: Some(1),
body: String::new(),
};
assert!(frame_visible(&per_agent, "acme", 1));
assert!(!frame_visible(&per_agent, "acme", 2)); assert!(!frame_visible(&per_agent, "other", 1));
let tenant_wide = ScopedFrame {
tenant: "acme".into(),
agent_id: None,
body: String::new(),
};
assert!(frame_visible(&tenant_wide, "acme", 1));
assert!(frame_visible(&tenant_wide, "acme", 999)); assert!(!frame_visible(&tenant_wide, "other", 1)); }
#[tokio::test]
async fn notify_emits_a_scoped_jsonrpc_frame() {
let mut rx = bus().subscribe();
notify_prompts_list_changed("acme", Some(7));
for _ in 0..200 {
let Ok(frame) = rx.recv().await else { continue };
if frame.tenant == "acme" && frame.agent_id == Some(7) {
let v: serde_json::Value = serde_json::from_str(&frame.body).unwrap();
assert_eq!(v["method"], "notifications/prompts/list_changed");
assert!(v.get("id").is_none());
return;
}
}
panic!("did not observe the scoped frame");
}
}