Skip to main content

tmprl_core/
loadable.rs

1//! Four-state remote data.
2//!
3//! Every value fetched over the network is one of these. Views render all four, which is
4//! how the interface avoids ever having a code path that blocks waiting for data: there is
5//! no way to express "waiting", only "not here yet", which draws a skeleton.
6
7use std::time::Instant;
8
9#[derive(Debug, Clone, Default)]
10pub enum Loadable<T> {
11    #[default]
12    NotAsked,
13    Loading,
14    Loaded(T, Instant),
15    Failed(String),
16}
17
18impl<T> Loadable<T> {
19    pub fn value(&self) -> Option<&T> {
20        match self {
21            Loadable::Loaded(v, _) => Some(v),
22            _ => None,
23        }
24    }
25
26    /// Mutable access to loaded data, for a list that grows a page at a time rather than
27    /// being replaced wholesale.
28    pub fn value_mut(&mut self) -> Option<&mut T> {
29        match self {
30            Loadable::Loaded(v, _) => Some(v),
31            _ => None,
32        }
33    }
34
35    pub fn is_loading(&self) -> bool {
36        matches!(self, Loadable::Loading)
37    }
38
39    pub fn error(&self) -> Option<&str> {
40        match self {
41            Loadable::Failed(e) => Some(e),
42            _ => None,
43        }
44    }
45
46    /// How stale the data is, for the statusline.
47    pub fn age(&self) -> Option<std::time::Duration> {
48        match self {
49            Loadable::Loaded(_, at) => Some(at.elapsed()),
50            _ => None,
51        }
52    }
53
54    pub fn loaded(value: T) -> Self {
55        Loadable::Loaded(value, Instant::now())
56    }
57
58    /// Mark as loading while keeping any value already on screen, so a refresh does not
59    /// blank the view.
60    pub fn begin_refresh(&mut self) {
61        if !matches!(self, Loadable::Loaded(..)) {
62            *self = Loadable::Loading;
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn accessors_report_the_right_state() {
73        let n: Loadable<u8> = Loadable::NotAsked;
74        assert!(n.value().is_none() && n.error().is_none() && !n.is_loading());
75
76        let l = Loadable::loaded(7u8);
77        assert_eq!(l.value(), Some(&7));
78        assert!(l.age().is_some());
79
80        let f: Loadable<u8> = Loadable::Failed("boom".into());
81        assert_eq!(f.error(), Some("boom"));
82    }
83
84    #[test]
85    fn loaded_data_can_be_grown_in_place() {
86        // Infinite scroll appends to a list that is already on screen; replacing the
87        // Loadable would drop the fetch time the statusline reports staleness from.
88        let mut l = Loadable::loaded(vec![1u8]);
89        let at = l.age();
90        l.value_mut().unwrap().push(2);
91        assert_eq!(l.value(), Some(&vec![1, 2]));
92        assert!(at.is_some() && l.age().is_some());
93
94        let mut n: Loadable<Vec<u8>> = Loadable::NotAsked;
95        assert!(n.value_mut().is_none());
96    }
97
98    #[test]
99    fn refreshing_keeps_existing_data_on_screen() {
100        let mut l = Loadable::loaded(7u8);
101        l.begin_refresh();
102        assert_eq!(l.value(), Some(&7), "a refresh must not blank the view");
103
104        let mut e: Loadable<u8> = Loadable::Failed("boom".into());
105        e.begin_refresh();
106        assert!(e.is_loading());
107    }
108}