Skip to main content

osc7501_store/
change.rs

1use crate::{
2    Error, MAX_RECORDS, Record, Store,
3    parser::{AppId, MAX_APP, Report},
4};
5
6/// A change to a current record or its inherited app.
7///
8/// Unchanged reports emit no change, even though they refresh eviction order.
9/// An ancestor update can change a child's effective app without a child report.
10/// Keep reports and lifecycle events to reproduce the store's full history.
11///
12/// Views borrow the store only for the callback. Copy any data you need to retain.
13#[derive(Clone, Copy, Debug)]
14#[non_exhaustive]
15pub enum Change<'a> {
16    /// A new record, changed explicit fields, or a changed effective app.
17    Set {
18        /// The record after the complete store operation.
19        record: Record<'a>,
20        /// The app resolved through the record and its remaining ancestors.
21        effective_app: Option<AppId<'a>>,
22    },
23    /// An old record, delivered before its removal.
24    Remove(Record<'a>),
25}
26
27#[derive(Clone, Copy, Eq, PartialEq)]
28struct AppSnapshot {
29    // App ids cannot contain NUL (0x00). Zero-padding lets us compare the full
30    // buffers without storing each id's length.
31    bytes: [u8; MAX_APP],
32}
33
34impl AppSnapshot {
35    fn new(app: AppId<'_>) -> Self {
36        let value = app.as_str().as_bytes();
37        let mut snapshot = Self {
38            bytes: [0; MAX_APP],
39        };
40        snapshot.bytes[..value.len()].copy_from_slice(value);
41        snapshot
42    }
43
44    fn matches(&self, app: AppId<'_>) -> bool {
45        self == &Self::new(app)
46    }
47}
48
49impl Store<'_> {
50    /// Applies a report and delivers all resulting record and app changes.
51    ///
52    /// Removals arrive before each record is removed. Sets arrive after the
53    /// complete operation, once per new or changed surviving record. Their
54    /// effective apps include ancestor replacement and eviction effects.
55    /// Order within removals or sets is unspecified; all removals precede sets.
56    ///
57    /// Equal decoded fields and effective apps produce no set notification.
58    /// The report still refreshes update order, just as [`apply`](Self::apply) does.
59    /// Does not allocate. Change detection copies effective apps into bounded
60    /// scratch storage instead of copying complete records.
61    ///
62    /// # Errors
63    /// Returns [`Error::Parser`] if field preparation fails, before any callback
64    /// or mutation. Callbacks are infallible and their effects are not rolled back.
65    pub fn apply_with_changes(
66        &mut self,
67        report: Report<'_>,
68        mut on_change: impl FnMut(Change<'_>),
69    ) -> Result<(), Error> {
70        let mut apps = [None; MAX_RECORDS];
71        self.snapshot_apps(&mut apps);
72        let changed = self.apply_inner(report, |record| on_change(Change::Remove(record)), true)?;
73        self.notify_sets(&apps, changed, &mut on_change);
74        Ok(())
75    }
76
77    /// Removes active records and reports inherited-app changes on survivors.
78    ///
79    /// Uses [`on_prompt`](Self::on_prompt) retention rules. All removals precede
80    /// sets, which contain the final records and effective apps. Does not allocate.
81    pub fn on_prompt_with_changes(&mut self, mut on_change: impl FnMut(Change<'_>)) {
82        let mut apps = [None; MAX_RECORDS];
83        self.snapshot_apps(&mut apps);
84        self.remove_active(&mut |record| on_change(Change::Remove(record)));
85        self.notify_sets(&apps, None, &mut on_change);
86    }
87
88    /// Applies prompt cleanup and change reporting when the attached process exits.
89    pub fn on_process_exit_with_changes(&mut self, on_change: impl FnMut(Change<'_>)) {
90        self.on_prompt_with_changes(on_change);
91    }
92
93    /// Removes every record on a full terminal reset, delivering only removals.
94    pub fn on_full_reset_with_changes(&mut self, mut on_change: impl FnMut(Change<'_>)) {
95        self.remove_matching(|_| true, &mut |record| on_change(Change::Remove(record)));
96    }
97
98    fn snapshot_apps(&self, apps: &mut [Option<AppSnapshot>; MAX_RECORDS]) {
99        for (index, slot) in self.slots.iter().enumerate() {
100            apps[index] = slot
101                .record
102                .as_ref()
103                .and_then(|record| self.effective_app(record.view().id()))
104                .map(AppSnapshot::new);
105        }
106    }
107
108    fn notify_sets(
109        &self,
110        apps: &[Option<AppSnapshot>; MAX_RECORDS],
111        changed: Option<usize>,
112        on_change: &mut impl FnMut(Change<'_>),
113    ) {
114        // Slots do not move; changed also marks a reused slot.
115        for (index, slot) in self.slots.iter().enumerate() {
116            let Some(record) = slot.record.as_ref().map(super::record::StoredRecord::view) else {
117                continue;
118            };
119            let effective_app = self.effective_app(record.id());
120            let same_app = match (&apps[index], effective_app) {
121                (Some(previous), Some(app)) => previous.matches(app),
122                (None, None) => true,
123                _ => false,
124            };
125            if changed == Some(index) || !same_app {
126                on_change(Change::Set {
127                    record,
128                    effective_app,
129                });
130            }
131        }
132    }
133}