use crate::state_table::{Action, EventTemplate, EventType, Guard};
use crate::types::CallState;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryConfig {
pub max_transitions: usize,
pub enabled: bool,
pub track_actions: bool,
pub track_guards: bool,
}
impl Default for HistoryConfig {
fn default() -> Self {
Self {
max_transitions: 50,
enabled: true,
track_actions: true,
track_guards: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransitionRecord {
#[serde(skip, default = "Instant::now")]
pub timestamp: Instant,
pub timestamp_ms: u64,
pub sequence: u64,
pub from_state: CallState,
pub event: EventType,
pub to_state: Option<CallState>,
pub guards_evaluated: Vec<GuardResult>,
pub actions_executed: Vec<ActionRecord>,
pub events_published: Vec<EventTemplate>,
pub duration_ms: u64,
pub errors: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardResult {
pub guard: Guard,
pub passed: bool,
pub evaluation_time_us: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionRecord {
pub action: Action,
pub success: bool,
pub execution_time_us: u64,
pub error: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SessionHistory {
transitions: VecDeque<TransitionRecord>,
config: HistoryConfig,
next_sequence: u64,
pub total_transitions: u64,
pub total_errors: u64,
pub session_created: Instant,
pub last_activity: Instant,
}
impl SessionHistory {
pub fn new(config: HistoryConfig) -> Self {
Self {
transitions: VecDeque::with_capacity(config.max_transitions),
config,
next_sequence: 0,
total_transitions: 0,
total_errors: 0,
session_created: Instant::now(),
last_activity: Instant::now(),
}
}
pub fn record_transition(&mut self, mut record: TransitionRecord) {
if !self.config.enabled {
return;
}
if !self.config.track_guards {
record.guards_evaluated.clear();
}
if !self.config.track_actions {
record.actions_executed.clear();
}
self.total_transitions += 1;
if !record.errors.is_empty() {
self.total_errors += 1;
}
self.last_activity = Instant::now();
record.sequence = self.next_sequence;
self.next_sequence += 1;
if self.transitions.len() >= self.config.max_transitions {
self.transitions.pop_front();
}
self.transitions.push_back(record);
}
pub fn get_recent(&self, count: usize) -> Vec<TransitionRecord> {
self.transitions.iter().rev().take(count).cloned().collect()
}
pub fn get_by_state(&self, state: CallState) -> Vec<TransitionRecord> {
self.transitions
.iter()
.filter(|t| t.from_state == state || t.to_state == Some(state))
.cloned()
.collect()
}
pub fn get_errors(&self) -> Vec<TransitionRecord> {
self.transitions
.iter()
.filter(|t| !t.errors.is_empty())
.cloned()
.collect()
}
pub fn count_by_event(&self, event_type: &EventType) -> usize {
self.transitions
.iter()
.filter(|t| std::mem::discriminant(&t.event) == std::mem::discriminant(event_type))
.count()
}
pub fn average_duration_ms(&self) -> f64 {
if self.transitions.is_empty() {
return 0.0;
}
let total: u64 = self.transitions.iter().map(|t| t.duration_ms).sum();
total as f64 / self.transitions.len() as f64
}
pub fn clear(&mut self) {
self.transitions.clear();
self.next_sequence = 0;
self.total_transitions = 0;
self.total_errors = 0;
self.last_activity = Instant::now();
}
pub fn export_json(&self) -> String {
serde_json::to_string_pretty(&self.transitions).unwrap_or_else(|_| "[]".to_string())
}
pub fn export_csv(&self) -> String {
let mut csv =
String::from("sequence,timestamp_ms,from_state,event,to_state,duration_ms,errors\n");
for t in &self.transitions {
csv.push_str(&format!(
"{},{},{:?},\"{:?}\",{:?},{},{}\n",
t.sequence,
t.timestamp.elapsed().as_millis(),
t.from_state,
t.event,
t.to_state
.map(|s| format!("{:?}", s))
.unwrap_or_else(|| "None".to_string()),
t.duration_ms,
t.errors.join(";")
));
}
csv
}
pub fn session_age(&self) -> Duration {
self.session_created.elapsed()
}
pub fn idle_time(&self) -> Duration {
self.last_activity.elapsed()
}
pub fn is_idle(&self, threshold: Duration) -> bool {
self.idle_time() > threshold
}
pub fn error_rate(&self) -> f32 {
if self.total_transitions == 0 {
return 0.0;
}
self.total_errors as f32 / self.total_transitions as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn test_ring_buffer_limit() {
let config = HistoryConfig {
max_transitions: 3,
enabled: true,
..Default::default()
};
let mut history = SessionHistory::new(config);
for i in 0..5 {
let now = Instant::now();
let record = TransitionRecord {
timestamp: now,
timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64,
sequence: 0,
from_state: CallState::Idle,
event: EventType::MakeCall {
target: format!("test{}", i),
},
to_state: Some(CallState::Initiating),
guards_evaluated: vec![],
actions_executed: vec![],
events_published: vec![],
duration_ms: 10,
errors: vec![],
};
history.record_transition(record);
}
assert_eq!(history.transitions.len(), 3);
assert_eq!(history.total_transitions, 5);
let recent = history.get_recent(10);
assert_eq!(recent.len(), 3);
assert_eq!(recent[0].sequence, 4);
assert_eq!(recent[1].sequence, 3);
assert_eq!(recent[2].sequence, 2);
}
#[test]
fn test_error_tracking() {
let mut history = SessionHistory::new(HistoryConfig::default());
for i in 0..5 {
let now = Instant::now();
let mut record = TransitionRecord {
timestamp: now,
timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64,
sequence: 0,
from_state: CallState::Active,
event: EventType::HangupCall,
to_state: Some(CallState::Terminating),
guards_evaluated: vec![],
actions_executed: vec![],
events_published: vec![],
duration_ms: 10,
errors: vec![],
};
if i % 2 == 0 {
record.errors.push(format!("Error {}", i));
}
history.record_transition(record);
}
assert_eq!(history.total_transitions, 5);
assert_eq!(history.total_errors, 3);
assert_eq!(history.get_errors().len(), 3);
assert_eq!(history.error_rate(), 0.6);
}
#[test]
fn test_state_filtering() {
let mut history = SessionHistory::new(HistoryConfig::default());
let states = vec![
(CallState::Idle, CallState::Initiating),
(CallState::Initiating, CallState::Ringing),
(CallState::Ringing, CallState::Active),
(CallState::Active, CallState::OnHold),
(CallState::OnHold, CallState::Active),
];
for (from, to) in states {
let record = TransitionRecord {
timestamp: Instant::now(),
timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64,
sequence: 0,
from_state: from,
event: EventType::MakeCall {
target: "test".to_string(),
},
to_state: Some(to),
guards_evaluated: vec![],
actions_executed: vec![],
events_published: vec![],
duration_ms: 10,
errors: vec![],
};
history.record_transition(record);
}
let active_transitions = history.get_by_state(CallState::Active);
assert_eq!(active_transitions.len(), 3); }
}