use crate::cancellation::AgentCancellation;
use crossbeam_channel::{Sender, bounded};
use std::{
sync::Mutex,
time::{Duration, Instant},
};
pub(super) const APPROVAL_TIMEOUT: Duration = Duration::from_secs(60);
struct PendingApproval {
id: String,
deadline: Instant,
reply: Sender<bool>,
}
#[derive(Default)]
pub(super) struct Approvals {
pending: Mutex<Option<PendingApproval>>,
}
impl Approvals {
pub(super) fn answer(
&self,
id: &str,
allow: bool,
cancellation: &AgentCancellation,
) -> Result<(), &'static str> {
let mut pending = self.pending.lock().unwrap_or_else(|e| e.into_inner());
let approval = pending.as_ref().ok_or("stale_approval")?;
if approval.id != id || Instant::now() >= approval.deadline || cancellation.is_canceled() {
return Err("stale_approval");
}
pending
.take()
.expect("checked pending approval")
.reply
.try_send(allow)
.map_err(|_| "stale_approval")
}
pub(super) fn request(
&self,
cancellation: &AgentCancellation,
emit: impl FnOnce(&str) -> anyhow::Result<()>,
) -> anyhow::Result<bool> {
self.wait(cancellation, APPROVAL_TIMEOUT, emit)
}
fn wait(
&self,
cancellation: &AgentCancellation,
timeout: Duration,
emit: impl FnOnce(&str) -> anyhow::Result<()>,
) -> anyhow::Result<bool> {
let id = uuid::Uuid::new_v4().to_string();
let deadline = Instant::now() + timeout;
let (reply, receiver) = bounded(1);
{
let mut pending = self.pending.lock().unwrap_or_else(|e| e.into_inner());
if pending.is_some() || cancellation.is_canceled() {
return Ok(false);
}
*pending = Some(PendingApproval {
id: id.clone(),
deadline,
reply,
});
}
let result = (|| {
emit(&id)?;
loop {
if cancellation.is_canceled() || Instant::now() >= deadline {
return Ok(false);
}
match receiver.recv_timeout(
Duration::from_millis(20)
.min(deadline.saturating_duration_since(Instant::now())),
) {
Ok(allow) => {
return Ok(allow
&& !cancellation.is_canceled()
&& Instant::now() < deadline);
}
Err(crossbeam_channel::RecvTimeoutError::Disconnected) => return Ok(false),
Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
}
}
})();
let mut pending = self.pending.lock().unwrap_or_else(|e| e.into_inner());
if pending.as_ref().is_some_and(|p| p.id == id) {
pending.take();
}
result
}
}