Skip to main content

appcore_filemaker/
patch_log.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: patch_log.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/30 05:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/30 05:00:00 by dnettoRaw
8//      ###########      S: 1.0.2-rc
9// =============================================================================
10
11//! Retains bounded undo and redo snapshots without duplicate document clones.
12
13use std::collections::VecDeque;
14use std::mem;
15use std::sync::Arc;
16
17use crate::{DocumentIr, ErrorCode, FileMakerError, Patch, PatchTransaction, Result};
18
19/// Bounded in-memory operation log providing explicit undo and redo.
20pub struct OperationLog {
21    max_entries: usize,
22    max_bytes: usize,
23    used_bytes: usize,
24    undo: VecDeque<HistoryEntry>,
25    redo: VecDeque<HistoryEntry>,
26}
27
28struct HistoryEntry {
29    document: Arc<DocumentIr>,
30    bytes: usize,
31}
32
33impl OperationLog {
34    /// Default aggregate serialized snapshot budget for undo and redo.
35    pub const DEFAULT_MAX_BYTES: usize = 64 * 1024 * 1024;
36
37    /// Creates a log with a non-zero entry bound and default byte budget.
38    pub fn new(max_entries: usize) -> Result<Self> {
39        Self::new_bounded(max_entries, Self::DEFAULT_MAX_BYTES)
40    }
41
42    /// Creates a log bounded by both entries and aggregate snapshot bytes.
43    pub fn new_bounded(max_entries: usize, max_bytes: usize) -> Result<Self> {
44        if max_entries == 0 || max_bytes == 0 {
45            return Err(FileMakerError::new(
46                ErrorCode::LimitExceeded,
47                "operation log entry and byte bounds must be non-zero",
48            ));
49        }
50        Ok(Self {
51            max_entries,
52            max_bytes,
53            used_bytes: 0,
54            undo: VecDeque::new(),
55            redo: VecDeque::new(),
56        })
57    }
58
59    /// Applies one atomic patch and records the previous document on success.
60    pub fn apply(
61        &mut self,
62        document: &mut DocumentIr,
63        patch: &Patch,
64        max_operations: usize,
65    ) -> Result<()> {
66        let bytes = crate::memory::serialized_size(document)?;
67        self.ensure_snapshot_fits(bytes)?;
68        let previous =
69            PatchTransaction::new(document, max_operations).apply_with_rollback_snapshot(patch)?;
70        self.clear_redo();
71        self.push_undo(HistoryEntry {
72            document: Arc::new(previous),
73            bytes,
74        });
75        Ok(())
76    }
77
78    /// Restores the previous successful document state.
79    pub fn undo(&mut self, document: &mut DocumentIr) -> Result<()> {
80        let current_bytes = crate::memory::serialized_size(document)?;
81        self.ensure_snapshot_fits(current_bytes)?;
82        let previous = self.undo.pop_back().ok_or_else(|| {
83            FileMakerError::new(ErrorCode::PatchInvalid, "operation log has no undo entry")
84        })?;
85        self.used_bytes = self.used_bytes.saturating_sub(previous.bytes);
86        let current = mem::replace(document, unwrap_snapshot(previous.document));
87        self.push_redo(HistoryEntry {
88            document: Arc::new(current),
89            bytes: current_bytes,
90        });
91        Ok(())
92    }
93
94    /// Reapplies the most recently undone document state.
95    pub fn redo(&mut self, document: &mut DocumentIr) -> Result<()> {
96        let current_bytes = crate::memory::serialized_size(document)?;
97        self.ensure_snapshot_fits(current_bytes)?;
98        let next = self.redo.pop_back().ok_or_else(|| {
99            FileMakerError::new(ErrorCode::PatchInvalid, "operation log has no redo entry")
100        })?;
101        self.used_bytes = self.used_bytes.saturating_sub(next.bytes);
102        let current = mem::replace(document, unwrap_snapshot(next.document));
103        self.push_undo(HistoryEntry {
104            document: Arc::new(current),
105            bytes: current_bytes,
106        });
107        Ok(())
108    }
109
110    /// Number of available undo entries.
111    #[must_use]
112    pub fn undo_len(&self) -> usize {
113        self.undo.len()
114    }
115
116    /// Number of available redo entries.
117    #[must_use]
118    pub fn redo_len(&self) -> usize {
119        self.redo.len()
120    }
121
122    /// Aggregate serialized bytes retained by undo and redo snapshots.
123    #[must_use]
124    pub const fn used_bytes(&self) -> usize {
125        self.used_bytes
126    }
127
128    /// Configured aggregate serialized snapshot byte budget.
129    #[must_use]
130    pub const fn max_bytes(&self) -> usize {
131        self.max_bytes
132    }
133
134    fn ensure_snapshot_fits(&self, bytes: usize) -> Result<()> {
135        if bytes > self.max_bytes {
136            return Err(FileMakerError::new(
137                ErrorCode::LimitExceeded,
138                "document snapshot exceeds the operation log byte budget",
139            ));
140        }
141        Ok(())
142    }
143
144    fn clear_redo(&mut self) {
145        while let Some(entry) = self.redo.pop_front() {
146            self.used_bytes = self.used_bytes.saturating_sub(entry.bytes);
147        }
148    }
149
150    fn push_undo(&mut self, entry: HistoryEntry) {
151        make_room(
152            &mut self.undo,
153            &mut self.used_bytes,
154            self.max_entries,
155            self.max_bytes,
156            entry.bytes,
157        );
158        evict_bytes(
159            &mut self.redo,
160            &mut self.used_bytes,
161            self.max_bytes,
162            entry.bytes,
163        );
164        self.used_bytes += entry.bytes;
165        self.undo.push_back(entry);
166    }
167
168    fn push_redo(&mut self, entry: HistoryEntry) {
169        make_room(
170            &mut self.redo,
171            &mut self.used_bytes,
172            self.max_entries,
173            self.max_bytes,
174            entry.bytes,
175        );
176        evict_bytes(
177            &mut self.undo,
178            &mut self.used_bytes,
179            self.max_bytes,
180            entry.bytes,
181        );
182        self.used_bytes += entry.bytes;
183        self.redo.push_back(entry);
184    }
185}
186
187fn make_room(
188    entries: &mut VecDeque<HistoryEntry>,
189    used_bytes: &mut usize,
190    max_entries: usize,
191    max_bytes: usize,
192    incoming_bytes: usize,
193) {
194    while entries.len() >= max_entries || used_bytes.saturating_add(incoming_bytes) > max_bytes {
195        let Some(entry) = entries.pop_front() else {
196            break;
197        };
198        *used_bytes = used_bytes.saturating_sub(entry.bytes);
199    }
200}
201
202fn evict_bytes(
203    entries: &mut VecDeque<HistoryEntry>,
204    used_bytes: &mut usize,
205    max_bytes: usize,
206    incoming_bytes: usize,
207) {
208    while used_bytes.saturating_add(incoming_bytes) > max_bytes {
209        let Some(entry) = entries.pop_front() else {
210            break;
211        };
212        *used_bytes = used_bytes.saturating_sub(entry.bytes);
213    }
214}
215
216fn unwrap_snapshot(snapshot: Arc<DocumentIr>) -> DocumentIr {
217    Arc::try_unwrap(snapshot).unwrap_or_else(|snapshot| (*snapshot).clone())
218}