Skip to main content

egui_charts/ui/dialogs/templates/
store.rs

1//! Sequential storage requests keep a late save or delete from racing another edit.
2
3use crate::data::FeedError;
4use crate::data::inbox::Inbox;
5use crate::services::{Storage, StoreKind};
6use serde::{Serialize, de::DeserializeOwned};
7use std::collections::{BTreeMap, VecDeque};
8use std::sync::Arc;
9
10#[derive(serde::Serialize, serde::Deserialize)]
11struct Document<T> {
12    version: u32,
13    template: T,
14}
15
16enum Pending {
17    List,
18    Load(String),
19    Save,
20    Delete(String),
21}
22
23/// Named, host-readable templates. Without a storage service they live in memory. With a
24/// service attached, poll every frame, including while the template UI is closed. A failed
25/// read or an unknown document version disables writes until a successful reload, preventing
26/// accidental replacement of documents that could not be understood.
27pub struct TemplateStore<T> {
28    entries: BTreeMap<String, T>,
29    kind: StoreKind,
30    storage: Option<Arc<dyn Storage>>,
31    inbox: Arc<Inbox>,
32    pending: Option<Pending>,
33    remaining: VecDeque<String>,
34    loaded: bool,
35    /// Last storage or document error. Failed saves retain the edited template in memory.
36    pub error: Option<String>,
37}
38
39impl<T: Clone + Serialize + DeserializeOwned> TemplateStore<T> {
40    /// An empty catalog in the supplied namespace, initially kept only in memory.
41    pub fn new(kind: StoreKind) -> Self {
42        Self {
43            entries: BTreeMap::new(),
44            kind,
45            storage: None,
46            inbox: Arc::default(),
47            pending: None,
48            remaining: VecDeque::new(),
49            loaded: true,
50            error: None,
51        }
52    }
53
54    /// Read the catalog, sorted by name, for a host-provided menu or export.
55    pub fn entries(&self) -> &BTreeMap<String, T> {
56        &self.entries
57    }
58
59    /// Whether saving, loading or deleting can begin. Reads must finish before writes start.
60    pub fn ready(&self) -> bool {
61        self.loaded && self.pending.is_none()
62    }
63
64    /// Attach the host storage service and read its catalog. No document is written on attach.
65    pub fn set_storage(&mut self, storage: Arc<dyn Storage>) {
66        self.storage = Some(storage);
67        self.pending = None;
68        self.reload();
69    }
70
71    /// Retry reading the catalog after a failed read. While a request is outstanding this does
72    /// nothing, so reloading cannot race a write or discard an unsaved edit.
73    pub fn reload(&mut self) {
74        if self.pending.is_some() {
75            return;
76        }
77        if let Some(storage) = &self.storage {
78            self.entries.clear();
79            self.remaining.clear();
80            self.inbox = Arc::default();
81            self.loaded = false;
82            self.error = None;
83            self.pending = Some(Pending::List);
84            storage.list(self.kind, self.inbox.reply(0));
85        }
86    }
87
88    /// Receive storage answers and wake the UI when the next asynchronous answer arrives.
89    pub fn poll(&mut self, ctx: &egui::Context) {
90        let repaint = ctx.clone();
91        self.inbox.set_waker(move || repaint.request_repaint());
92        for (_, answer) in self.inbox.drain().answers {
93            let Some(pending) = self.pending.take() else {
94                continue;
95            };
96            let result = match pending {
97                Pending::List => {
98                    let result = *answer
99                        .downcast::<Result<Vec<String>, FeedError>>()
100                        .expect("template list");
101                    result
102                        .map(|names| {
103                            self.remaining = names.into_iter().collect();
104                        })
105                        .map_err(|error| error.to_string())
106                }
107                Pending::Load(name) => {
108                    let result = *answer
109                        .downcast::<Result<Option<String>, FeedError>>()
110                        .expect("template load");
111                    result
112                        .map_err(|error| error.to_string())
113                        .and_then(|document| {
114                            if let Some(document) = document {
115                                let document: Document<T> = serde_json::from_str(&document)
116                                    .map_err(|error| error.to_string())?;
117                                if document.version != 1 {
118                                    return Err("Unsupported template version".into());
119                                }
120                                self.entries.insert(name, document.template);
121                            }
122                            Ok(())
123                        })
124                }
125                Pending::Save | Pending::Delete(_) => {
126                    let result = *answer
127                        .downcast::<Result<(), FeedError>>()
128                        .expect("template write");
129                    result
130                        .map(|()| {
131                            if let Pending::Delete(name) = pending {
132                                self.entries.remove(&name);
133                            }
134                        })
135                        .map_err(|error| error.to_string())
136                }
137            };
138            if let Err(error) = result {
139                self.error = Some(error);
140                self.remaining.clear();
141            } else if !self.loaded {
142                if let Some(name) = self.remaining.pop_front() {
143                    self.storage.as_ref().expect("template storage").load(
144                        self.kind,
145                        &name,
146                        self.inbox.reply(0),
147                    );
148                    self.pending = Some(Pending::Load(name));
149                } else {
150                    self.loaded = true;
151                }
152            }
153        }
154    }
155
156    /// Save or explicitly replace a named template. Blank names and concurrent writes are rejected.
157    pub fn save(&mut self, name: &str, template: T) -> Result<(), String> {
158        if !self.ready() {
159            return Err("Templates are not ready".into());
160        }
161        let name = name.trim();
162        if name.is_empty() {
163            return Err("Enter a template name".into());
164        }
165        let document = serde_json::to_string(&Document {
166            version: 1,
167            template: template.clone(),
168        })
169        .map_err(|error| error.to_string())?;
170        self.entries.insert(name.into(), template);
171        self.error = None;
172        if let Some(storage) = &self.storage {
173            self.pending = Some(Pending::Save);
174            storage.save(self.kind, name, document, self.inbox.reply(0));
175        }
176        Ok(())
177    }
178
179    /// Delete a named template. With storage attached the row remains until deletion succeeds.
180    pub fn delete(&mut self, name: &str) -> Result<(), String> {
181        if !self.ready() {
182            return Err("Templates are not ready".into());
183        }
184        self.error = None;
185        if let Some(storage) = &self.storage {
186            self.pending = Some(Pending::Delete(name.into()));
187            storage.delete(self.kind, name, self.inbox.reply(0));
188        } else {
189            self.entries.remove(name);
190        }
191        Ok(())
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::data::Reply;
199    use std::sync::Mutex;
200
201    #[derive(Default)]
202    struct Host {
203        documents: Mutex<BTreeMap<String, String>>,
204        fail_write: Mutex<bool>,
205    }
206
207    impl Storage for Host {
208        fn list(&self, _: StoreKind, reply: Reply<Vec<String>>) {
209            reply.send(Ok(self.documents.lock().unwrap().keys().cloned().collect()));
210        }
211        fn load(&self, _: StoreKind, name: &str, reply: Reply<Option<String>>) {
212            reply.send(Ok(self.documents.lock().unwrap().get(name).cloned()));
213        }
214        fn save(&self, _: StoreKind, name: &str, document: String, reply: Reply<()>) {
215            if *self.fail_write.lock().unwrap() {
216                reply.send(Err(FeedError::NotSupported));
217            } else {
218                self.documents.lock().unwrap().insert(name.into(), document);
219                reply.send(Ok(()));
220            }
221        }
222        fn delete(&self, _: StoreKind, name: &str, reply: Reply<()>) {
223            if *self.fail_write.lock().unwrap() {
224                reply.send(Err(FeedError::NotSupported));
225            } else {
226                self.documents.lock().unwrap().remove(name);
227                reply.send(Ok(()));
228            }
229        }
230    }
231
232    #[test]
233    fn named_templates_survive_restart_and_failed_or_unknown_documents_are_not_lost() {
234        let ctx = egui::Context::default();
235        let host = Arc::new(Host::default());
236        let mut store = TemplateStore::<Vec<u32>>::new(StoreKind::ChartTemplates);
237        store.set_storage(host.clone());
238        assert!(
239            store.save("Example", vec![7]).is_err(),
240            "wait for the initial read"
241        );
242        store.poll(&ctx);
243        assert!(store.save("  ", vec![7]).is_err());
244        store.save("  Example  ", vec![7]).unwrap();
245        assert!(!store.ready(), "one write per document at a time");
246        store.reload();
247        assert_eq!(
248            store.entries()["Example"],
249            vec![7],
250            "reloading cannot discard an outstanding save"
251        );
252        store.poll(&ctx);
253        let mut restored = TemplateStore::<Vec<u32>>::new(StoreKind::ChartTemplates);
254        restored.set_storage(host.clone());
255        restored.poll(&ctx);
256        restored.poll(&ctx);
257        assert_eq!(restored.entries()["Example"], vec![7]);
258        *host.fail_write.lock().unwrap() = true;
259        restored.save("Example", vec![11]).unwrap();
260        restored.poll(&ctx);
261        assert!(restored.error.is_some());
262        assert_eq!(
263            restored.entries()["Example"],
264            vec![11],
265            "retain edits when save fails"
266        );
267        restored.delete("Example").unwrap();
268        restored.poll(&ctx);
269        assert!(
270            restored.entries().contains_key("Example"),
271            "failed delete retains the row"
272        );
273        *host.fail_write.lock().unwrap() = false;
274        restored.save("Example", vec![11]).unwrap();
275        restored.poll(&ctx);
276        restored.delete("Example").unwrap();
277        restored.poll(&ctx);
278        assert!(restored.entries().is_empty());
279        assert!(host.documents.lock().unwrap().is_empty());
280        for document in [r#"{"version":2,"template":[7]}"#, "invalid json"] {
281            host.documents
282                .lock()
283                .unwrap()
284                .insert("Future".into(), document.into());
285            restored.reload();
286            restored.poll(&ctx);
287            restored.poll(&ctx);
288            assert!(!restored.ready());
289            assert!(restored.error.is_some());
290            assert!(restored.save("Future", vec![9]).is_err());
291            assert!(restored.delete("Future").is_err());
292            assert_eq!(host.documents.lock().unwrap()["Future"], document);
293        }
294    }
295}