1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::VecDeque;
4use std::sync::{Arc, Mutex};
5
6use crate::event::{AppEvent, IpcCall};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct StateCheckpoint {
11 pub id: String,
13 pub label: Option<String>,
15 pub timestamp: DateTime<Utc>,
17 pub state: serde_json::Value,
19 pub event_index: usize,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct RecordedSession {
26 pub id: String,
28 pub started_at: DateTime<Utc>,
30 pub events: Vec<RecordedEvent>,
32 pub checkpoints: Vec<StateCheckpoint>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct RecordedEvent {
39 pub index: usize,
41 pub timestamp: DateTime<Utc>,
43 pub event: AppEvent,
45}
46
47#[derive(Debug, Clone)]
50pub struct EventRecorder {
51 recording: Arc<Mutex<Option<ActiveRecording>>>,
52 max_events: usize,
53}
54
55#[derive(Debug, Clone)]
56struct ActiveRecording {
57 session_id: String,
58 started_at: DateTime<Utc>,
59 events: VecDeque<RecordedEvent>,
60 checkpoints: VecDeque<StateCheckpoint>,
61 event_counter: usize,
62 max_events: usize,
63 max_checkpoints: usize,
64}
65
66impl EventRecorder {
67 pub fn new(max_events: usize) -> Self {
77 Self {
78 recording: Arc::new(Mutex::new(None)),
79 max_events,
80 }
81 }
82
83 pub fn start(&self, session_id: String) -> bool {
85 let mut rec = self.recording.lock().unwrap_or_else(|e| e.into_inner());
86 if rec.is_some() {
87 return false;
88 }
89 *rec = Some(ActiveRecording {
90 session_id,
91 started_at: Utc::now(),
92 events: VecDeque::new(),
93 checkpoints: VecDeque::new(),
94 event_counter: 0,
95 max_events: self.max_events,
96 max_checkpoints: 1000,
97 });
98 true
99 }
100
101 pub fn stop(&self) -> Option<RecordedSession> {
103 let mut rec = self.recording.lock().unwrap_or_else(|e| e.into_inner());
104 rec.take().map(|r| RecordedSession {
105 id: r.session_id,
106 started_at: r.started_at,
107 events: r.events.into_iter().collect(),
108 checkpoints: r.checkpoints.into_iter().collect(),
109 })
110 }
111
112 pub fn is_recording(&self) -> bool {
114 self.recording
115 .lock()
116 .unwrap_or_else(|e| e.into_inner())
117 .is_some()
118 }
119
120 pub fn record_event(&self, event: AppEvent) {
122 let mut rec = self.recording.lock().unwrap_or_else(|e| e.into_inner());
123 if let Some(ref mut active) = *rec {
124 let timestamp = extract_timestamp(&event);
125 let index = active.event_counter;
126 active.event_counter += 1;
127
128 if active.events.len() >= active.max_events {
129 active.events.pop_front();
130 }
131
132 active.events.push_back(RecordedEvent {
133 index,
134 timestamp,
135 event,
136 });
137 }
138 }
139
140 pub fn checkpoint(&self, id: String, label: Option<String>, state: serde_json::Value) -> bool {
142 let mut rec = self.recording.lock().unwrap_or_else(|e| e.into_inner());
143 if let Some(ref mut active) = *rec {
144 let event_index = active.event_counter;
145 if active.checkpoints.len() >= active.max_checkpoints {
146 active.checkpoints.pop_front();
147 }
148 active.checkpoints.push_back(StateCheckpoint {
149 id,
150 label,
151 timestamp: Utc::now(),
152 state,
153 event_index,
154 });
155 true
156 } else {
157 false
158 }
159 }
160
161 pub fn event_count(&self) -> usize {
163 self.recording
164 .lock()
165 .unwrap_or_else(|e| e.into_inner())
166 .as_ref()
167 .map(|r| r.events.len())
168 .unwrap_or(0)
169 }
170
171 pub fn checkpoint_count(&self) -> usize {
173 self.recording
174 .lock()
175 .unwrap_or_else(|e| e.into_inner())
176 .as_ref()
177 .map(|r| r.checkpoints.len())
178 .unwrap_or(0)
179 }
180
181 pub fn events_since(&self, index: usize) -> Vec<RecordedEvent> {
183 let rec = self.recording.lock().unwrap_or_else(|e| e.into_inner());
184 match rec.as_ref() {
185 Some(active) => active
186 .events
187 .iter()
188 .filter(|e| e.index >= index)
189 .cloned()
190 .collect(),
191 None => Vec::new(),
192 }
193 }
194
195 pub fn events_between(&self, from: DateTime<Utc>, to: DateTime<Utc>) -> Vec<RecordedEvent> {
197 let rec = self.recording.lock().unwrap_or_else(|e| e.into_inner());
198 match rec.as_ref() {
199 Some(active) => active
200 .events
201 .iter()
202 .filter(|e| e.timestamp >= from && e.timestamp <= to)
203 .cloned()
204 .collect(),
205 None => Vec::new(),
206 }
207 }
208
209 pub fn get_checkpoints(&self) -> Vec<StateCheckpoint> {
211 let rec = self.recording.lock().unwrap_or_else(|e| e.into_inner());
212 match rec.as_ref() {
213 Some(active) => active.checkpoints.iter().cloned().collect(),
214 None => Vec::new(),
215 }
216 }
217
218 pub fn events_between_checkpoints(
220 &self,
221 from_checkpoint_id: &str,
222 to_checkpoint_id: &str,
223 ) -> Option<Vec<RecordedEvent>> {
224 let rec = self.recording.lock().unwrap_or_else(|e| e.into_inner());
225 let active = rec.as_ref()?;
226
227 let from_idx = active
228 .checkpoints
229 .iter()
230 .find(|c| c.id == from_checkpoint_id)?
231 .event_index;
232 let to_idx = active
233 .checkpoints
234 .iter()
235 .find(|c| c.id == to_checkpoint_id)?
236 .event_index;
237
238 let (start, end) = if from_idx <= to_idx {
239 (from_idx, to_idx)
240 } else {
241 (to_idx, from_idx)
242 };
243
244 Some(
245 active
246 .events
247 .iter()
248 .filter(|e| e.index >= start && e.index < end)
249 .cloned()
250 .collect(),
251 )
252 }
253
254 pub fn ipc_replay_sequence(&self) -> Vec<IpcCall> {
256 let rec = self.recording.lock().unwrap_or_else(|e| e.into_inner());
257 match rec.as_ref() {
258 Some(active) => active
259 .events
260 .iter()
261 .filter_map(|re| match &re.event {
262 AppEvent::Ipc(call) => Some(call.clone()),
263 _ => None,
264 })
265 .collect(),
266 None => Vec::new(),
267 }
268 }
269}
270
271impl Default for EventRecorder {
272 fn default() -> Self {
273 Self::new(50_000)
274 }
275}
276
277fn extract_timestamp(event: &AppEvent) -> DateTime<Utc> {
278 match event {
279 AppEvent::Ipc(call) => call.timestamp,
280 AppEvent::StateChange { timestamp, .. } => *timestamp,
281 AppEvent::DomMutation { timestamp, .. } => *timestamp,
282 AppEvent::WindowEvent { timestamp, .. } => *timestamp,
283 }
284}