use std::time::{Duration, Instant};
use crate::core::{Error, Result};
#[derive(Debug, Clone, PartialEq)]
pub enum ReplayMode {
FullSpeed,
OriginalTiming,
TimeScaled {
factor: f64,
},
Burst {
count: usize,
gap: Duration,
},
SingleStep,
}
impl ReplayMode {
pub fn validate(&self) -> Result<()> {
match self {
ReplayMode::TimeScaled { factor } if !factor.is_finite() || *factor <= 0.0 => {
Err(Error::config("TimeScaled factor must be finite and > 0"))
}
ReplayMode::Burst { count, .. } if *count == 0 => {
Err(Error::config("Burst count must be ≥ 1"))
}
_ => Ok(()),
}
}
}
pub struct ReplayEngine {
mode: ReplayMode,
start: Option<Instant>,
first_ts_ns: Option<u64>,
burst_count: usize,
burst_resume: Option<Instant>,
single_step_armed: bool,
}
impl ReplayEngine {
pub fn new(mode: ReplayMode) -> Result<Self> {
mode.validate()?;
Ok(Self {
mode,
start: None,
first_ts_ns: None,
burst_count: 0,
burst_resume: None,
single_step_armed: true,
})
}
pub fn new_unchecked(mode: ReplayMode) -> Self {
Self {
mode,
start: None,
first_ts_ns: None,
burst_count: 0,
burst_resume: None,
single_step_armed: true,
}
}
pub fn advance(&mut self) {
if matches!(self.mode, ReplayMode::SingleStep) {
self.single_step_armed = true;
}
}
pub fn poll(&mut self, timestamp_ns: u64) -> bool {
let now = Instant::now();
match &self.mode {
ReplayMode::FullSpeed => true,
ReplayMode::SingleStep => {
if !self.single_step_armed {
return false;
}
self.single_step_armed = false;
true
}
ReplayMode::OriginalTiming => self.original_timing_ready(now, timestamp_ns, 1.0),
ReplayMode::TimeScaled { factor } => {
let f = *factor;
self.original_timing_ready(now, timestamp_ns, f)
}
ReplayMode::Burst { count, gap } => {
let count = *count;
let gap = *gap;
self.burst_ready(now, count, gap)
}
}
}
fn original_timing_ready(&mut self, now: Instant, ts_ns: u64, factor: f64) -> bool {
let start = *self.start.get_or_insert(now);
let first_ts = *self.first_ts_ns.get_or_insert(ts_ns);
if ts_ns <= first_ts {
return true;
}
let original_delta_ns = ts_ns.saturating_sub(first_ts);
let scaled_ns = if (factor - 1.0).abs() < f64::EPSILON {
original_delta_ns
} else {
let scaled = (original_delta_ns as f64) * factor;
if !scaled.is_finite() || scaled < 0.0 {
return true;
}
if scaled >= u64::MAX as f64 {
u64::MAX
} else {
scaled as u64
}
};
let deadline = start + Duration::from_nanos(scaled_ns);
now >= deadline
}
fn burst_ready(&mut self, now: Instant, burst_count: usize, gap: Duration) -> bool {
if let Some(resume) = self.burst_resume {
if now < resume {
return false;
}
self.burst_resume = None;
self.burst_count = 0;
}
self.burst_count += 1;
if self.burst_count >= burst_count {
self.burst_resume = Some(now + gap);
self.burst_count = 0;
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn full_speed_always_ready() {
let mut engine = ReplayEngine::new(ReplayMode::FullSpeed).unwrap();
for ts in [0, 1000, 2000, 999_999_999] {
assert!(engine.poll(ts), "full-speed should always return true");
}
}
#[test]
fn single_step_requires_advance() {
let mut engine = ReplayEngine::new(ReplayMode::SingleStep).unwrap();
assert!(engine.poll(0));
assert!(!engine.poll(0), "second poll blocks until advance");
engine.advance();
assert!(engine.poll(0));
}
#[test]
fn original_timing_first_record_immediate() {
let mut engine = ReplayEngine::new(ReplayMode::OriginalTiming).unwrap();
assert!(engine.poll(1_000_000_000));
}
#[test]
fn time_scaled_rejects_bad_factor() {
assert!(ReplayEngine::new(ReplayMode::TimeScaled { factor: 0.0 }).is_err());
assert!(ReplayEngine::new(ReplayMode::TimeScaled { factor: f64::NAN }).is_err());
}
#[test]
fn time_scaled_first_record_immediate() {
let mut engine = ReplayEngine::new(ReplayMode::TimeScaled { factor: 2.0 }).unwrap();
assert!(engine.poll(500_000_000));
}
#[test]
fn burst_emits_then_pauses() {
let mut engine = ReplayEngine::new(ReplayMode::Burst {
count: 2,
gap: Duration::from_secs(3600),
})
.unwrap();
assert!(engine.poll(0));
assert!(engine.poll(0));
assert!(!engine.poll(0));
}
#[test]
fn burst_resumes_after_gap() {
let gap = Duration::from_millis(0);
let mut engine = ReplayEngine::new(ReplayMode::Burst { count: 1, gap }).unwrap();
assert!(engine.poll(0));
assert!(engine.poll(0));
}
}