use crate::cancel::CancelFlag;
use crate::error::{ProviderError, Result};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
static NEXT_REQUEST_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone)]
pub struct OpProgress {
pub stage: &'static str,
pub detail: String,
}
pub type ProgressSink = Arc<dyn Fn(OpProgress) + Send + Sync>;
#[derive(Clone)]
pub struct OpContext {
pub request_id: u64,
pub cancel: CancelFlag,
deadline: Option<Instant>,
pub progress: Option<ProgressSink>,
}
impl std::fmt::Debug for OpContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OpContext")
.field("request_id", &self.request_id)
.field("cancelled", &self.cancel.is_cancelled())
.field("has_deadline", &self.deadline.is_some())
.finish()
}
}
impl Default for OpContext {
fn default() -> Self {
Self::new()
}
}
impl OpContext {
pub fn new() -> Self {
Self {
request_id: NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed),
cancel: CancelFlag::new(),
deadline: None,
progress: None,
}
}
pub fn with_cancel(cancel: CancelFlag) -> Self {
Self {
request_id: NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed),
cancel,
deadline: None,
progress: None,
}
}
pub fn from_optional_cancel(cancel: Option<CancelFlag>) -> Self {
match cancel {
Some(c) => Self::with_cancel(c),
None => Self::new(),
}
}
pub fn with_deadline_from_now(mut self, timeout: Duration) -> Self {
if !timeout.is_zero() {
self.deadline = Some(Instant::now() + timeout);
}
self
}
pub fn with_absolute_deadline(mut self, at: Instant) -> Self {
self.deadline = Some(at);
self
}
pub fn with_progress(mut self, sink: ProgressSink) -> Self {
self.progress = Some(sink);
self
}
pub fn deadline(&self) -> Option<Instant> {
self.deadline
}
pub fn remaining(&self) -> Option<Duration> {
self.deadline
.map(|d| d.saturating_duration_since(Instant::now()))
}
pub fn check(&self) -> Result<()> {
if self.cancel.is_cancelled() {
return Err(ProviderError::Cancelled.into());
}
if let Some(d) = self.deadline {
if Instant::now() >= d {
return Err(ProviderError::DeadlineExceeded.into());
}
}
Ok(())
}
pub fn emit(&self, stage: &'static str, detail: impl Into<String>) {
if let Some(sink) = &self.progress {
sink(OpProgress {
stage,
detail: detail.into(),
});
}
}
pub fn timeout_or(&self, fallback: Duration) -> Duration {
self.remaining().unwrap_or(fallback)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cancel_beats_deadline() {
let ctx = OpContext::new().with_deadline_from_now(Duration::from_secs(60));
ctx.cancel.cancel();
let err = ctx.check().unwrap_err();
assert!(matches!(
err,
crate::error::TranscriptionError::Provider(ProviderError::Cancelled)
));
}
#[test]
fn deadline_fires() {
let ctx = OpContext::new().with_deadline_from_now(Duration::from_millis(1));
std::thread::sleep(Duration::from_millis(5));
let err = ctx.check().unwrap_err();
assert!(matches!(
err,
crate::error::TranscriptionError::Provider(ProviderError::DeadlineExceeded)
));
}
#[test]
fn unique_request_ids() {
let a = OpContext::new();
let b = OpContext::new();
assert_ne!(a.request_id, b.request_id);
}
}