1use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::VecDeque;
7use std::sync::{Arc, Mutex};
8
9use crate::error::VictauriError;
10use crate::event::{AppEvent, IpcCall};
11
12const DEFAULT_MAX_CHECKPOINTS: usize = 1000;
13const DEFAULT_MAX_EVENTS: usize = 50_000;
14
15#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
17pub struct StateCheckpoint {
18 pub id: String,
20 pub label: Option<String>,
22 pub timestamp: DateTime<Utc>,
24 pub state: serde_json::Value,
26 pub event_index: usize,
28}
29
30#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
32pub struct RecordedSession {
33 pub id: String,
35 pub started_at: DateTime<Utc>,
37 pub events: Vec<RecordedEvent>,
39 pub checkpoints: Vec<StateCheckpoint>,
41}
42
43#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
45pub struct RecordedEvent {
46 pub index: usize,
48 pub timestamp: DateTime<Utc>,
50 pub event: AppEvent,
52}
53
54#[derive(Debug, Clone)]
57pub struct EventRecorder {
58 recording: Arc<Mutex<Option<ActiveRecording>>>,
59 max_events: usize,
60}
61
62#[derive(Debug, Clone)]
63struct ActiveRecording {
64 session_id: String,
65 started_at: DateTime<Utc>,
66 events: VecDeque<RecordedEvent>,
67 checkpoints: VecDeque<StateCheckpoint>,
68 event_counter: usize,
69 max_events: usize,
70 max_checkpoints: usize,
71}
72
73impl EventRecorder {
74 #[must_use]
84 pub fn new(max_events: usize) -> Self {
85 Self {
86 recording: Arc::new(Mutex::new(None)),
87 max_events,
88 }
89 }
90
91 pub fn start(&self, session_id: String) -> crate::error::Result<()> {
107 let mut rec = self
108 .recording
109 .lock()
110 .unwrap_or_else(std::sync::PoisonError::into_inner);
111 if rec.is_some() {
112 return Err(VictauriError::RecordingAlreadyActive);
113 }
114 *rec = Some(ActiveRecording {
115 session_id,
116 started_at: Utc::now(),
117 events: VecDeque::new(),
118 checkpoints: VecDeque::new(),
119 event_counter: 0,
120 max_events: self.max_events,
121 max_checkpoints: DEFAULT_MAX_CHECKPOINTS,
122 });
123 Ok(())
124 }
125
126 #[must_use]
140 pub fn stop(&self) -> Option<RecordedSession> {
141 let mut rec = self
142 .recording
143 .lock()
144 .unwrap_or_else(std::sync::PoisonError::into_inner);
145 rec.take().map(|r| RecordedSession {
146 id: r.session_id,
147 started_at: r.started_at,
148 events: r.events.into_iter().collect(),
149 checkpoints: r.checkpoints.into_iter().collect(),
150 })
151 }
152
153 #[must_use]
155 pub fn is_recording(&self) -> bool {
156 self.recording
157 .lock()
158 .unwrap_or_else(std::sync::PoisonError::into_inner)
159 .is_some()
160 }
161
162 pub fn record_event(&self, event: AppEvent) {
164 let mut rec = self
165 .recording
166 .lock()
167 .unwrap_or_else(std::sync::PoisonError::into_inner);
168 if let Some(ref mut active) = *rec {
169 let timestamp = event.timestamp();
170 let index = active.event_counter;
171 active.event_counter += 1;
172
173 if active.events.len() >= active.max_events {
174 active.events.pop_front();
175 }
176
177 active.events.push_back(RecordedEvent {
178 index,
179 timestamp,
180 event,
181 });
182 }
183 }
184
185 pub fn checkpoint(
191 &self,
192 id: String,
193 label: Option<String>,
194 state: serde_json::Value,
195 ) -> crate::error::Result<()> {
196 let mut rec = self
197 .recording
198 .lock()
199 .unwrap_or_else(std::sync::PoisonError::into_inner);
200 if let Some(ref mut active) = *rec {
201 let event_index = active.event_counter;
202 if active.checkpoints.len() >= active.max_checkpoints {
203 active.checkpoints.pop_front();
204 }
205 active.checkpoints.push_back(StateCheckpoint {
206 id,
207 label,
208 timestamp: Utc::now(),
209 state,
210 event_index,
211 });
212 Ok(())
213 } else {
214 Err(VictauriError::NoActiveRecording)
215 }
216 }
217
218 #[must_use]
220 pub fn event_count(&self) -> usize {
221 self.recording
222 .lock()
223 .unwrap_or_else(std::sync::PoisonError::into_inner)
224 .as_ref()
225 .map_or(0, |r| r.events.len())
226 }
227
228 #[must_use]
230 pub fn checkpoint_count(&self) -> usize {
231 self.recording
232 .lock()
233 .unwrap_or_else(std::sync::PoisonError::into_inner)
234 .as_ref()
235 .map_or(0, |r| r.checkpoints.len())
236 }
237
238 #[must_use]
240 pub fn events_since(&self, index: usize) -> Vec<RecordedEvent> {
241 let rec = self
242 .recording
243 .lock()
244 .unwrap_or_else(std::sync::PoisonError::into_inner);
245 match rec.as_ref() {
246 Some(active) => active
247 .events
248 .iter()
249 .filter(|e| e.index >= index)
250 .cloned()
251 .collect(),
252 None => Vec::new(),
253 }
254 }
255
256 #[must_use]
258 pub fn events_between(&self, from: DateTime<Utc>, to: DateTime<Utc>) -> Vec<RecordedEvent> {
259 let rec = self
260 .recording
261 .lock()
262 .unwrap_or_else(std::sync::PoisonError::into_inner);
263 match rec.as_ref() {
264 Some(active) => active
265 .events
266 .iter()
267 .filter(|e| e.timestamp >= from && e.timestamp <= to)
268 .cloned()
269 .collect(),
270 None => Vec::new(),
271 }
272 }
273
274 #[must_use]
276 pub fn get_checkpoints(&self) -> Vec<StateCheckpoint> {
277 let rec = self
278 .recording
279 .lock()
280 .unwrap_or_else(std::sync::PoisonError::into_inner);
281 match rec.as_ref() {
282 Some(active) => active.checkpoints.iter().cloned().collect(),
283 None => Vec::new(),
284 }
285 }
286
287 pub fn events_between_checkpoints(
294 &self,
295 from_checkpoint_id: &str,
296 to_checkpoint_id: &str,
297 ) -> crate::error::Result<Vec<RecordedEvent>> {
298 let rec = self
299 .recording
300 .lock()
301 .unwrap_or_else(std::sync::PoisonError::into_inner);
302 let active = rec.as_ref().ok_or(VictauriError::NoActiveRecording)?;
303
304 let from_idx = active
305 .checkpoints
306 .iter()
307 .find(|c| c.id == from_checkpoint_id)
308 .ok_or_else(|| VictauriError::CheckpointNotFound {
309 id: from_checkpoint_id.to_string(),
310 })?
311 .event_index;
312 let to_idx = active
313 .checkpoints
314 .iter()
315 .find(|c| c.id == to_checkpoint_id)
316 .ok_or_else(|| VictauriError::CheckpointNotFound {
317 id: to_checkpoint_id.to_string(),
318 })?
319 .event_index;
320
321 let (start, end) = if from_idx <= to_idx {
322 (from_idx, to_idx)
323 } else {
324 (to_idx, from_idx)
325 };
326
327 Ok(active
328 .events
329 .iter()
330 .filter(|e| e.index >= start && e.index < end)
331 .cloned()
332 .collect())
333 }
334
335 #[must_use]
337 pub fn export(&self) -> Option<RecordedSession> {
338 let rec = self
339 .recording
340 .lock()
341 .unwrap_or_else(std::sync::PoisonError::into_inner);
342 rec.as_ref().map(|r| RecordedSession {
343 id: r.session_id.clone(),
344 started_at: r.started_at,
345 events: r.events.iter().cloned().collect(),
346 checkpoints: r.checkpoints.iter().cloned().collect(),
347 })
348 }
349
350 pub fn import(&self, session: RecordedSession) {
352 let event_counter = session.events.last().map_or(0, |e| e.index + 1);
353 let max_events = self.max_events;
354 let mut rec = self
355 .recording
356 .lock()
357 .unwrap_or_else(std::sync::PoisonError::into_inner);
358 *rec = Some(ActiveRecording {
359 session_id: session.id,
360 started_at: session.started_at,
361 events: session.events.into_iter().collect(),
362 checkpoints: session.checkpoints.into_iter().collect(),
363 event_counter,
364 max_events,
365 max_checkpoints: DEFAULT_MAX_CHECKPOINTS,
366 });
367 }
368
369 #[must_use]
371 pub fn ipc_replay_sequence(&self) -> Vec<IpcCall> {
372 let rec = self
373 .recording
374 .lock()
375 .unwrap_or_else(std::sync::PoisonError::into_inner);
376 match rec.as_ref() {
377 Some(active) => active
378 .events
379 .iter()
380 .filter_map(|re| match &re.event {
381 AppEvent::Ipc(call) => Some(call.clone()),
382 _ => None,
383 })
384 .collect(),
385 None => Vec::new(),
386 }
387 }
388}
389
390impl Default for EventRecorder {
391 fn default() -> Self {
392 Self::new(DEFAULT_MAX_EVENTS)
393 }
394}