egui_charts/services/storage.rs
1//! Named text documents kept by the host on behalf of the library.
2
3use super::Events;
4use crate::data::{FeedError, MaybeSend, Reply};
5
6/// What a stored document holds. Each kind is its own namespace of names.
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
8#[non_exhaustive]
9pub enum StoreKind {
10 /// The user's settings: theme, last symbol and interval, drawing toolbar favourites and the
11 /// open layout. One document, named `settings`.
12 Settings,
13 /// The drawings of one symbol, one document per symbol, named by the symbol.
14 Drawings,
15 /// Named alert presets in a versioned JSON document.
16 AlertPresets,
17 /// Named chart display settings templates.
18 ChartTemplates,
19 /// Named sets of study inputs and presentation settings.
20 StudyTemplates,
21 /// Named drawing property templates, without chart anchors.
22 DrawingTemplates,
23 /// The user's notes on one symbol, one document per symbol, named by the symbol.
24 SymbolNotes,
25 /// Named chart layouts: the arrangement of the charts and the symbol, interval, chart type,
26 /// studies, panes, scales, settings and drawings of each.
27 Layouts,
28 /// Recently opened instruments, newest first.
29 RecentSymbols,
30 /// Named screener screens: the universe, column set, filters, sort order and description.
31 Screens,
32}
33
34/// The host's revision of a stored document, such as a version counter, an entity tag or a
35/// content hash. The library compares revisions for equality only and hands them back unchanged.
36#[derive(Clone, Debug, PartialEq, Eq, Hash)]
37pub struct Revision(
38 /// The host's own token, different for every stored version of a document.
39 pub String,
40);
41
42/// A stored document with the revision it was read at.
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct Versioned {
45 /// The document as stored.
46 pub document: String,
47 /// Its revision.
48 pub revision: Revision,
49}
50
51/// The host's answer to [`Storage::save_versioned`].
52#[derive(Clone, Debug, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum SaveOutcome {
55 /// The document was written, at this revision.
56 Saved(Revision),
57 /// Nothing was written because the stored document is not at the expected revision. Carries
58 /// the stored version so that the user can load it, or `None` if it was deleted meanwhile.
59 Conflict(Option<Versioned>),
60}
61
62/// How the user settles a refused save. Until one is chosen the unsaved document stays in memory
63/// and nothing is written.
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65#[non_exhaustive]
66pub enum ConflictChoice {
67 /// Load the stored version and drop the unsaved changes.
68 ReloadRemote,
69 /// Save the unsaved changes as a new document named after the original with " (local copy)"
70 /// appended, or " (local copy 2)" and so on if that name is taken, then load the stored
71 /// version.
72 KeepLocalCopy,
73 /// Write the unsaved changes over the stored version, which is now the expected revision.
74 Retry,
75}
76
77/// Where the library keeps settings and drawings between sessions: files, browser storage or a
78/// server, as the host chooses. The library reads and writes nothing on its own.
79///
80/// Documents are JSON text with a version field that the library writes and migrates. Store them
81/// verbatim and hand them back unchanged. Names come from the library (a drawings document is named
82/// by its symbol) and may contain any character, `/` and `..` included: a host that keeps documents
83/// as files must encode the name rather than use it as a path, and `list` returns the names as they
84/// were saved.
85///
86/// These methods are called on the UI thread and must return at once. Do the work elsewhere and
87/// answer through the [`Reply`] you were given, from any thread, runtime or browser task. A failed
88/// `save` keeps the state in memory and is reported to the user once, so answer with an error
89/// rather than dropping a document. The library waits for the answer to one call before it makes
90/// another for the same document, so different calls may be worked on concurrently. Renaming is a
91/// `save` under the new name and, once that succeeds, a `delete` of the old one.
92///
93/// A host shared by several clients can also keep revisions, with
94/// [`load_versioned`](Storage::load_versioned), [`save_versioned`](Storage::save_versioned) and
95/// [`watch`](Storage::watch). The library then writes a document only over the revision it read,
96/// so a client never overwrites a newer version unnoticed: a refused save keeps the unsaved
97/// document in memory and is settled with a [`ConflictChoice`]. A host that answers
98/// `load_versioned` with [`FeedError::NotSupported`], as the default does, keeps the plain
99/// behaviour above. Switching accounts means a new `Storage`; the library drops the old one's
100/// pending answers and streams, so nothing of the old account reaches the new one.
101///
102/// ```
103/// use std::collections::HashMap;
104/// use std::sync::Mutex;
105///
106/// use egui_charts::data::Reply;
107/// use egui_charts::services::{Storage, StoreKind};
108///
109/// /// Documents held in memory, lost when the application exits. Answers inline.
110/// #[derive(Default)]
111/// struct InMemory(Mutex<HashMap<(StoreKind, String), String>>);
112///
113/// impl Storage for InMemory {
114/// fn list(&self, kind: StoreKind, reply: Reply<Vec<String>>) {
115/// let documents = self.0.lock().unwrap();
116/// let names = documents
117/// .keys()
118/// .filter(|(k, _)| *k == kind)
119/// .map(|(_, name)| name.clone())
120/// .collect();
121/// drop(documents);
122/// reply.send(Ok(names));
123/// }
124///
125/// fn load(&self, kind: StoreKind, name: &str, reply: Reply<Option<String>>) {
126/// let document = self.0.lock().unwrap().get(&(kind, name.to_owned())).cloned();
127/// reply.send(Ok(document));
128/// }
129///
130/// fn save(&self, kind: StoreKind, name: &str, document: String, reply: Reply<()>) {
131/// self.0.lock().unwrap().insert((kind, name.to_owned()), document);
132/// reply.send(Ok(()));
133/// }
134///
135/// fn delete(&self, kind: StoreKind, name: &str, reply: Reply<()>) {
136/// self.0.lock().unwrap().remove(&(kind, name.to_owned()));
137/// reply.send(Ok(()));
138/// }
139/// }
140///
141/// let storage: std::sync::Arc<dyn Storage> = std::sync::Arc::new(InMemory::default());
142/// # let _ = storage;
143/// ```
144pub trait Storage: MaybeSend + 'static {
145 /// The names of the stored documents of `kind`, in any order.
146 fn list(&self, kind: StoreKind, reply: Reply<Vec<String>>);
147
148 /// The document `name` of `kind`. Answer `Ok(None)` only when you know there is no such
149 /// document, and an error whenever it cannot be read, including when you cannot tell whether
150 /// it exists, so the library never saves over it.
151 fn load(&self, kind: StoreKind, name: &str, reply: Reply<Option<String>>);
152
153 /// Stores `document` as `name` of `kind`, replacing any document of that name. Answer `Ok`
154 /// only once it is kept.
155 fn save(&self, kind: StoreKind, name: &str, document: String, reply: Reply<()>);
156
157 /// Removes the document `name` of `kind`. Removing a document that does not exist succeeds.
158 fn delete(&self, kind: StoreKind, name: &str, reply: Reply<()>);
159
160 /// Like [`load`](Self::load), with the revision of the document. Answer
161 /// [`FeedError::NotSupported`] if the host keeps no revisions; the library then uses `load`
162 /// and `save` for that document. By default every call is refused so.
163 fn load_versioned(&self, kind: StoreKind, name: &str, reply: Reply<Option<Versioned>>) {
164 let _ = (kind, name);
165 reply.send(Err(FeedError::NotSupported));
166 }
167
168 /// Stores `document` only if the stored one is at revision `expected`, or does not exist when
169 /// `expected` is `None`. Answer [`SaveOutcome::Saved`] with the new revision once it is kept.
170 /// If the stored document differs, change nothing and answer [`SaveOutcome::Conflict`] with
171 /// the stored version. The check and the write must be one atomic step.
172 fn save_versioned(
173 &self,
174 kind: StoreKind,
175 name: &str,
176 document: String,
177 expected: Option<Revision>,
178 reply: Reply<SaveOutcome>,
179 ) {
180 let _ = (kind, name, document, expected);
181 reply.send(Err(FeedError::NotSupported));
182 }
183
184 /// Pushes the new revision each time another client changes the document `name` of `kind`,
185 /// until `events` is inactive. Writes of this client may be pushed too; the library ignores
186 /// the revisions it knows. Called right after the library starts reading a document that
187 /// follows remote changes, so a change made while it is read is not missed, and by default
188 /// pushes nothing.
189 fn watch(&self, kind: StoreKind, name: &str, events: Events<Revision>) {
190 let _ = (kind, name, events);
191 }
192}