use crate::TokenEvent;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::UnboundedSender;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayRecord {
pub timestamp_ms: u64,
pub event: TokenEvent,
}
pub struct Recorder {
records: Vec<ReplayRecord>,
}
impl Recorder {
pub fn new() -> Self {
Recorder {
records: Vec::new(),
}
}
pub fn record(&mut self, event: &TokenEvent) {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
self.records.push(ReplayRecord {
timestamp_ms,
event: event.clone(),
});
}
pub fn save(&self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_string_pretty(&self.records)?;
std::fs::write(path, json)?;
Ok(())
}
}
impl Default for Recorder {
fn default() -> Self {
Self::new()
}
}
pub struct Replayer;
impl Replayer {
pub fn load(path: &str) -> Result<Vec<ReplayRecord>, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
let records: Vec<ReplayRecord> = serde_json::from_str(&content)?;
Ok(records)
}
pub fn replay_to_channel(
records: Vec<ReplayRecord>,
tx: UnboundedSender<TokenEvent>,
) -> Result<(), Box<dyn std::error::Error>> {
for record in records {
tx.send(record.event)
.map_err(|e| format!("send error: {}", e))?;
}
Ok(())
}
pub async fn replay_to_channel_timed(
records: Vec<ReplayRecord>,
tx: UnboundedSender<TokenEvent>,
speed: f64,
) -> Result<(), Box<dyn std::error::Error>> {
let start_wall = std::time::Instant::now();
let first_ts = records.first().map(|r| r.timestamp_ms).unwrap_or(0);
for record in records {
if speed > 0.0 {
let offset_ms = record.timestamp_ms.saturating_sub(first_ts);
let target_wall_ms = (offset_ms as f64 / speed) as u64;
let elapsed_ms = start_wall.elapsed().as_millis() as u64;
if target_wall_ms > elapsed_ms {
tokio::time::sleep(std::time::Duration::from_millis(
target_wall_ms - elapsed_ms,
))
.await;
}
}
tx.send(record.event)
.map_err(|e| format!("send error: {}", e))?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::TokenEvent;
fn make_event(idx: usize) -> TokenEvent {
TokenEvent {
text: "hello".to_string(),
original: "hello".to_string(),
index: idx,
transformed: false,
importance: 0.0,
chaos_label: None,
provider: None,
confidence: Some(0.9),
perplexity: None,
alternatives: vec![],
is_error: false,
arrival_ms: None,
}
}
#[test]
fn test_recorder_save_load() {
let mut rec = Recorder::new();
rec.record(&make_event(0));
rec.record(&make_event(1));
let tmp = std::env::temp_dir().join("replay_test.json");
rec.save(tmp.to_str().unwrap()).expect("save");
let records = Replayer::load(tmp.to_str().unwrap()).expect("load");
assert_eq!(records.len(), 2);
assert_eq!(records[0].event.index, 0);
assert_eq!(records[1].event.index, 1);
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_replay_to_channel() {
let event = make_event(0);
let records = vec![ReplayRecord {
timestamp_ms: 12345,
event: event.clone(),
}];
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
Replayer::replay_to_channel(records, tx).expect("replay");
let received = rx.try_recv().expect("recv");
assert_eq!(received.index, 0);
}
#[test]
fn test_recorder_timestamps_non_decreasing() {
let mut rec = Recorder::new();
rec.record(&make_event(0));
rec.record(&make_event(1));
rec.record(&make_event(2));
let times: Vec<u64> = rec.records.iter().map(|r| r.timestamp_ms).collect();
for i in 1..times.len() {
assert!(
times[i] >= times[i - 1],
"timestamps must be non-decreasing: {} < {}",
times[i],
times[i - 1]
);
}
}
#[test]
fn test_replayer_load_empty_array() {
let tmp = std::env::temp_dir().join("replay_empty.json");
std::fs::write(&tmp, "[]").expect("write");
let records = Replayer::load(tmp.to_str().unwrap()).expect("load empty");
assert!(records.is_empty());
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_replayer_load_invalid_json_returns_err() {
let tmp = std::env::temp_dir().join("replay_bad.json");
std::fs::write(&tmp, "not json at all").expect("write");
assert!(Replayer::load(tmp.to_str().unwrap()).is_err());
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_replay_to_channel_preserves_order() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let records: Vec<ReplayRecord> = (0..5)
.map(|i| ReplayRecord {
timestamp_ms: i as u64 * 100,
event: make_event(i),
})
.collect();
Replayer::replay_to_channel(records, tx).expect("replay");
for expected_idx in 0..5 {
let ev = rx.try_recv().expect("recv");
assert_eq!(ev.index, expected_idx);
}
}
#[tokio::test]
async fn test_replay_to_channel_timed_instant_speed() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let records: Vec<ReplayRecord> = (0..3)
.map(|i| ReplayRecord {
timestamp_ms: i as u64 * 1_000, event: make_event(i),
})
.collect();
let start = std::time::Instant::now();
Replayer::replay_to_channel_timed(records, tx, 0.0)
.await
.expect("timed replay");
assert!(start.elapsed().as_millis() < 100);
for expected in 0..3 {
let ev = rx.try_recv().expect("recv");
assert_eq!(ev.index, expected);
}
}
}