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 = crate::acquire_lock(&self.recording, "EventRecorder");
108 if rec.is_some() {
109 return Err(VictauriError::RecordingAlreadyActive);
110 }
111 *rec = Some(ActiveRecording {
112 session_id,
113 started_at: Utc::now(),
114 events: VecDeque::new(),
115 checkpoints: VecDeque::new(),
116 event_counter: 0,
117 max_events: self.max_events,
118 max_checkpoints: DEFAULT_MAX_CHECKPOINTS,
119 });
120 Ok(())
121 }
122
123 #[must_use]
137 pub fn stop(&self) -> Option<RecordedSession> {
138 let mut rec = crate::acquire_lock(&self.recording, "EventRecorder");
139 rec.take().map(|r| RecordedSession {
140 id: r.session_id,
141 started_at: r.started_at,
142 events: r.events.into_iter().collect(),
143 checkpoints: r.checkpoints.into_iter().collect(),
144 })
145 }
146
147 #[must_use]
149 pub fn is_recording(&self) -> bool {
150 crate::acquire_lock(&self.recording, "EventRecorder").is_some()
151 }
152
153 pub fn record_event(&self, event: AppEvent) {
155 let mut rec = crate::acquire_lock(&self.recording, "EventRecorder");
156 if let Some(ref mut active) = *rec {
157 let timestamp = event.timestamp();
158 let index = active.event_counter;
159 active.event_counter += 1;
160
161 if active.events.len() >= active.max_events {
162 active.events.pop_front();
163 }
164
165 active.events.push_back(RecordedEvent {
166 index,
167 timestamp,
168 event,
169 });
170 }
171 }
172
173 pub fn checkpoint(
179 &self,
180 id: String,
181 label: Option<String>,
182 state: serde_json::Value,
183 ) -> crate::error::Result<()> {
184 let mut rec = crate::acquire_lock(&self.recording, "EventRecorder");
185 if let Some(ref mut active) = *rec {
186 let event_index = active.event_counter;
187 if active.checkpoints.len() >= active.max_checkpoints {
188 active.checkpoints.pop_front();
189 }
190 active.checkpoints.push_back(StateCheckpoint {
191 id,
192 label,
193 timestamp: Utc::now(),
194 state,
195 event_index,
196 });
197 Ok(())
198 } else {
199 Err(VictauriError::NoActiveRecording)
200 }
201 }
202
203 #[must_use]
205 pub fn event_count(&self) -> usize {
206 crate::acquire_lock(&self.recording, "EventRecorder")
207 .as_ref()
208 .map_or(0, |r| r.events.len())
209 }
210
211 #[must_use]
213 pub fn checkpoint_count(&self) -> usize {
214 crate::acquire_lock(&self.recording, "EventRecorder")
215 .as_ref()
216 .map_or(0, |r| r.checkpoints.len())
217 }
218
219 #[must_use]
221 pub fn events_since(&self, index: usize) -> Vec<RecordedEvent> {
222 let rec = crate::acquire_lock(&self.recording, "EventRecorder");
223 match rec.as_ref() {
224 Some(active) => active
225 .events
226 .iter()
227 .filter(|e| e.index >= index)
228 .cloned()
229 .collect(),
230 None => Vec::new(),
231 }
232 }
233
234 #[must_use]
236 pub fn events_between(&self, from: DateTime<Utc>, to: DateTime<Utc>) -> Vec<RecordedEvent> {
237 let rec = crate::acquire_lock(&self.recording, "EventRecorder");
238 match rec.as_ref() {
239 Some(active) => active
240 .events
241 .iter()
242 .filter(|e| e.timestamp >= from && e.timestamp <= to)
243 .cloned()
244 .collect(),
245 None => Vec::new(),
246 }
247 }
248
249 #[must_use]
251 pub fn get_checkpoints(&self) -> Vec<StateCheckpoint> {
252 let rec = crate::acquire_lock(&self.recording, "EventRecorder");
253 match rec.as_ref() {
254 Some(active) => active.checkpoints.iter().cloned().collect(),
255 None => Vec::new(),
256 }
257 }
258
259 pub fn events_between_checkpoints(
266 &self,
267 from_checkpoint_id: &str,
268 to_checkpoint_id: &str,
269 ) -> crate::error::Result<Vec<RecordedEvent>> {
270 let rec = crate::acquire_lock(&self.recording, "EventRecorder");
271 let active = rec.as_ref().ok_or(VictauriError::NoActiveRecording)?;
272
273 let from_idx = active
274 .checkpoints
275 .iter()
276 .find(|c| c.id == from_checkpoint_id)
277 .ok_or_else(|| VictauriError::CheckpointNotFound {
278 id: from_checkpoint_id.to_string(),
279 })?
280 .event_index;
281 let to_idx = active
282 .checkpoints
283 .iter()
284 .find(|c| c.id == to_checkpoint_id)
285 .ok_or_else(|| VictauriError::CheckpointNotFound {
286 id: to_checkpoint_id.to_string(),
287 })?
288 .event_index;
289
290 let (start, end) = if from_idx <= to_idx {
291 (from_idx, to_idx)
292 } else {
293 (to_idx, from_idx)
294 };
295
296 Ok(active
297 .events
298 .iter()
299 .filter(|e| e.index >= start && e.index < end)
300 .cloned()
301 .collect())
302 }
303
304 #[must_use]
306 pub fn export(&self) -> Option<RecordedSession> {
307 let rec = crate::acquire_lock(&self.recording, "EventRecorder");
308 rec.as_ref().map(|r| RecordedSession {
309 id: r.session_id.clone(),
310 started_at: r.started_at,
311 events: r.events.iter().cloned().collect(),
312 checkpoints: r.checkpoints.iter().cloned().collect(),
313 })
314 }
315
316 pub fn import(&self, session: RecordedSession) {
318 let event_counter = session.events.last().map_or(0, |e| e.index + 1);
319 let max_events = self.max_events;
320 let mut rec = crate::acquire_lock(&self.recording, "EventRecorder");
321 *rec = Some(ActiveRecording {
322 session_id: session.id,
323 started_at: session.started_at,
324 events: session.events.into_iter().collect(),
325 checkpoints: session.checkpoints.into_iter().collect(),
326 event_counter,
327 max_events,
328 max_checkpoints: DEFAULT_MAX_CHECKPOINTS,
329 });
330 }
331
332 #[must_use]
334 pub fn ipc_replay_sequence(&self) -> Vec<IpcCall> {
335 let rec = crate::acquire_lock(&self.recording, "EventRecorder");
336 match rec.as_ref() {
337 Some(active) => active
338 .events
339 .iter()
340 .filter_map(|re| match &re.event {
341 AppEvent::Ipc(call) => Some(call.clone()),
342 _ => None,
343 })
344 .collect(),
345 None => Vec::new(),
346 }
347 }
348}
349
350impl Default for EventRecorder {
351 fn default() -> Self {
352 Self::new(DEFAULT_MAX_EVENTS)
353 }
354}