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}