use std::time::{Duration, SystemTime};
use serde::{Deserialize, Serialize};
use crate::vortix_core::engine::state::{ConnectionHealth, DegradedReason, FailureReason};
use crate::vortix_core::profile::{ProfileId, ProtocolKind};
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventEnvelope {
pub schema_version: u32,
pub timestamp: SystemTime,
pub event: EngineEvent,
}
impl EventEnvelope {
#[must_use]
pub fn new(event: EngineEvent) -> Self {
Self {
schema_version: SCHEMA_VERSION,
timestamp: SystemTime::now(),
event,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum EngineEvent {
ConnectAttemptStarted {
profile_id: ProfileId,
protocol: ProtocolKind,
attempt: u32,
},
ConnectAttemptFailed {
profile_id: ProfileId,
attempt: u32,
reason: FailureReason,
},
TunnelUp {
profile_id: ProfileId,
protocol: ProtocolKind,
interface_name: String,
pid: Option<u32>,
},
TunnelDown {
profile_id: ProfileId,
reason: TunnelDownReason,
},
HandshakeStale {
profile_id: ProfileId,
seconds_since_last_handshake: u64,
},
ConnectionHealthChanged {
profile_id: ProfileId,
old: ConnectionHealth,
new: ConnectionHealth,
},
IpChanged { old: Option<String>, new: String },
KillswitchEngaged { reason: KillswitchEngageReason },
KillswitchDisengaged,
RetryScheduled {
profile_id: ProfileId,
next_attempt: u32,
delay: Duration,
retry_budget_remaining: Duration,
},
RetryBudgetExhausted {
profile_id: ProfileId,
total_attempts: u32,
elapsed: Duration,
},
NetworkLinkLost,
NetworkLinkRestored { new_gateway: Option<String> },
ProfileRenamed {
profile_id: ProfileId,
old_display_name: String,
new_display_name: String,
},
ProfileDeletionRequested { profile_id: ProfileId },
JournalRetentionApplied { deleted: u32 },
DegradedReasonCleared {
profile_id: ProfileId,
reason: DegradedReason,
},
UserPromptRequested {
profile_id: ProfileId,
prompt_id: String,
prompt_kind: crate::vortix_core::engine::state::PromptKind,
prompt_text: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum TunnelDownReason {
UserDisconnect,
NetworkLinkLost,
DaemonExited,
HandshakeFailed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum KillswitchEngageReason {
UserRequest,
AutoOnConnect,
AlwaysOn,
RecoveredFromCrash,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn envelope_round_trips_through_json() {
let env = EventEnvelope::new(EngineEvent::TunnelUp {
profile_id: ProfileId::new("corp"),
protocol: ProtocolKind::WireGuard,
interface_name: "wg0".to_string(),
pid: None,
});
let json = serde_json::to_string(&env).unwrap();
let back: EventEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(back.schema_version, SCHEMA_VERSION);
match back.event {
EngineEvent::TunnelUp { interface_name, .. } => {
assert_eq!(interface_name, "wg0");
}
_ => panic!("expected TunnelUp"),
}
}
#[test]
fn snake_case_tag_uses_kind() {
let env = EventEnvelope::new(EngineEvent::NetworkLinkLost);
let json = serde_json::to_string(&env).unwrap();
assert!(json.contains(r#""kind":"network_link_lost""#));
}
}