openlatch-provider 0.0.0

Self-service onboarding CLI + runtime daemon for OpenLatch Editors and Providers
//! Per-event hard deadline propagation.
//!
//! Every inbound request carries an `X-OpenLatch-Deadline-Ms` header — the
//! platform's hard kill-switch for the event. The runtime arms a wall-clock
//! deadline at the moment of receipt, threads a [`Deadline`] through the
//! pipeline, and uses [`Deadline::remaining`] to bound the localhost proxy
//! call. If we run out of budget while waiting on the tool, we surface
//! `OL-4228` rather than letting the platform time us out — it's cheaper to
//! drop the call early than to keep the worker pinned.

use std::time::{Duration, Instant};

use crate::error::{OlError, OL_4228_DEADLINE_EXCEEDED};

/// A hard deadline relative to a fixed start instant.
#[derive(Debug, Clone, Copy)]
pub struct Deadline {
    started: Instant,
    budget: Duration,
}

impl Deadline {
    pub fn from_budget_ms(budget_ms: u64) -> Self {
        Self {
            started: Instant::now(),
            budget: Duration::from_millis(budget_ms),
        }
    }

    pub fn from_started(started: Instant, budget: Duration) -> Self {
        Self { started, budget }
    }

    /// Remaining wall-clock budget. Returns `Err(OL-4228)` when expired.
    pub fn remaining(&self) -> Result<Duration, OlError> {
        let elapsed = self.started.elapsed();
        if elapsed >= self.budget {
            Err(OlError::new(
                OL_4228_DEADLINE_EXCEEDED,
                format!(
                    "deadline exceeded: budget {} ms, elapsed {} ms",
                    self.budget.as_millis(),
                    elapsed.as_millis()
                ),
            ))
        } else {
            Ok(self.budget - elapsed)
        }
    }

    pub fn budget(&self) -> Duration {
        self.budget
    }

    pub fn elapsed(&self) -> Duration {
        self.started.elapsed()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fresh_deadline_has_full_budget_remaining() {
        let d = Deadline::from_budget_ms(200);
        let r = d.remaining().unwrap();
        // Allow a tiny slack for the `Instant::now()` call between construct
        // and read; budget is well above any reasonable jitter here.
        assert!(r > Duration::from_millis(150));
    }

    #[test]
    fn expired_deadline_returns_4228() {
        let d = Deadline::from_started(
            Instant::now() - Duration::from_secs(1),
            Duration::from_millis(100),
        );
        let err = d.remaining().unwrap_err();
        assert_eq!(err.code.code, "OL-4228");
    }

    #[test]
    fn zero_budget_is_immediately_expired() {
        let d = Deadline::from_budget_ms(0);
        assert!(d.remaining().is_err());
    }
}