1use crate::storage::{Format, RyoStorage, StateRef, StorageResult, TxLogMode};
6use crate::txlog::{MutationRecord, TxAction, TxLog, TxLogger};
7use std::path::{Path, PathBuf};
8use std::sync::{Arc, Mutex};
9
10pub struct AutoSaveLogger {
33 inner: TxLogger,
35 mode: TxLogMode,
37 format: Format,
39 storage: Arc<Mutex<Option<RyoStorage>>>,
41 persisted: bool,
43 session_id: Option<String>,
45}
46
47impl AutoSaveLogger {
48 pub fn new(
50 project_path: impl Into<PathBuf>,
51 file_count: usize,
52 mode: TxLogMode,
53 ) -> StorageResult<Self> {
54 Self::with_format(project_path, file_count, mode, Format::default())
55 }
56
57 pub fn with_format(
59 project_path: impl Into<PathBuf>,
60 file_count: usize,
61 mode: TxLogMode,
62 format: Format,
63 ) -> StorageResult<Self> {
64 let inner = TxLogger::start(project_path, file_count);
65
66 Ok(Self {
67 inner,
68 mode,
69 format,
70 storage: Arc::new(Mutex::new(None)),
71 persisted: false,
72 session_id: None,
73 })
74 }
75
76 pub fn with_storage(
78 project_path: impl Into<PathBuf>,
79 file_count: usize,
80 mode: TxLogMode,
81 storage: RyoStorage,
82 ) -> Self {
83 let inner = TxLogger::start(project_path, file_count);
84
85 Self {
86 inner,
87 mode,
88 format: Format::default(),
89 storage: Arc::new(Mutex::new(Some(storage))),
90 persisted: false,
91 session_id: None,
92 }
93 }
94
95 pub fn format(&self) -> Format {
97 self.format
98 }
99
100 pub fn mode(&self) -> TxLogMode {
102 self.mode
103 }
104
105 pub fn set_mode(&mut self, mode: TxLogMode) {
107 self.mode = mode;
108 }
109
110 pub fn log(&self, action: TxAction) {
116 self.inner.log(action);
117 }
118
119 pub fn log_goal(&self, query: &str, intent_type: &str, confidence: f64) {
121 self.inner.log_goal(query, intent_type, confidence);
122 }
123
124 pub fn log_mutation(
129 &mut self,
130 mutation_type: &str,
131 target: &str,
132 changes: usize,
133 ) -> StorageResult<()> {
134 self.inner.log_mutation(mutation_type, target, changes);
135
136 if self.mode.persist_on_mutation() {
137 self.persist()?;
138 }
139
140 Ok(())
141 }
142
143 pub fn record_mutation<M>(&mut self, mutation: &M, changes: usize) -> StorageResult<()>
148 where
149 M: ryo_mutations::Mutation + ryo_mutations::ToSerializable,
150 {
151 self.inner.record_mutation(mutation, changes);
152
153 if self.mode.persist_on_mutation() {
154 self.persist()?;
155 }
156
157 Ok(())
158 }
159
160 pub fn record_mutation_for_file<M>(
164 &mut self,
165 mutation: &M,
166 changes: usize,
167 file_path: impl AsRef<Path>,
168 ) -> StorageResult<()>
169 where
170 M: ryo_mutations::Mutation + ryo_mutations::ToSerializable,
171 {
172 self.inner
173 .record_mutation_for_file(mutation, changes, file_path);
174
175 if self.mode.persist_on_mutation() {
176 self.persist()?;
177 }
178
179 Ok(())
180 }
181
182 pub fn record_mutation_tracked<M>(
186 &mut self,
187 mutation: &M,
188 changes: usize,
189 file_path: impl AsRef<Path>,
190 pre_state: StateRef,
191 post_state: StateRef,
192 ) -> StorageResult<()>
193 where
194 M: ryo_mutations::Mutation + ryo_mutations::ToSerializable,
195 {
196 self.inner
197 .record_mutation_tracked(mutation, changes, file_path, pre_state, post_state);
198
199 if self.mode.persist_on_mutation() {
200 self.persist()?;
201 }
202
203 Ok(())
204 }
205
206 pub fn log_mutation_with_data(
208 &mut self,
209 mutation_type: &str,
210 target: &str,
211 changes: usize,
212 data: serde_json::Value,
213 ) -> StorageResult<()> {
214 self.inner
215 .log_mutation_with_data(mutation_type, target, changes, data);
216
217 if self.mode.persist_on_mutation() {
218 self.persist()?;
219 }
220
221 Ok(())
222 }
223
224 pub fn log_mutation_batch(
226 &mut self,
227 mutations: Vec<MutationRecord>,
228 total_changes: usize,
229 ) -> StorageResult<()> {
230 self.inner.log_mutation_batch(mutations, total_changes);
231
232 if self.mode.persist_on_mutation() {
233 self.persist()?;
234 }
235
236 Ok(())
237 }
238
239 pub fn log_file_loaded(&self, path: &Path, size_bytes: usize) {
241 self.inner.log_file_loaded(path, size_bytes);
242 }
243
244 pub fn log_file_modified(&self, path: &Path, changes: usize) {
246 self.inner.log_file_modified(path, changes);
247 }
248
249 pub fn log_file_written(&self, path: &Path) {
251 self.inner.log_file_written(path);
252 }
253
254 pub fn log_compile_check(&self, success: bool, errors: Vec<String>) {
256 self.inner.log_compile_check(success, errors);
257 }
258
259 pub fn checkpoint(&self, name: &str) {
261 self.inner.checkpoint(name);
262 }
263
264 pub fn log_undo(&self, target_id: u64) {
266 self.inner.log_undo(target_id);
267 }
268
269 pub fn log_redo(&self, target_id: u64) {
271 self.inner.log_redo(target_id);
272 }
273
274 pub fn log_custom(&self, name: &str, data: serde_json::Value) {
276 self.inner.log_custom(name, data);
277 }
278
279 pub fn on_commit(&mut self) -> StorageResult<()> {
287 if self.mode.persist_on_commit() {
288 self.persist()?;
289 }
290 Ok(())
291 }
292
293 pub fn persist(&mut self) -> StorageResult<String> {
298 if !self.mode.should_persist() && self.session_id.is_none() {
299 return Ok(String::from("not-persisted"));
302 }
303
304 self.persisted = false;
312
313 Ok(self
314 .session_id
315 .clone()
316 .unwrap_or_else(|| "pending".to_string()))
317 }
318
319 pub fn finish(self) -> StorageResult<(TxLog, Option<String>)> {
323 let should_persist = self.mode.should_persist();
324 let storage_arc = Arc::clone(&self.storage);
325 let format = self.format;
326
327 let log = self.inner.finish();
329
330 let session_id = if should_persist {
332 let mut guard = storage_arc.lock().expect("autosave storage mutex poisoned");
333 if guard.is_none() {
334 let storage = RyoStorage::global()?.with_format(format);
335 storage.ensure_init()?;
336 *guard = Some(storage);
337 }
338 let storage = guard
339 .as_mut()
340 .expect("Some(storage) ensured by the is_none() init above");
341 let id = storage.dump(&log)?;
342 Some(id)
343 } else {
344 None
345 };
346
347 Ok((log, session_id))
348 }
349
350 pub fn finish_log(self) -> TxLog {
352 self.inner.finish()
353 }
354
355 pub fn elapsed_ms(&self) -> u64 {
357 self.inner.elapsed_ms()
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364 use tempfile::TempDir;
365
366 #[test]
367 fn test_autosave_off_mode() {
368 let logger = AutoSaveLogger::new("/test/project", 10, TxLogMode::Off).unwrap();
369
370 logger.log_goal("test", "test", 0.9);
371
372 let (log, session_id) = logger.finish().unwrap();
373 assert!(!log.is_empty());
374 assert!(session_id.is_none());
375 }
376
377 #[test]
378 fn test_autosave_memory_mode() {
379 let logger = AutoSaveLogger::new("/test/project", 10, TxLogMode::Memory).unwrap();
380
381 logger.log_goal("test", "test", 0.9);
382
383 let (log, session_id) = logger.finish().unwrap();
384 assert!(!log.is_empty());
385 assert!(session_id.is_none());
386 }
387
388 #[test]
389 fn test_autosave_on_commit_mode() {
390 let temp = TempDir::new().unwrap();
391 let storage = RyoStorage::new(temp.path().join(".ryo")).unwrap();
392
393 let mut logger =
394 AutoSaveLogger::with_storage("/test/project", 10, TxLogMode::OnCommit, storage);
395
396 logger.log_goal("test", "test", 0.9);
397 logger.on_commit().unwrap();
398
399 let (log, session_id) = logger.finish().unwrap();
400 assert!(!log.is_empty());
401 assert!(session_id.is_some());
402 }
403
404 #[test]
405 fn test_autosave_on_mutation_mode() {
406 let temp = TempDir::new().unwrap();
407 let storage = RyoStorage::new(temp.path().join(".ryo")).unwrap();
408
409 let mut logger =
410 AutoSaveLogger::with_storage("/test/project", 10, TxLogMode::OnMutation, storage);
411
412 logger.log_mutation("Rename", "foo -> bar", 3).unwrap();
413
414 let (log, session_id) = logger.finish().unwrap();
415 assert!(!log.is_empty());
416 assert!(session_id.is_some());
417 }
418}