Files
gridcraft/crates/engine/src/record.rs
T
prxnvt ca2be34e8a Engine: a rolled-back group takes what its script runs ran out of the recording too
Taking back the steps a rolled-back group recorded left what a taken-back script or macro run
ran (`Recorder::ran`) under its old index, where a later run could pick it up, and left it out of
the size count. `cut_back` now drops it with its step, moves a kept run's entry to the step's new
index and counts both again.

- Test: `a_rolled_back_script_run_takes_what_it_ran_out_of_the_recording`.
2026-10-25 06:29:23 -04:00

138 lines
6.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The recording buffer: the commands a person (or an agent) ran while recording, each with the
//! document, sheet and selection it ran in.
//!
//! Only top-level calls are kept. A command that runs other commands (Custom Views › Show, a
//! script) is one step, so playing the recording back does not run the inner commands twice. The
//! buffer is separate from the journal, so trimming the journal never loses recorded steps, and
//! secret parameters are left out as they are in the journal ([`crate::cmd::journal_params`]).
use serde::Serialize;
use serde_json::Value as Json;
use std::collections::BTreeMap;
use crate::Selection;
/// The most steps one recording keeps; later ones are counted in [`Recorder::dropped`].
pub const RECORD_LIMIT: usize = 100_000;
/// The most bytes (steps serialized as JSON) one recording keeps. A step keeps its full
/// parameters so it can be played back, and a `file.open` holds the whole file as base64, so
/// without a size cap a recording of a few opens of a large workbook keeps tens of MB alive.
pub const RECORD_MAX_BYTES: usize = 64 * 1024 * 1024;
/// Commands never recorded: recovery actions and the debugger, not steps of the person's work. A
/// recording that replayed Stop Action would stop the run it is a step of, and the debugger runs
/// on its own, never as a step of a script or macro.
pub const NOT_RECORDED: [&str; 2] = ["automate.stopAction", "macro.debug"];
/// One recorded command and where it ran.
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RecordedStep {
/// Uid of the document that was active when the command ran (0 when none was open).
pub doc: u64,
/// That document's active sheet (index and name) just before the command ran.
pub sheet: usize,
pub sheet_name: String,
/// That document's selection just before the command ran.
pub selection: Selection,
/// The command id and its parameters, without secrets.
pub id: String,
pub params: Json,
}
/// The steps recorded since recording started.
#[derive(Clone, Debug, Default)]
pub struct Recorder {
pub steps: Vec<RecordedStep>,
/// Steps not kept because the recording reached [`RECORD_LIMIT`] or [`RECORD_MAX_BYTES`].
/// Once a step is dropped, every later one is too, so `steps` is always the beginning of
/// what ran, never a sequence with gaps.
pub dropped: usize,
/// The size of `steps` and `ran` serialized as JSON.
bytes: usize,
/// What a step ran, by its index in `steps`, for a command that runs steps stored in the
/// workbook (`automate.runScript`, `macro.run`): the commands as they ran, so a recording
/// keeps them even when the script or macro is edited or deleted before the recording stops.
/// A run without an entry is left out when the recording is used.
pub ran: BTreeMap<usize, Vec<(String, Json)>>,
}
/// `params` with the state a toggle set (Bold's `on`, from the command's result), as Excel
/// records `Bold = True`: playing the step back sets that state rather than flipping it again.
pub(crate) fn with_toggle_state(mut params: Json, result: &Json) -> Json {
if let Some(on @ Json::Bool(_)) = result.get("on") {
if params.is_null() {
params = Json::Object(serde_json::Map::new());
}
if let Json::Object(p) = &mut params
&& !p.contains_key("on")
{
p.insert("on".into(), on.clone());
}
}
params
}
impl Recorder {
/// Adds a step, or counts it as dropped once the buffer is full; `true` when it was kept.
pub fn push(&mut self, step: RecordedStep) -> bool {
// Once a step has been dropped, or the step count is full, the rest are counted without
// being serialized (a `file.open` step holds a whole file).
if self.dropped > 0 || self.steps.len() >= RECORD_LIMIT {
self.dropped = self.dropped.saturating_add(1);
return false;
}
let size = serde_json::to_vec(&step).map_or(0, |v| v.len());
if self.bytes.saturating_add(size) <= RECORD_MAX_BYTES {
self.bytes = self.bytes.saturating_add(size);
self.steps.push(step);
true
} else {
self.dropped = self.dropped.saturating_add(1);
false
}
}
/// Attaches what the last kept step ran ([`Recorder::ran`]), within the byte budget: past it,
/// the step stays without it, and the recording leaves the run out and lists it as skipped.
pub fn attach_ran(&mut self, steps: Vec<(String, Json)>) {
let Some(index) = self.steps.len().checked_sub(1) else { return };
let size = serde_json::to_vec(&steps).map_or(0, |v| v.len());
if self.bytes.saturating_add(size) <= RECORD_MAX_BYTES {
self.bytes = self.bytes.saturating_add(size);
self.ran.insert(index, steps);
}
}
/// Where the recording is now, for [`Recorder::cut_back`].
pub(crate) fn mark(&self) -> (usize, usize) {
(self.steps.len(), self.dropped)
}
/// Takes back the steps recorded in the document with uid `doc` since `mark` (an undo group
/// on it was rolled back, so playing them back would make edits it no longer has). Steps in
/// other documents stay: their edits do too. Steps the buffer dropped stay counted.
/// What a step ran ([`Recorder::ran`]) goes with its step, and stays with a kept one under its
/// new index.
pub(crate) fn cut_back(&mut self, (len, _): (usize, usize), doc: u64) {
if !self.steps.iter().skip(len).any(|s| s.doc == doc) {
return;
}
let mut ran = std::mem::take(&mut self.ran);
let mut steps = Vec::with_capacity(self.steps.len());
for (i, step) in std::mem::take(&mut self.steps).into_iter().enumerate() {
if i >= len && step.doc == doc {
continue;
}
if let Some(r) = ran.remove(&i) {
self.ran.insert(steps.len(), r);
}
steps.push(step);
}
self.steps = steps;
let size = |v: Result<Vec<u8>, serde_json::Error>| v.map_or(0, |v| v.len());
self.bytes = self.steps.iter().map(|s| size(serde_json::to_vec(s))).sum::<usize>()
+ self.ran.values().map(|r| size(serde_json::to_vec(r))).sum::<usize>();
}
}