use std::sync::Arc;
use std::time::Duration;
use crate::host::EngineHost;
const RESUBSCRIBE_DELAY: Duration = Duration::from_millis(500);
pub fn spawn_notification_router(host: Arc<EngineHost>) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let handlers = host.ext_manager().read().await.handlers();
for (ext_id, handler) in handlers {
let host = Arc::clone(&host);
tokio::spawn(async move {
forward_extension_notifications(host, ext_id, handler).await;
});
}
})
}
pub async fn forward_extension_notifications(
host: Arc<EngineHost>,
ext_id: String,
handler: Arc<dyn super::runtime::ExtensionHandler>,
) {
loop {
let (_sub_id, mut rx) = handler.subscribe_notifications().await;
while let Some(frame) = rx.recv().await {
if !super::widgets::is_widget_method(&frame.method) {
continue;
}
if super::widgets::parse_widget_event(&frame.method, &frame.params).is_err() {
tracing::debug!(extension = %ext_id, method = %frame.method,
"ignoring malformed widget frame");
continue;
}
let delivered = match route_target(&frame.params) {
Some(sid) => {
let id = crate::session::SessionId(sid.to_string());
match host.attach(&id) {
Some(handle) => {
let cmd = crate::session::SessionCommand::HostEvent(
crate::session::HostEvent::ExtensionNotification {
extension_id: ext_id.clone(),
method: frame.method.clone(),
params: frame.params,
},
);
match handle.send(cmd).await {
Ok(()) => 1,
Err(err) => {
tracing::warn!(session = %id, extension = %ext_id,
method = %frame.method, error = %err,
"dropping extension notification for session");
0
}
}
}
None => {
tracing::debug!(session = %id, extension = %ext_id,
method = %frame.method,
"extension notification for a session that is not live; dropped");
0
}
}
}
None => {
host.broadcast_extension_notification(&ext_id, &frame.method, frame.params)
.await
}
};
tracing::trace!(extension = %ext_id, method = %frame.method, sessions = delivered,
"extension notification routed");
}
tokio::time::sleep(RESUBSCRIBE_DELAY).await;
let still_loaded = host
.ext_manager()
.read()
.await
.handlers()
.iter()
.any(|(id, h)| id == &ext_id && Arc::ptr_eq(h, &handler));
if !still_loaded {
tracing::debug!(extension = %ext_id, "notification router: extension unloaded; stopping");
return;
}
}
}
pub fn route_target(params: &serde_json::Value) -> Option<&str> {
params
.get("session_id")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
}
#[cfg(test)]
mod tests {
use super::route_target;
#[test]
fn route_target_is_session_id_string_or_global() {
assert_eq!(
route_target(&serde_json::json!({"id": "w", "session_id": "s-1"})),
Some("s-1")
);
assert_eq!(route_target(&serde_json::json!({"id": "w"})), None);
assert_eq!(route_target(&serde_json::json!({"session_id": ""})), None);
assert_eq!(route_target(&serde_json::json!({"session_id": 7})), None);
assert_eq!(route_target(&serde_json::json!(null)), None);
}
}