Skip to main content

victauri_core/
event.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::VecDeque;
4use std::sync::{Arc, Mutex};
5
6/// A single Tauri IPC call with timing, result, and source webview.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct IpcCall {
9    /// Unique call identifier for correlation.
10    pub id: String,
11    /// Name of the Tauri command that was invoked.
12    pub command: String,
13    /// When the call was initiated.
14    pub timestamp: DateTime<Utc>,
15    /// Round-trip duration in milliseconds, if completed.
16    pub duration_ms: Option<u64>,
17    /// Current outcome of the call (pending, ok, or error).
18    pub result: IpcResult,
19    /// Size of the serialized arguments in bytes.
20    pub arg_size_bytes: usize,
21    /// Label of the webview that initiated the call.
22    pub webview_label: String,
23}
24
25/// Outcome of an IPC call: pending, success with a JSON value, or error.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[non_exhaustive]
28pub enum IpcResult {
29    /// Call is still in flight, awaiting a response.
30    Pending,
31    /// Call completed successfully with a JSON return value.
32    Ok(serde_json::Value),
33    /// Call failed with an error message.
34    Err(String),
35}
36
37/// Application event captured by the introspection layer.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(tag = "type")]
40#[non_exhaustive]
41pub enum AppEvent {
42    /// An IPC call between webview and Rust backend.
43    Ipc(IpcCall),
44    /// A change to application state in the backend.
45    StateChange {
46        /// State key that changed.
47        key: String,
48        /// When the change occurred.
49        timestamp: DateTime<Utc>,
50        /// Command or action that triggered the change, if known.
51        caused_by: Option<String>,
52    },
53    /// A batch of DOM mutations observed in a webview.
54    DomMutation {
55        /// Webview where the mutations were observed.
56        webview_label: String,
57        /// When the mutations were observed.
58        timestamp: DateTime<Utc>,
59        /// Number of individual DOM mutations in this batch.
60        mutation_count: u32,
61    },
62    /// A native window lifecycle event (e.g. focus, resize, close).
63    WindowEvent {
64        /// Tauri window label that emitted the event.
65        label: String,
66        /// Event name (e.g. "focus", "resize").
67        event: String,
68        /// When the event occurred.
69        timestamp: DateTime<Utc>,
70    },
71}
72
73/// Thread-safe ring-buffer event log. Automatically evicts the oldest events
74/// when capacity is reached. All operations recover from mutex poisoning.
75#[derive(Debug, Clone)]
76pub struct EventLog {
77    events: Arc<Mutex<VecDeque<AppEvent>>>,
78    max_capacity: usize,
79}
80
81impl EventLog {
82    /// Creates a new event log with the given maximum capacity.
83    ///
84    /// ```
85    /// use victauri_core::EventLog;
86    ///
87    /// let log = EventLog::new(100);
88    /// assert!(log.is_empty());
89    /// assert_eq!(log.capacity(), 100);
90    /// ```
91    pub fn new(max_capacity: usize) -> Self {
92        Self {
93            events: Arc::new(Mutex::new(VecDeque::with_capacity(max_capacity))),
94            max_capacity,
95        }
96    }
97
98    /// Returns the maximum number of events this log can hold.
99    pub fn capacity(&self) -> usize {
100        self.max_capacity
101    }
102
103    /// Appends an event, evicting the oldest if at capacity.
104    pub fn push(&self, event: AppEvent) {
105        let mut events = self.events.lock().unwrap_or_else(|e| e.into_inner());
106        if events.len() >= self.max_capacity {
107            events.pop_front();
108        }
109        events.push_back(event);
110    }
111
112    /// Returns a clone of all events currently in the log.
113    pub fn snapshot(&self) -> Vec<AppEvent> {
114        self.events
115            .lock()
116            .unwrap_or_else(|e| e.into_inner())
117            .iter()
118            .cloned()
119            .collect()
120    }
121
122    /// Returns a paginated slice of events starting at `offset`, up to `limit` items.
123    pub fn snapshot_range(&self, offset: usize, limit: usize) -> Vec<AppEvent> {
124        self.events
125            .lock()
126            .unwrap_or_else(|e| e.into_inner())
127            .iter()
128            .skip(offset)
129            .take(limit)
130            .cloned()
131            .collect()
132    }
133
134    /// Returns all events with a timestamp at or after the given time.
135    pub fn since(&self, timestamp: DateTime<Utc>) -> Vec<AppEvent> {
136        self.events
137            .lock()
138            .unwrap_or_else(|e| e.into_inner())
139            .iter()
140            .filter(|e| match e {
141                AppEvent::Ipc(call) => call.timestamp >= timestamp,
142                AppEvent::StateChange { timestamp: ts, .. } => *ts >= timestamp,
143                AppEvent::DomMutation { timestamp: ts, .. } => *ts >= timestamp,
144                AppEvent::WindowEvent { timestamp: ts, .. } => *ts >= timestamp,
145            })
146            .cloned()
147            .collect()
148    }
149
150    /// Returns all IPC call events, filtering out non-IPC events.
151    pub fn ipc_calls(&self) -> Vec<IpcCall> {
152        self.events
153            .lock()
154            .unwrap_or_else(|e| e.into_inner())
155            .iter()
156            .filter_map(|e| match e {
157                AppEvent::Ipc(call) => Some(call.clone()),
158                _ => None,
159            })
160            .collect()
161    }
162
163    /// Returns IPC calls with a timestamp at or after the given time.
164    pub fn ipc_calls_since(&self, timestamp: DateTime<Utc>) -> Vec<IpcCall> {
165        self.events
166            .lock()
167            .unwrap_or_else(|e| e.into_inner())
168            .iter()
169            .filter_map(|e| match e {
170                AppEvent::Ipc(call) if call.timestamp >= timestamp => Some(call.clone()),
171                _ => None,
172            })
173            .collect()
174    }
175
176    /// Returns the number of events currently in the log.
177    pub fn len(&self) -> usize {
178        self.events.lock().unwrap_or_else(|e| e.into_inner()).len()
179    }
180
181    /// Returns true if the log contains no events.
182    pub fn is_empty(&self) -> bool {
183        self.events
184            .lock()
185            .unwrap_or_else(|e| e.into_inner())
186            .is_empty()
187    }
188
189    /// Removes all events from the log.
190    pub fn clear(&self) {
191        self.events
192            .lock()
193            .unwrap_or_else(|e| e.into_inner())
194            .clear();
195    }
196}