use std::collections::HashMap;
use std::sync::Arc;
use futures::StreamExt;
use serde::Deserialize;
use tokio::sync::{Mutex, Notify};
use tokio_util::sync::CancellationToken;
use tracing::{trace, warn};
use zendriver_transport::SessionHandle;
#[derive(Debug)]
pub(crate) struct InFlightTracker {
pub(crate) in_flight: Mutex<HashMap<String, tokio::time::Instant>>,
pub(crate) notifier: Notify,
}
#[derive(Debug, Deserialize)]
struct RequestEvent {
#[serde(rename = "requestId")]
request_id: String,
}
impl InFlightTracker {
pub(crate) fn new() -> Arc<Self> {
Arc::new(Self {
in_flight: Mutex::new(HashMap::new()),
notifier: Notify::new(),
})
}
pub(crate) async fn run(self: Arc<Self>, session: SessionHandle, cancel: CancellationToken) {
let session_id = session.session_id().to_string();
let mut events = session.connection().subscribe_raw();
let enable_session = session.clone();
tokio::spawn(async move {
if let Err(e) = enable_session
.call("Network.enable", serde_json::json!({}))
.await
{
warn!(error = %e, "InFlightTracker: Network.enable failed; events may be inactive");
}
});
loop {
tokio::select! {
() = cancel.cancelled() => {
trace!("InFlightTracker: cancellation received, exiting");
return;
}
next = events.next() => {
let Some(ev) = next else {
trace!("InFlightTracker: event stream closed, exiting");
return;
};
if ev.session_id.as_deref() != Some(session_id.as_str()) {
continue;
}
let changed = match ev.method.as_str() {
"Network.requestWillBeSent" => {
let Ok(parsed) = serde_json::from_value::<RequestEvent>(ev.params) else { continue };
let mut set = self.in_flight.lock().await;
set.insert(parsed.request_id, tokio::time::Instant::now());
true
}
"Network.responseReceived"
| "Network.loadingFailed"
| "Network.loadingFinished" => {
let Ok(parsed) = serde_json::from_value::<RequestEvent>(ev.params) else { continue };
let mut set = self.in_flight.lock().await;
set.remove(&parsed.request_id);
true
}
_ => false,
};
if changed {
self.notifier.notify_waiters();
}
}
}
}
}
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
use serde_json::json;
use zendriver_transport::testing::MockConnection;
#[tokio::test]
async fn round_trip_request_will_be_sent_and_response_received() {
let (mut mock, conn) = MockConnection::pair();
let session = SessionHandle::new(conn.clone(), "S1");
let tracker = InFlightTracker::new();
let cancel = CancellationToken::new();
let task = tokio::spawn({
let t = tracker.clone();
let s = session.clone();
let c = cancel.clone();
async move {
t.run(s, c).await;
}
});
let id = mock.expect_cmd("Network.enable").await;
mock.reply(id, json!({})).await;
mock.emit_event_for_session(
"Network.requestWillBeSent",
json!({ "requestId": "R1" }),
"S1",
)
.await;
for _ in 0..50 {
if tracker.in_flight.lock().await.len() == 1 {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
{
let set = tracker.in_flight.lock().await;
assert_eq!(set.len(), 1, "expected exactly one in-flight request");
assert!(set.contains_key("R1"));
}
mock.emit_event_for_session(
"Network.responseReceived",
json!({ "requestId": "R1" }),
"S1",
)
.await;
for _ in 0..50 {
if tracker.in_flight.lock().await.is_empty() {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
{
let set = tracker.in_flight.lock().await;
assert!(set.is_empty(), "expected in-flight set to drain to empty");
}
cancel.cancel();
task.await.unwrap();
conn.shutdown();
}
}