leyline/core/device/
autosave.rs1use std::path::{Path, PathBuf};
2use std::sync::{Arc, Mutex};
3use std::time::Duration;
4
5use crate::core::error::Result;
6use crate::core::{Session, SessionState, Tab};
7use crate::util::autosave::{Autosave, SaveJob, SaveTarget, Schedule};
8use crate::util::lock;
9
10use super::Device;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13#[non_exhaustive]
14pub struct DeviceAutosaveOptions {
15 pub interval: Duration,
16 pub state_interval: Duration,
17}
18
19impl DeviceAutosaveOptions {
20 #[must_use]
21 pub const fn new(interval: Duration) -> Self {
22 Self {
23 interval,
24 state_interval: Duration::from_secs(300),
25 }
26 }
27
28 #[must_use]
29 pub const fn state_interval(mut self, state_interval: Duration) -> Self {
30 self.state_interval = state_interval;
31 self
32 }
33}
34
35impl From<Duration> for DeviceAutosaveOptions {
36 fn from(interval: Duration) -> Self {
37 Self::new(interval)
38 }
39}
40
41pub struct DeviceAutosave {
42 path: Arc<Path>,
43 device: Arc<Mutex<Device>>,
44 tab: Arc<Mutex<Option<Tab>>>,
45 autosave: Autosave,
46}
47
48struct DeviceTarget {
49 device: Arc<Mutex<Device>>,
50 tab: Arc<Mutex<Option<Tab>>>,
51 session: Session,
52 path: Arc<Path>,
53 saved: Option<u64>,
54 writing: Option<SessionState>,
55 saved_state: Option<SessionState>,
56}
57
58impl Device {
59 pub fn autosave(
60 &self,
61 session: &Session,
62 path: impl Into<PathBuf>,
63 options: impl Into<DeviceAutosaveOptions>,
64 ) -> DeviceAutosave {
65 let options = options.into();
66 let path: Arc<Path> = Arc::from(path.into());
67 let device = Arc::new(Mutex::new(self.clone()));
68 let tab = Arc::new(Mutex::new(None));
69 let target = DeviceTarget {
70 device: Arc::clone(&device),
71 tab: Arc::clone(&tab),
72 session: session.clone(),
73 path: Arc::clone(&path),
74 saved: None,
75 writing: None,
76 saved_state: None,
77 };
78 let schedule = Schedule {
79 interval: options.interval,
80 periodic: Some(options.state_interval),
81 label: "device",
82 };
83 DeviceAutosave {
84 path,
85 device,
86 tab,
87 autosave: Autosave::spawn(target, session.cookies().changes(), schedule),
88 }
89 }
90}
91
92impl DeviceAutosave {
93 pub fn update(&self, change: impl FnOnce(&mut Device)) {
94 change(&mut lock(&self.device));
95 self.autosave.touch();
96 }
97
98 pub fn track(&self, tab: &Tab) {
99 *lock(&self.tab) = Some(tab.clone());
100 self.autosave.touch();
101 }
102
103 #[must_use]
104 pub fn device(&self) -> Device {
105 snapshot(&self.device, &self.tab)
106 }
107
108 pub async fn flush(&self) -> Result<()> {
109 self.autosave.flush().await
110 }
111
112 pub async fn shutdown(self) -> Result<()> {
113 self.autosave.shutdown().await
114 }
115}
116
117impl std::fmt::Debug for DeviceAutosave {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.debug_struct("DeviceAutosave")
120 .field("path", &self.path)
121 .finish_non_exhaustive()
122 }
123}
124
125impl SaveTarget for DeviceTarget {
126 fn prepare(&mut self) -> Option<SaveJob> {
127 let live = self.session.cookies();
128 let generation = *live.changes().borrow();
129 let jar = live.snapshot();
130 let jar_dirty = self.saved != Some(generation);
131 let mut device = snapshot(&self.device, &self.tab);
132 device.state = self.session.state();
133 self.writing = Some(device.state.clone());
134 let jar_path = device.jar_path.clone();
135 device.jar = jar_path.is_none().then(|| jar.clone());
136 let path = Arc::clone(&self.path);
137 Some(SaveJob {
138 generation,
139 write: Box::new(move || {
140 if let Some(jar_path) = jar_path.filter(|_| jar_dirty) {
141 jar.save_to(jar_path)?;
142 }
143 device.save_to(&*path)
144 }),
145 })
146 }
147
148 fn saved(&mut self, generation: u64) {
149 self.saved = Some(generation);
150 self.saved_state = self.writing.take();
151 }
152
153 fn unsaved(&self) -> bool {
154 self.saved_state.as_ref() != Some(&self.session.state())
155 }
156}
157
158fn snapshot(device: &Mutex<Device>, tab: &Mutex<Option<Tab>>) -> Device {
159 let mut device = lock(device).clone();
160 if let Some(tab) = lock(tab).as_ref() {
161 device.page = tab.current();
162 }
163 device
164}