use crate::storage::{Format, RyoStorage, StateRef, StorageResult, TxLogMode};
use crate::txlog::{MutationRecord, TxAction, TxLog, TxLogger};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
pub struct AutoSaveLogger {
inner: TxLogger,
mode: TxLogMode,
format: Format,
storage: Arc<Mutex<Option<RyoStorage>>>,
persisted: bool,
session_id: Option<String>,
}
impl AutoSaveLogger {
pub fn new(
project_path: impl Into<PathBuf>,
file_count: usize,
mode: TxLogMode,
) -> StorageResult<Self> {
Self::with_format(project_path, file_count, mode, Format::default())
}
pub fn with_format(
project_path: impl Into<PathBuf>,
file_count: usize,
mode: TxLogMode,
format: Format,
) -> StorageResult<Self> {
let inner = TxLogger::start(project_path, file_count);
Ok(Self {
inner,
mode,
format,
storage: Arc::new(Mutex::new(None)),
persisted: false,
session_id: None,
})
}
pub fn with_storage(
project_path: impl Into<PathBuf>,
file_count: usize,
mode: TxLogMode,
storage: RyoStorage,
) -> Self {
let inner = TxLogger::start(project_path, file_count);
Self {
inner,
mode,
format: Format::default(),
storage: Arc::new(Mutex::new(Some(storage))),
persisted: false,
session_id: None,
}
}
pub fn format(&self) -> Format {
self.format
}
pub fn mode(&self) -> TxLogMode {
self.mode
}
pub fn set_mode(&mut self, mode: TxLogMode) {
self.mode = mode;
}
pub fn log(&self, action: TxAction) {
self.inner.log(action);
}
pub fn log_goal(&self, query: &str, intent_type: &str, confidence: f64) {
self.inner.log_goal(query, intent_type, confidence);
}
pub fn log_mutation(
&mut self,
mutation_type: &str,
target: &str,
changes: usize,
) -> StorageResult<()> {
self.inner.log_mutation(mutation_type, target, changes);
if self.mode.persist_on_mutation() {
self.persist()?;
}
Ok(())
}
pub fn record_mutation<M>(&mut self, mutation: &M, changes: usize) -> StorageResult<()>
where
M: ryo_mutations::Mutation + ryo_mutations::ToSerializable,
{
self.inner.record_mutation(mutation, changes);
if self.mode.persist_on_mutation() {
self.persist()?;
}
Ok(())
}
pub fn record_mutation_for_file<M>(
&mut self,
mutation: &M,
changes: usize,
file_path: impl AsRef<Path>,
) -> StorageResult<()>
where
M: ryo_mutations::Mutation + ryo_mutations::ToSerializable,
{
self.inner
.record_mutation_for_file(mutation, changes, file_path);
if self.mode.persist_on_mutation() {
self.persist()?;
}
Ok(())
}
pub fn record_mutation_tracked<M>(
&mut self,
mutation: &M,
changes: usize,
file_path: impl AsRef<Path>,
pre_state: StateRef,
post_state: StateRef,
) -> StorageResult<()>
where
M: ryo_mutations::Mutation + ryo_mutations::ToSerializable,
{
self.inner
.record_mutation_tracked(mutation, changes, file_path, pre_state, post_state);
if self.mode.persist_on_mutation() {
self.persist()?;
}
Ok(())
}
pub fn log_mutation_with_data(
&mut self,
mutation_type: &str,
target: &str,
changes: usize,
data: serde_json::Value,
) -> StorageResult<()> {
self.inner
.log_mutation_with_data(mutation_type, target, changes, data);
if self.mode.persist_on_mutation() {
self.persist()?;
}
Ok(())
}
pub fn log_mutation_batch(
&mut self,
mutations: Vec<MutationRecord>,
total_changes: usize,
) -> StorageResult<()> {
self.inner.log_mutation_batch(mutations, total_changes);
if self.mode.persist_on_mutation() {
self.persist()?;
}
Ok(())
}
pub fn log_file_loaded(&self, path: &Path, size_bytes: usize) {
self.inner.log_file_loaded(path, size_bytes);
}
pub fn log_file_modified(&self, path: &Path, changes: usize) {
self.inner.log_file_modified(path, changes);
}
pub fn log_file_written(&self, path: &Path) {
self.inner.log_file_written(path);
}
pub fn log_compile_check(&self, success: bool, errors: Vec<String>) {
self.inner.log_compile_check(success, errors);
}
pub fn checkpoint(&self, name: &str) {
self.inner.checkpoint(name);
}
pub fn log_undo(&self, target_id: u64) {
self.inner.log_undo(target_id);
}
pub fn log_redo(&self, target_id: u64) {
self.inner.log_redo(target_id);
}
pub fn log_custom(&self, name: &str, data: serde_json::Value) {
self.inner.log_custom(name, data);
}
pub fn on_commit(&mut self) -> StorageResult<()> {
if self.mode.persist_on_commit() {
self.persist()?;
}
Ok(())
}
pub fn persist(&mut self) -> StorageResult<String> {
if !self.mode.should_persist() && self.session_id.is_none() {
return Ok(String::from("not-persisted"));
}
self.persisted = false;
Ok(self
.session_id
.clone()
.unwrap_or_else(|| "pending".to_string()))
}
pub fn finish(self) -> StorageResult<(TxLog, Option<String>)> {
let should_persist = self.mode.should_persist();
let storage_arc = Arc::clone(&self.storage);
let format = self.format;
let log = self.inner.finish();
let session_id = if should_persist {
let mut guard = storage_arc.lock().expect("autosave storage mutex poisoned");
if guard.is_none() {
let storage = RyoStorage::global()?.with_format(format);
storage.ensure_init()?;
*guard = Some(storage);
}
let storage = guard
.as_mut()
.expect("Some(storage) ensured by the is_none() init above");
let id = storage.dump(&log)?;
Some(id)
} else {
None
};
Ok((log, session_id))
}
pub fn finish_log(self) -> TxLog {
self.inner.finish()
}
pub fn elapsed_ms(&self) -> u64 {
self.inner.elapsed_ms()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_autosave_off_mode() {
let logger = AutoSaveLogger::new("/test/project", 10, TxLogMode::Off).unwrap();
logger.log_goal("test", "test", 0.9);
let (log, session_id) = logger.finish().unwrap();
assert!(!log.is_empty());
assert!(session_id.is_none());
}
#[test]
fn test_autosave_memory_mode() {
let logger = AutoSaveLogger::new("/test/project", 10, TxLogMode::Memory).unwrap();
logger.log_goal("test", "test", 0.9);
let (log, session_id) = logger.finish().unwrap();
assert!(!log.is_empty());
assert!(session_id.is_none());
}
#[test]
fn test_autosave_on_commit_mode() {
let temp = TempDir::new().unwrap();
let storage = RyoStorage::new(temp.path().join(".ryo")).unwrap();
let mut logger =
AutoSaveLogger::with_storage("/test/project", 10, TxLogMode::OnCommit, storage);
logger.log_goal("test", "test", 0.9);
logger.on_commit().unwrap();
let (log, session_id) = logger.finish().unwrap();
assert!(!log.is_empty());
assert!(session_id.is_some());
}
#[test]
fn test_autosave_on_mutation_mode() {
let temp = TempDir::new().unwrap();
let storage = RyoStorage::new(temp.path().join(".ryo")).unwrap();
let mut logger =
AutoSaveLogger::with_storage("/test/project", 10, TxLogMode::OnMutation, storage);
logger.log_mutation("Rename", "foo -> bar", 3).unwrap();
let (log, session_id) = logger.finish().unwrap();
assert!(!log.is_empty());
assert!(session_id.is_some());
}
}