appcore_filemaker/
patch_log.rs1use std::collections::VecDeque;
14use std::mem;
15use std::sync::Arc;
16
17use crate::{DocumentIr, ErrorCode, FileMakerError, Patch, PatchTransaction, Result};
18
19pub 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 pub const DEFAULT_MAX_BYTES: usize = 64 * 1024 * 1024;
36
37 pub fn new(max_entries: usize) -> Result<Self> {
39 Self::new_bounded(max_entries, Self::DEFAULT_MAX_BYTES)
40 }
41
42 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 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 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 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 #[must_use]
112 pub fn undo_len(&self) -> usize {
113 self.undo.len()
114 }
115
116 #[must_use]
118 pub fn redo_len(&self) -> usize {
119 self.redo.len()
120 }
121
122 #[must_use]
124 pub const fn used_bytes(&self) -> usize {
125 self.used_bytes
126 }
127
128 #[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}