Skip to main content

ashlar_command/
journal.rs

1//! Execution state owned by the host: command nesting depth and the journal of mutating commands.
2
3use std::collections::VecDeque;
4use std::sync::Arc;
5use std::time::Duration;
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// Entries kept before the oldest are dropped.
11pub const DEFAULT_CAP: usize = 10_000;
12/// How many of the oldest entries are dropped at once when the cap is reached.
13pub const DEFAULT_DROP: usize = 1_000;
14
15/// One recorded command call.
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct JournalEntry {
18    /// Increases by one per recorded call over the journal's life (survives dropping old entries).
19    pub seq: u64,
20    pub id: String,
21    pub params: Value,
22}
23
24/// One top-level command that ran, as the chokepoint saw it: the registered id (a fixed program
25/// string, never its parameters), whether it succeeded and how long its body took.
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct CommandRun {
28    pub id: &'static str,
29    pub ok: bool,
30    pub took: Duration,
31}
32
33/// Told about every top-level command that ran (not about calls refused before running, and not
34/// about nested calls). Runs on the thread that executed the command, so it must be cheap.
35pub type RunObserver = Arc<dyn Fn(CommandRun) + Send + Sync>;
36
37/// The journal of a host: the mutating commands that succeeded, in order, with scrubbed params
38/// (replay, session logs, agent "what happened"). Also tracks the current nesting depth of
39/// [`crate::Registry::execute`].
40///
41/// A capped ring: past `cap` entries the oldest `drop` are removed in one go. By default only
42/// top-level calls (depth 0) are recorded; [`Journal::record_nested`] records nested calls too.
43#[derive(Debug, Clone)]
44pub struct Journal {
45    entries: VecDeque<JournalEntry>,
46    cap: usize,
47    drop: usize,
48    nested: bool,
49    depth: usize,
50    next_seq: u64,
51    observer: Observer,
52}
53
54/// [`Journal`]'s observer slot (the closure itself has no `Debug`).
55#[derive(Clone, Default)]
56struct Observer(Option<RunObserver>);
57
58impl std::fmt::Debug for Observer {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.write_str(if self.0.is_some() { "Observer(set)" } else { "Observer(none)" })
61    }
62}
63
64impl Default for Journal {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl Journal {
71    pub fn new() -> Self {
72        Self::with_cap(DEFAULT_CAP, DEFAULT_DROP)
73    }
74
75    /// Keep at most `cap` entries (at least 1), dropping the oldest `drop` (1..=cap) when full.
76    pub fn with_cap(cap: usize, drop: usize) -> Self {
77        let cap = cap.max(1);
78        Self { entries: VecDeque::new(), cap, drop: drop.clamp(1, cap), nested: false, depth: 0, next_seq: 0, observer: Observer::default() }
79    }
80
81    /// Also record commands run from inside other commands.
82    pub fn record_nested(mut self, yes: bool) -> Self {
83        self.nested = yes;
84        self
85    }
86
87    /// Tell `observer` about every top-level command that runs from now on (`None`: nobody).
88    /// Telemetry uses this; [`crate::Registry::execute`] calls it after [`crate::Host::after`].
89    pub fn set_observer(&mut self, observer: Option<RunObserver>) {
90        self.observer = Observer(observer);
91    }
92
93    pub fn observer(&self) -> Option<&RunObserver> {
94        self.observer.0.as_ref()
95    }
96
97    /// Current nesting depth: 0 outside any command, 1 inside a top-level command…
98    pub fn depth(&self) -> usize {
99        self.depth
100    }
101
102    /// Set the nesting depth. [`crate::Registry::execute`] does this around every command; a host
103    /// sets it only to run a command from one of its hooks as nested (see [`crate::Host`]), and
104    /// puts the old value back afterwards.
105    pub fn set_depth(&mut self, depth: usize) {
106        self.depth = depth;
107    }
108
109    /// Whether a call at `depth` is recorded.
110    pub fn records(&self, depth: usize) -> bool {
111        depth == 0 || self.nested
112    }
113
114    /// Record a call (the chokepoint does this; hosts may record extra entries).
115    pub fn push(&mut self, id: impl Into<String>, params: Value) {
116        self.entries.push_back(JournalEntry { seq: self.next_seq, id: id.into(), params });
117        self.next_seq = self.next_seq.saturating_add(1);
118        if self.entries.len() > self.cap {
119            let n = self.drop.min(self.entries.len());
120            self.entries.drain(..n);
121        }
122    }
123
124    pub fn entries(&self) -> impl DoubleEndedIterator<Item = &JournalEntry> + ExactSizeIterator {
125        self.entries.iter()
126    }
127
128    /// Entries recorded at or after sequence number `seq`.
129    pub fn since(&self, seq: u64) -> impl Iterator<Item = &JournalEntry> {
130        self.entries.iter().filter(move |e| e.seq >= seq)
131    }
132
133    pub fn last(&self) -> Option<&JournalEntry> {
134        self.entries.back()
135    }
136
137    pub fn len(&self) -> usize {
138        self.entries.len()
139    }
140
141    pub fn is_empty(&self) -> bool {
142        self.entries.is_empty()
143    }
144
145    /// The sequence number the next recorded entry gets.
146    pub fn next_seq(&self) -> u64 {
147        self.next_seq
148    }
149
150    /// Keep only the first `len` entries (drop the newest; e.g. calls undone and then replaced).
151    /// Sequence numbers already handed out are not reused.
152    pub fn truncate(&mut self, len: usize) {
153        self.entries.truncate(len);
154    }
155
156    /// Keep only the entries `keep` returns true for, in order.
157    pub fn retain(&mut self, mut keep: impl FnMut(&JournalEntry) -> bool) {
158        self.entries.retain(|e| keep(e));
159    }
160
161    /// Remove and return the newest entry.
162    pub fn pop(&mut self) -> Option<JournalEntry> {
163        self.entries.pop_back()
164    }
165
166    pub fn clear(&mut self) {
167        self.entries.clear();
168    }
169
170    /// The entries as a JSON array (`[{seq, id, params}]`).
171    pub fn to_json(&self) -> Value {
172        Value::Array(self.entries.iter().filter_map(|e| serde_json::to_value(e).ok()).collect())
173    }
174}