Skip to main content

ravenrs_sheet/engine/
record.rs

1//! The recording buffer: the commands a person (or an agent) ran while recording, each with the
2//! document, sheet and selection it ran in.
3//!
4//! Only top-level calls are kept. A command that runs other commands (Custom Views › Show, a
5//! script) is one step, so playing the recording back does not run the inner commands twice. The
6//! buffer is separate from the journal, so trimming the journal never loses recorded steps, and
7//! secret parameters are left out as they are in the journal ([`crate::engine::cmd::journal_params`]).
8
9use serde::Serialize;
10use serde_json::Value as Json;
11use std::collections::BTreeMap;
12
13use crate::engine::Selection;
14
15/// The most steps one recording keeps; later ones are counted in [`Recorder::dropped`].
16pub const RECORD_LIMIT: usize = 100_000;
17/// The most bytes (steps serialized as JSON) one recording keeps. A step keeps its full
18/// parameters so it can be played back, and a `file.open` holds the whole file as base64, so
19/// without a size cap a recording of a few opens of a large workbook keeps tens of MB alive.
20pub const RECORD_MAX_BYTES: usize = 64 * 1024 * 1024;
21
22/// Commands never recorded: recovery actions and the debugger, not steps of the person's work. A
23/// recording that replayed Stop Action would stop the run it is a step of, and the debugger runs
24/// on its own, never as a step of a script or macro.
25pub const NOT_RECORDED: [&str; 2] = ["automate.stopAction", "macro.debug"];
26
27/// One recorded command and where it ran.
28#[derive(Clone, Debug, PartialEq, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub struct RecordedStep {
31    /// Uid of the document that was active when the command ran (0 when none was open).
32    pub doc: u64,
33    /// That document's active sheet (index and name) just before the command ran.
34    pub sheet: usize,
35    pub sheet_name: String,
36    /// That document's selection just before the command ran.
37    pub selection: Selection,
38    /// The command id and its parameters, without secrets.
39    pub id: String,
40    pub params: Json,
41}
42
43/// Numbers each recording, so an undo group's mark from an earlier one isn't read as this one's.
44static NEXT_RECORDING: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
45
46/// The steps recorded since recording started.
47#[derive(Clone, Debug)]
48pub struct Recorder {
49    pub steps: Vec<RecordedStep>,
50    /// Steps not kept because the recording reached [`RECORD_LIMIT`] or [`RECORD_MAX_BYTES`].
51    /// Once a step is dropped, every later one is too, so `steps` is always the beginning of
52    /// what ran, never a sequence with gaps.
53    pub dropped: usize,
54    /// The size of `steps` and `ran` serialized as JSON.
55    bytes: usize,
56    /// What a step ran, by its index in `steps`, for a command that runs steps stored in the
57    /// workbook (`automate.runScript`, `macro.run`): the commands as they ran, so a recording
58    /// keeps them even when the script or macro is edited or deleted before the recording stops.
59    /// A run without an entry is left out when the recording is used.
60    pub ran: BTreeMap<usize, Vec<(String, Json)>>,
61    /// Which recording this is, and the number the next kept step gets: each step's number is in
62    /// `seqs`, beside it, so a mark stays right however many steps rollbacks take out.
63    generation: u64,
64    next_seq: u64,
65    seqs: Vec<u64>,
66}
67
68impl Default for Recorder {
69    fn default() -> Self {
70        let generation = NEXT_RECORDING.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
71        Recorder { steps: Vec::new(), dropped: 0, bytes: 0, ran: BTreeMap::new(), generation, next_seq: 0, seqs: Vec::new() }
72    }
73}
74
75/// `params` with the state a toggle set (Bold's `on`, from the command's result), as Excel
76/// records `Bold = True`: playing the step back sets that state rather than flipping it again.
77/// `python.output` without `mode` is a toggle too: it keeps the return type it set (the result's
78/// `output`) as its `mode`.
79pub(crate) fn with_toggle_state(id: &str, mut params: Json, result: &Json) -> Json {
80    let state = match id {
81        "python.output" => result.get("output").filter(|o| o.is_string()).map(|o| ("mode", o)),
82        _ => result.get("on").filter(|o| o.is_boolean()).map(|o| ("on", o)),
83    };
84    if let Some((key, state)) = state {
85        if params.is_null() {
86            params = Json::Object(serde_json::Map::new());
87        }
88        if let Json::Object(p) = &mut params
89            && p.get(key).is_none_or(Json::is_null)
90        {
91            p.insert(key.into(), state.clone());
92        }
93    }
94    params
95}
96
97impl Recorder {
98    /// Adds a step, or counts it as dropped once the buffer is full; `true` when it was kept.
99    pub fn push(&mut self, step: RecordedStep) -> bool {
100        // Once a step has been dropped, or the step count is full, the rest are counted without
101        // being serialized (a `file.open` step holds a whole file).
102        if self.dropped > 0 || self.steps.len() >= RECORD_LIMIT {
103            self.dropped = self.dropped.saturating_add(1);
104            return false;
105        }
106        let size = serde_json::to_vec(&step).map_or(0, |v| v.len());
107        if self.bytes.saturating_add(size) <= RECORD_MAX_BYTES {
108            self.bytes = self.bytes.saturating_add(size);
109            self.steps.push(step);
110            self.seqs.push(self.next_seq);
111            self.next_seq = self.next_seq.saturating_add(1);
112            true
113        } else {
114            self.dropped = self.dropped.saturating_add(1);
115            false
116        }
117    }
118
119    /// Attaches what the last kept step ran ([`Recorder::ran`]), within the byte budget: past it,
120    /// the step stays without it, and the recording leaves the run out and lists it as skipped.
121    pub fn attach_ran(&mut self, steps: Vec<(String, Json)>) {
122        let Some(index) = self.steps.len().checked_sub(1) else { return };
123        let size = serde_json::to_vec(&steps).map_or(0, |v| v.len());
124        if self.bytes.saturating_add(size) <= RECORD_MAX_BYTES {
125            self.bytes = self.bytes.saturating_add(size);
126            self.ran.insert(index, steps);
127        }
128    }
129
130    /// Where the recording is now, for [`Recorder::cut_back`]: this recording and the number its
131    /// next step gets.
132    pub(crate) fn mark(&self) -> (u64, u64) {
133        (self.generation, self.next_seq)
134    }
135
136    /// Takes back the steps recorded in the document with uid `doc` since `mark` (an undo group
137    /// on it was rolled back, so playing them back would make edits it no longer has). Steps in
138    /// other documents stay: their edits do too. Steps the buffer dropped stay counted.
139    /// What a step ran ([`Recorder::ran`]) goes with its step, and stays with a kept one under its
140    /// new index. A mark from another recording (the recording started again inside the group)
141    /// means every step of this one came after it.
142    pub(crate) fn cut_back(&mut self, (generation, seq): (u64, u64), doc: u64) {
143        let from = if generation == self.generation { seq } else { 0 };
144        let gone = |i: usize, step: &RecordedStep| step.doc == doc && self.seqs.get(i).is_some_and(|&n| n >= from);
145        if !self.steps.iter().enumerate().any(|(i, s)| gone(i, s)) {
146            return;
147        }
148        let mut ran = std::mem::take(&mut self.ran);
149        let (mut steps, mut seqs) = (Vec::with_capacity(self.steps.len()), Vec::with_capacity(self.seqs.len()));
150        for (i, step) in self.steps.iter().enumerate() {
151            if gone(i, step) {
152                continue;
153            }
154            if let Some(r) = ran.remove(&i) {
155                self.ran.insert(steps.len(), r);
156            }
157            steps.push(step.clone());
158            seqs.push(self.seqs.get(i).copied().unwrap_or(0));
159        }
160        (self.steps, self.seqs) = (steps, seqs);
161        let size = |v: Result<Vec<u8>, serde_json::Error>| v.map_or(0, |v| v.len());
162        self.bytes = self.steps.iter().map(|s| size(serde_json::to_vec(s))).sum::<usize>()
163            + self.ran.values().map(|r| size(serde_json::to_vec(r))).sum::<usize>();
164    }
165}