sequoia_async/
resource.rs1#![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}