Skip to main content

sequoia_async/
resource.rs

1#![forbid(unsafe_code)]
2
3use std::cell::Cell;
4use std::rc::Rc;
5
6use sequoia_reactive::Signal;
7
8#[derive(Clone, Debug, PartialEq)]
9#[non_exhaustive]
10pub enum AsyncState<T: Clone + PartialEq> {
11    Idle,
12
13    Loading,
14
15    Ready(T),
16
17    Failed(String),
18
19    Cancelled,
20}
21
22impl<T: Clone + PartialEq> AsyncState<T> {
23    pub fn is_loading(&self) -> bool {
24        matches!(self, Self::Loading)
25    }
26
27    pub fn data(&self) -> Option<&T> {
28        match self {
29            Self::Ready(v) => Some(v),
30            _ => None,
31        }
32    }
33
34    pub fn error(&self) -> Option<&str> {
35        match self {
36            Self::Failed(e) => Some(e),
37            _ => None,
38        }
39    }
40}
41
42#[derive(Clone)]
43pub struct Resource<T: Clone + PartialEq + 'static> {
44    state: Signal<AsyncState<T>>,
45    generation: Rc<Cell<u64>>,
46}
47
48impl<T: Clone + PartialEq + 'static> Resource<T> {
49    pub fn new() -> Self {
50        Self {
51            state: Signal::new(AsyncState::Idle),
52            generation: Rc::new(Cell::new(0)),
53        }
54    }
55
56    pub fn with_initial(initial: T) -> Self {
57        Self {
58            state: Signal::new(AsyncState::Ready(initial)),
59            generation: Rc::new(Cell::new(0)),
60        }
61    }
62
63    pub fn state(&self) -> Signal<AsyncState<T>> {
64        self.state.clone()
65    }
66
67    pub fn generation(&self) -> u64 {
68        self.generation.get()
69    }
70
71    pub fn begin_loading(&self) -> u64 {
72        let next = self.generation.get() + 1;
73        self.generation.set(next);
74        self.state.set(AsyncState::Loading);
75        next
76    }
77
78    pub fn complete(&self, generation: u64, value: T) {
79        if generation != self.generation.get() {
80            return;
81        }
82        self.state.set(AsyncState::Ready(value));
83    }
84
85    pub fn fail(&self, generation: u64, error: impl Into<String>) {
86        if generation != self.generation.get() {
87            return;
88        }
89        self.state.set(AsyncState::Failed(error.into()));
90    }
91
92    pub fn cancel(&self) {
93        self.generation.set(self.generation.get() + 1);
94        self.state.set(AsyncState::Cancelled);
95    }
96
97    pub fn reset(&self) {
98        self.generation.set(self.generation.get() + 1);
99        self.state.set(AsyncState::Idle);
100    }
101
102    pub fn is_loading(&self) -> bool {
103        self.state.get_untracked().is_loading()
104    }
105
106    pub fn data_untracked(&self) -> Option<T> {
107        match self.state.get_untracked() {
108            AsyncState::Ready(v) => Some(v),
109            _ => None,
110        }
111    }
112}
113
114impl<T: Clone + PartialEq + 'static> Default for Resource<T> {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120impl<T: Clone + PartialEq + 'static> std::fmt::Debug for Resource<T>
121where
122    T: std::fmt::Debug,
123{
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        f.debug_struct("Resource")
126            .field("state", &self.state.get_untracked())
127            .field("generation", &self.generation.get())
128            .finish()
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn stale_generation_is_ignored() {
138        let r: Resource<String> = Resource::new();
139        let old = r.begin_loading();
140        let fresh = r.begin_loading();
141        r.complete(old, "stale".into());
142        assert!(r.state.get_untracked().is_loading(), "stale ignored");
143        r.complete(fresh, "fresh".into());
144        assert_eq!(r.data_untracked().as_deref(), Some("fresh"));
145    }
146
147    #[test]
148    fn cancel_invalidates_pending() {
149        let r: Resource<u32> = Resource::new();
150        let loading = r.begin_loading();
151        r.cancel();
152        assert_eq!(r.state.get_untracked(), AsyncState::Cancelled);
153        r.complete(loading, 1);
154        assert_eq!(r.state.get_untracked(), AsyncState::Cancelled);
155    }
156
157    #[test]
158    fn fail_and_reset_flow() {
159        let r: Resource<u32> = Resource::new();
160        let loading = r.begin_loading();
161        r.fail(loading, "boom");
162        assert_eq!(r.state.get_untracked(), AsyncState::Failed("boom".into()));
163        r.reset();
164        assert_eq!(r.state.get_untracked(), AsyncState::Idle);
165    }
166}