victauri_core/event.rs
1//! Application event types and a thread-safe ring-buffer event log.
2
3use std::collections::VecDeque;
4use std::fmt;
5use std::sync::{Arc, Mutex};
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10/// A single Tauri IPC call with timing, result, and source webview.
11#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
12pub struct IpcCall {
13 /// Unique call identifier for correlation.
14 pub id: String,
15 /// Name of the Tauri command that was invoked.
16 pub command: String,
17 /// When the call was initiated.
18 pub timestamp: DateTime<Utc>,
19 /// Round-trip duration in milliseconds, if completed.
20 pub duration_ms: Option<u64>,
21 /// Current outcome of the call (pending, ok, or error).
22 pub result: IpcResult,
23 /// Size of the serialized arguments in bytes.
24 pub arg_size_bytes: usize,
25 /// Label of the webview that initiated the call.
26 pub webview_label: String,
27}
28
29/// Outcome of an IPC call: pending, success with a JSON value, or error.
30#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
31#[non_exhaustive]
32pub enum IpcResult {
33 /// Call is still in flight, awaiting a response.
34 Pending,
35 /// Call completed successfully with a JSON return value.
36 Ok(serde_json::Value),
37 /// Call failed with an error message.
38 Err(String),
39}
40
41impl fmt::Display for IpcResult {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 Self::Pending => f.write_str("pending"),
45 Self::Ok(_) => f.write_str("ok"),
46 Self::Err(msg) => write!(f, "error: {msg}"),
47 }
48 }
49}
50
51impl fmt::Display for IpcCall {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, "{} [{}] \u{2192} {}", self.command, self.id, self.result)
54 }
55}
56
57impl From<IpcCall> for AppEvent {
58 fn from(call: IpcCall) -> Self {
59 Self::Ipc(call)
60 }
61}
62
63/// The kind of user interaction captured from the DOM.
64#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
65pub enum InteractionKind {
66 /// Mouse click on an element.
67 Click,
68 /// Double-click on an element.
69 DoubleClick,
70 /// Text typed into an input field.
71 Fill,
72 /// Individual key press event.
73 KeyPress,
74 /// Option selected from a dropdown.
75 Select,
76 /// Page navigation (URL change).
77 Navigate,
78 /// Scroll to element or position.
79 Scroll,
80}
81
82impl fmt::Display for InteractionKind {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::Click => f.write_str("click"),
86 Self::DoubleClick => f.write_str("double_click"),
87 Self::Fill => f.write_str("fill"),
88 Self::KeyPress => f.write_str("key_press"),
89 Self::Select => f.write_str("select"),
90 Self::Navigate => f.write_str("navigate"),
91 Self::Scroll => f.write_str("scroll"),
92 }
93 }
94}
95
96/// Application event captured by the introspection layer.
97#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
98#[serde(tag = "type")]
99#[non_exhaustive]
100pub enum AppEvent {
101 /// An IPC call between webview and Rust backend.
102 Ipc(IpcCall),
103 /// A change to application state in the backend.
104 StateChange {
105 /// State key that changed.
106 key: String,
107 /// When the change occurred.
108 timestamp: DateTime<Utc>,
109 /// Command or action that triggered the change, if known.
110 caused_by: Option<String>,
111 },
112 /// A batch of DOM mutations observed in a webview.
113 DomMutation {
114 /// Webview where the mutations were observed.
115 webview_label: String,
116 /// When the mutations were observed.
117 timestamp: DateTime<Utc>,
118 /// Number of individual DOM mutations in this batch.
119 mutation_count: u32,
120 },
121 /// A user interaction captured from the DOM during recording.
122 DomInteraction {
123 /// What kind of interaction occurred.
124 action: InteractionKind,
125 /// Best available selector for the target element (data-testid, id, CSS path).
126 selector: String,
127 /// Value associated with the interaction (typed text, selected option, URL, key name).
128 value: Option<String>,
129 /// When the interaction occurred.
130 timestamp: DateTime<Utc>,
131 /// Label of the webview where the interaction happened.
132 webview_label: String,
133 },
134 /// A native window lifecycle event (e.g. focus, resize, close).
135 WindowEvent {
136 /// Tauri window label that emitted the event.
137 label: String,
138 /// Event name (e.g. "focus", "resize").
139 event: String,
140 /// When the event occurred.
141 timestamp: DateTime<Utc>,
142 },
143}
144
145impl AppEvent {
146 /// Returns the timestamp of this event, regardless of variant.
147 #[must_use]
148 pub fn timestamp(&self) -> DateTime<Utc> {
149 match self {
150 Self::Ipc(call) => call.timestamp,
151 Self::StateChange { timestamp, .. }
152 | Self::DomMutation { timestamp, .. }
153 | Self::DomInteraction { timestamp, .. }
154 | Self::WindowEvent { timestamp, .. } => *timestamp,
155 }
156 }
157}
158
159/// Thread-safe ring-buffer event log. Automatically evicts the oldest events
160/// when capacity is reached. All operations recover from mutex poisoning.
161#[derive(Debug, Clone)]
162pub struct EventLog {
163 events: Arc<Mutex<VecDeque<AppEvent>>>,
164 max_capacity: usize,
165}
166
167impl EventLog {
168 /// Creates a new event log with the given maximum capacity.
169 ///
170 /// ```
171 /// use victauri_core::EventLog;
172 ///
173 /// let log = EventLog::new(100);
174 /// assert!(log.is_empty());
175 /// assert_eq!(log.capacity(), 100);
176 /// ```
177 #[must_use]
178 pub fn new(max_capacity: usize) -> Self {
179 Self {
180 events: Arc::new(Mutex::new(VecDeque::with_capacity(max_capacity))),
181 max_capacity,
182 }
183 }
184
185 /// Returns the maximum number of events this log can hold.
186 #[must_use]
187 pub fn capacity(&self) -> usize {
188 self.max_capacity
189 }
190
191 /// Appends an event, evicting the oldest if at capacity.
192 ///
193 /// # Examples
194 ///
195 /// ```
196 /// use victauri_core::{EventLog, AppEvent};
197 /// use chrono::Utc;
198 ///
199 /// let log = EventLog::new(100);
200 /// log.push(AppEvent::StateChange {
201 /// key: "theme".to_string(),
202 /// timestamp: Utc::now(),
203 /// caused_by: None,
204 /// });
205 /// assert_eq!(log.len(), 1);
206 /// assert_eq!(log.snapshot().len(), 1);
207 /// ```
208 pub fn push(&self, event: AppEvent) {
209 let mut events = crate::acquire_lock(&self.events, "EventLog");
210 if events.len() >= self.max_capacity {
211 events.pop_front();
212 }
213 events.push_back(event);
214 }
215
216 /// Returns a clone of all events currently in the log.
217 #[must_use]
218 pub fn snapshot(&self) -> Vec<AppEvent> {
219 crate::acquire_lock(&self.events, "EventLog")
220 .iter()
221 .cloned()
222 .collect()
223 }
224
225 /// Returns a paginated slice of events starting at `offset`, up to `limit` items.
226 #[must_use]
227 pub fn snapshot_range(&self, offset: usize, limit: usize) -> Vec<AppEvent> {
228 crate::acquire_lock(&self.events, "EventLog")
229 .iter()
230 .skip(offset)
231 .take(limit)
232 .cloned()
233 .collect()
234 }
235
236 /// Returns all events with a timestamp at or after the given time.
237 #[must_use]
238 pub fn since(&self, timestamp: DateTime<Utc>) -> Vec<AppEvent> {
239 crate::acquire_lock(&self.events, "EventLog")
240 .iter()
241 .filter(|e| e.timestamp() >= timestamp)
242 .cloned()
243 .collect()
244 }
245
246 /// Returns all IPC call events, filtering out non-IPC events.
247 ///
248 /// # Examples
249 ///
250 /// ```
251 /// use victauri_core::{EventLog, AppEvent, IpcCall, IpcResult};
252 /// use chrono::Utc;
253 ///
254 /// let log = EventLog::new(100);
255 /// log.push(AppEvent::Ipc(IpcCall {
256 /// id: "c1".to_string(),
257 /// command: "greet".to_string(),
258 /// timestamp: Utc::now(),
259 /// duration_ms: Some(5),
260 /// result: IpcResult::Ok(serde_json::json!("hi")),
261 /// arg_size_bytes: 0,
262 /// webview_label: "main".to_string(),
263 /// }));
264 /// assert_eq!(log.ipc_calls().len(), 1);
265 /// ```
266 #[must_use]
267 pub fn ipc_calls(&self) -> Vec<IpcCall> {
268 crate::acquire_lock(&self.events, "EventLog")
269 .iter()
270 .filter_map(|e| match e {
271 AppEvent::Ipc(call) => Some(call.clone()),
272 _ => None,
273 })
274 .collect()
275 }
276
277 /// Returns IPC calls with a timestamp at or after the given time.
278 #[must_use]
279 pub fn ipc_calls_since(&self, timestamp: DateTime<Utc>) -> Vec<IpcCall> {
280 crate::acquire_lock(&self.events, "EventLog")
281 .iter()
282 .filter_map(|e| match e {
283 AppEvent::Ipc(call) if call.timestamp >= timestamp => Some(call.clone()),
284 _ => None,
285 })
286 .collect()
287 }
288
289 /// Returns the number of events currently in the log.
290 #[must_use]
291 pub fn len(&self) -> usize {
292 crate::acquire_lock(&self.events, "EventLog").len()
293 }
294
295 /// Returns true if the log contains no events.
296 #[must_use]
297 pub fn is_empty(&self) -> bool {
298 crate::acquire_lock(&self.events, "EventLog").is_empty()
299 }
300
301 /// Removes all events from the log.
302 pub fn clear(&self) {
303 crate::acquire_lock(&self.events, "EventLog").clear();
304 }
305}