Skip to main content

jk_core/command_history/
history.rs

1use std::collections::VecDeque;
2
3use super::{CommandRecord, CommandRecordFinish, CommandRecordStart};
4
5/// A stable command-history id for the current process.
6#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
7pub struct CommandRecordId(u64);
8
9impl CommandRecordId {
10    /// Returns the numeric id assigned by the in-memory history.
11    #[must_use]
12    pub const fn get(self) -> u64 {
13        self.0
14    }
15}
16
17/// Recent command records retained in memory.
18#[derive(Clone, Debug)]
19pub struct CommandHistory {
20    records: VecDeque<CommandRecord>,
21    limit: usize,
22    next_id: u64,
23}
24
25impl CommandHistory {
26    /// Creates an empty command history with a maximum record count.
27    #[must_use]
28    pub const fn new(limit: usize) -> Self {
29        Self {
30            records: VecDeque::new(),
31            limit,
32            next_id: 1,
33        }
34    }
35
36    /// Returns the maximum number of records retained in memory.
37    #[must_use]
38    pub const fn limit(&self) -> usize {
39        self.limit
40    }
41
42    /// Starts a command record before execution.
43    pub fn start(&mut self, input: CommandRecordStart) -> PendingCommandRecord {
44        let id = self.allocate_id();
45        let record = CommandRecord::started(id, input);
46        self.push_record(record);
47        PendingCommandRecord { id }
48    }
49
50    /// Finishes a pending command record.
51    ///
52    /// If the pending record has already been evicted by the history limit, this method does
53    /// nothing.
54    pub fn finish(&mut self, pending: &PendingCommandRecord, finish: CommandRecordFinish) -> bool {
55        if let Some(record) = self
56            .records
57            .iter_mut()
58            .find(|record| record.id == pending.id)
59        {
60            return record.finish(finish);
61        }
62        false
63    }
64
65    /// Appends an already-completed command record.
66    ///
67    /// This is useful for synchronous command paths where the caller already has all timing and
68    /// result data at the recording boundary.
69    pub fn append(
70        &mut self,
71        input: CommandRecordStart,
72        finish: CommandRecordFinish,
73    ) -> CommandRecordId {
74        let pending = self.start(input);
75        let id = pending.id;
76        self.finish(&pending, finish);
77        id
78    }
79
80    /// Returns retained records from oldest to newest.
81    #[must_use]
82    pub fn records(&self) -> impl DoubleEndedIterator<Item = &CommandRecord> {
83        self.records.iter()
84    }
85
86    const fn allocate_id(&mut self) -> CommandRecordId {
87        let id = CommandRecordId(self.next_id);
88        self.next_id += 1;
89        id
90    }
91
92    fn push_record(&mut self, record: CommandRecord) {
93        if self.limit == 0 {
94            return;
95        }
96
97        while self.records.len() >= self.limit {
98            self.records.pop_front();
99        }
100        self.records.push_back(record);
101    }
102}
103
104impl Default for CommandHistory {
105    fn default() -> Self {
106        Self::new(128)
107    }
108}
109
110/// A pending command record handle returned by [`CommandHistory::start`].
111#[derive(Debug, Eq, PartialEq)]
112pub struct PendingCommandRecord {
113    id: CommandRecordId,
114}
115
116impl PendingCommandRecord {
117    /// Returns the id of the pending command record.
118    #[must_use]
119    pub const fn id(&self) -> CommandRecordId {
120        self.id
121    }
122}