Files
OneCAD-Simulation/crates/engine/src/autosave.rs
T
jyvikoandClaude Opus 5.5 d5bd74570f Rename the project to FieldCraft
Crates fieldcraft-*, binaries fieldcraft and fieldcraft-cli, the .fieldcraft file (format tag
fieldcraft/1, manifest fieldcraft.json), FIELDCRAFT_* environment variables, the MCP server
name, egui widget ids (fc_*), the app icon document FieldCraft.icon and its generated family,
docs and licences. No compatibility shim: no files of the earlier name exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-13 23:51:40 -04:00

108 lines
4.0 KiB
Rust

//! Autosave for recovery after a crash.
//!
//! The desktop app keeps an [`Autosaver`] and calls [`Autosaver::save`] on a timer: while the
//! session has unsaved changes not yet autosaved, the whole model is written to the app's entry in
//! the recovery folder ([`fieldcraft_io::recovery`]); once nothing is unsaved the entry goes. After
//! a crash the next launch lists the entry (`file.recovery.list`) and opens it (`file.recover`).
use std::path::Path;
use fieldcraft_io::recovery::Slot;
use crate::{Result, Session};
pub use fieldcraft_io::recovery::default_dir;
/// One app's autosaver.
#[derive(Debug)]
pub struct Autosaver {
slot: Slot,
/// The revision last autosaved.
saved: Option<u64>,
}
impl Autosaver {
/// Start autosaving into `dir` (created if needed).
pub fn new(dir: &Path) -> Result<Autosaver> {
Ok(Autosaver { slot: Slot::new(dir)?, saved: None })
}
/// The id of this app's entry.
pub fn id(&self) -> &str {
self.slot.id()
}
/// Autosave if the session has unsaved changes not written yet, or drop the entry when
/// nothing is unsaved. Returns whether it wrote.
pub fn save(&mut self, s: &Session) -> Result<bool> {
if !s.is_dirty() {
self.slot.clear();
self.saved = None;
return Ok(false);
}
if self.saved == Some(s.revision()) {
return Ok(false);
}
// Geometry paths stay absolute: the entry is not next to the model's own file.
let bytes = s.file_bytes(None)?;
self.slot.write(&bytes, s.path.as_deref(), s.model.nodes().len(), s.model.elements().len())?;
self.saved = Some(s.revision());
Ok(true)
}
/// The app is closing: a last autosave, then the lock is released. Unsaved work stays in the
/// recovery folder for the next launch to offer.
pub fn close(mut self, s: &Session) -> Result<()> {
let saved = self.save(s);
self.slot.close();
saved.map(|_| ())
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn autosaves_unsaved_work_and_recovers_it_after_a_crash() {
let dir = std::env::temp_dir().join(format!("fieldcraft-autosave-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let mut s = Session::default();
let mut a = Autosaver::new(&dir).unwrap();
assert!(!a.save(&s).unwrap(), "nothing unsaved");
s.execute("mesh.node.add", &json!({"label": 1, "xyz": [1, 2, 3]})).unwrap();
assert!(a.save(&s).unwrap());
assert!(!a.save(&s).unwrap(), "unchanged since the last autosave");
let id = a.id().to_string();
let listed = s.execute("file.recovery.list", &json!({"dir": dir})).unwrap();
assert_eq!(listed, json!([]), "a running app's autosave is not offered");
// The app dies.
drop(a);
let listed = s.execute("file.recovery.list", &json!({"dir": dir})).unwrap();
assert_eq!(listed[0]["id"], id);
assert_eq!(listed[0]["nodes"], 1);
let mut next = Session::default();
let r = next.execute("file.recover", &json!({"id": id, "dir": dir})).unwrap();
assert_eq!(r["nodes"], 1);
assert_eq!(next.model, s.model);
assert_eq!(next.journal, s.journal);
assert!(next.is_dirty(), "recovered work is unsaved");
next.execute("file.recovery.discard", &json!({"all": true, "dir": dir})).unwrap();
assert_eq!(next.execute("file.recovery.list", &json!({"dir": dir})).unwrap(), json!([]));
// Undoing back to the saved state drops the entry; a clean close leaves nothing.
let mut a = Autosaver::new(&dir).unwrap();
let mut s = Session::default();
s.execute("mesh.node.add", &json!({"label": 1, "xyz": [1, 2, 3]})).unwrap();
assert!(a.save(&s).unwrap());
s.execute("edit.undo", &json!({})).unwrap();
assert!(!a.save(&s).unwrap());
a.close(&s).unwrap();
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 0);
let _ = std::fs::remove_dir_all(&dir);
}
}