Skip to main content

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}