1use core::fmt;
2use std::{
3 cell::{Ref, RefCell},
4 collections::{HashMap, HashSet},
5 future::Future,
6 hash::Hash,
7 mem,
8 rc::Rc,
9 sync::{Arc, Mutex},
10 time::Duration,
11};
12
13use dioxus_lib::prelude::*;
14use dioxus_lib::signals::{Readable, Writable};
15use dioxus_lib::{
16 hooks::{use_memo, use_reactive},
17 signals::CopyValue,
18};
19use tokio::time::Instant;
20
21pub trait MutationCapability
22where
23 Self: 'static + Clone + PartialEq + Hash + Eq,
24{
25 type Ok;
26 type Err;
27 type Keys: Hash + PartialEq + Clone;
28
29 fn run(&self, keys: &Self::Keys) -> impl Future<Output = Result<Self::Ok, Self::Err>>;
31
32 fn matches(&self, _keys: &Self::Keys) -> bool {
34 true
35 }
36
37 fn on_settled(
40 &self,
41 _keys: &Self::Keys,
42 _result: &Result<Self::Ok, Self::Err>,
43 ) -> impl Future<Output = ()> {
44 async {}
45 }
46}
47
48pub enum MutationStateData<Q: MutationCapability> {
49 Pending,
51 Loading { res: Option<Result<Q::Ok, Q::Err>> },
53 Settled {
55 res: Result<Q::Ok, Q::Err>,
56 settlement_instant: Instant,
57 },
58}
59
60impl<Q> fmt::Debug for MutationStateData<Q>
61where
62 Q: MutationCapability,
63 Q::Ok: fmt::Debug,
64 Q::Err: fmt::Debug,
65{
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 Self::Pending => f.write_str("Pending"),
69 Self::Loading { res } => write!(f, "Loading {{ {res:?} }}"),
70 Self::Settled { res, .. } => write!(f, "Settled {{ {res:?} }}"),
71 }
72 }
73}
74
75impl<Q: MutationCapability> MutationStateData<Q> {
76 pub fn is_ok(&self) -> bool {
78 matches!(self, MutationStateData::Settled { res: Ok(_), .. })
79 }
80
81 pub fn is_err(&self) -> bool {
83 matches!(self, MutationStateData::Settled { res: Err(_), .. })
84 }
85
86 pub fn is_loading(&self) -> bool {
88 matches!(self, MutationStateData::Loading { .. })
89 }
90
91 pub fn is_pending(&self) -> bool {
93 matches!(self, MutationStateData::Pending)
94 }
95
96 pub fn ok(&self) -> Option<&Q::Ok> {
98 match self {
99 Self::Settled { res: Ok(res), .. } => Some(res),
100 Self::Loading { res: Some(Ok(res)) } => Some(res),
101 _ => None,
102 }
103 }
104
105 pub fn unwrap(&self) -> &Result<Q::Ok, Q::Err> {
107 match self {
108 Self::Loading { res: Some(v) } => v,
109 Self::Settled { res, .. } => res,
110 _ => unreachable!(),
111 }
112 }
113
114 fn into_loading(self) -> MutationStateData<Q> {
115 match self {
116 MutationStateData::Pending => MutationStateData::Loading { res: None },
117 MutationStateData::Loading { res } => MutationStateData::Loading { res },
118 MutationStateData::Settled { res, .. } => MutationStateData::Loading { res: Some(res) },
119 }
120 }
121}
122pub struct MutationsStorage<Q: MutationCapability> {
123 storage: CopyValue<HashMap<Mutation<Q>, MutationData<Q>>>,
124}
125
126impl<Q: MutationCapability> Copy for MutationsStorage<Q> {}
127
128impl<Q: MutationCapability> Clone for MutationsStorage<Q> {
129 fn clone(&self) -> Self {
130 *self
131 }
132}
133
134pub struct MutationData<Q: MutationCapability> {
135 state: Rc<RefCell<MutationStateData<Q>>>,
136 reactive_contexts: Arc<Mutex<HashSet<ReactiveContext>>>,
137
138 clean_task: Rc<RefCell<Option<Task>>>,
139}
140
141impl<Q: MutationCapability> Clone for MutationData<Q> {
142 fn clone(&self) -> Self {
143 Self {
144 state: self.state.clone(),
145 reactive_contexts: self.reactive_contexts.clone(),
146 clean_task: self.clean_task.clone(),
147 }
148 }
149}
150
151impl<Q: MutationCapability> MutationsStorage<Q> {
152 fn new_in_root() -> Self {
153 Self {
154 storage: CopyValue::new_in_scope(HashMap::default(), ScopeId::ROOT),
155 }
156 }
157
158 fn insert_or_get_mutation(&mut self, mutation: Mutation<Q>) -> MutationData<Q> {
159 let mut storage = self.storage.write();
160
161 let mutation_data = storage.entry(mutation).or_insert_with(|| MutationData {
162 state: Rc::new(RefCell::new(MutationStateData::Pending)),
163 reactive_contexts: Arc::default(),
164 clean_task: Rc::default(),
165 });
166
167 if let Some(clean_task) = mutation_data.clean_task.take() {
169 clean_task.cancel();
170 }
171
172 mutation_data.clone()
173 }
174
175 fn update_tasks(&mut self, mutation: Mutation<Q>) {
176 let mut storage_clone = self.storage;
177 let mut storage = self.storage.write();
178
179 let mutation_data = storage.get_mut(&mutation).unwrap();
180
181 if mutation_data.reactive_contexts.lock().unwrap().is_empty() {
183 *mutation_data.clean_task.borrow_mut() = spawn_forever(async move {
184 tokio::time::sleep(mutation.clean_time).await;
186
187 let mut storage = storage_clone.write();
189 storage.remove(&mutation);
190 });
191 }
192 }
193
194 async fn run(mutation: &Mutation<Q>, data: &MutationData<Q>, keys: Q::Keys) {
195 let res =
197 mem::replace(&mut *data.state.borrow_mut(), MutationStateData::Pending).into_loading();
198 *data.state.borrow_mut() = res;
199 for reactive_context in data.reactive_contexts.lock().unwrap().iter() {
200 reactive_context.mark_dirty();
201 }
202
203 let res = mutation.mutation.run(&keys).await;
205
206 mutation.mutation.on_settled(&keys, &res).await;
208 *data.state.borrow_mut() = MutationStateData::Settled {
209 res,
210 settlement_instant: Instant::now(),
211 };
212 for reactive_context in data.reactive_contexts.lock().unwrap().iter() {
213 reactive_context.mark_dirty();
214 }
215 }
216}
217
218#[derive(PartialEq, Clone)]
219pub struct Mutation<Q: MutationCapability> {
220 mutation: Q,
221
222 clean_time: Duration,
223}
224
225impl<Q: MutationCapability> Eq for Mutation<Q> {}
226impl<Q: MutationCapability> Hash for Mutation<Q> {
227 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
228 self.mutation.hash(state);
229 }
230}
231
232impl<Q: MutationCapability> Mutation<Q> {
233 pub fn new(mutation: Q) -> Self {
234 Self {
235 mutation,
236 clean_time: Duration::ZERO,
237 }
238 }
239
240 pub fn clean_time(self, clean_time: Duration) -> Self {
244 Self { clean_time, ..self }
245 }
246}
247
248pub struct MutationReader<Q: MutationCapability> {
249 state: Rc<RefCell<MutationStateData<Q>>>,
250}
251
252impl<Q: MutationCapability> MutationReader<Q> {
253 pub fn state(&self) -> Ref<MutationStateData<Q>> {
254 self.state.borrow()
255 }
256}
257
258pub struct UseMutation<Q: MutationCapability> {
259 mutation: Memo<Mutation<Q>>,
260}
261
262impl<Q: MutationCapability> Clone for UseMutation<Q> {
263 fn clone(&self) -> Self {
264 *self
265 }
266}
267
268impl<Q: MutationCapability> Copy for UseMutation<Q> {}
269
270impl<Q: MutationCapability> UseMutation<Q> {
271 pub fn read(&self) -> MutationReader<Q> {
276 let storage = consume_context::<MutationsStorage<Q>>();
277 let mutation_data = storage
278 .storage
279 .peek_unchecked()
280 .get(&self.mutation.peek())
281 .cloned()
282 .unwrap();
283
284 if let Some(reactive_context) = ReactiveContext::current() {
286 reactive_context.subscribe(mutation_data.reactive_contexts);
287 }
288
289 MutationReader {
290 state: mutation_data.state,
291 }
292 }
293
294 pub fn peek(&self) -> MutationReader<Q> {
299 let storage = consume_context::<MutationsStorage<Q>>();
300 let mutation_data = storage
301 .storage
302 .peek_unchecked()
303 .get(&self.mutation.peek())
304 .cloned()
305 .unwrap();
306
307 MutationReader {
308 state: mutation_data.state,
309 }
310 }
311
312 pub async fn mutate_async(&self, keys: Q::Keys) -> MutationReader<Q> {
316 let storage = consume_context::<MutationsStorage<Q>>();
317
318 let mutation = self.mutation.peek().clone();
319 let mutation_data = storage
320 .storage
321 .peek_unchecked()
322 .get(&mutation)
323 .cloned()
324 .unwrap();
325
326 MutationsStorage::run(&mutation, &mutation_data, keys).await;
328
329 MutationReader {
330 state: mutation_data.state,
331 }
332 }
333
334 pub fn mutate(&self, keys: Q::Keys) {
338 let storage = consume_context::<MutationsStorage<Q>>();
339
340 let mutation = self.mutation.peek().clone();
341 let mutation_data = storage
342 .storage
343 .peek_unchecked()
344 .get(&mutation)
345 .cloned()
346 .unwrap();
347
348 spawn(async move {
350 MutationsStorage::run(&mutation, &mutation_data, keys).await;
351 });
352 }
353}
354
355pub fn use_mutation<Q: MutationCapability>(mutation: Mutation<Q>) -> UseMutation<Q> {
362 let mut storage = match try_consume_context::<MutationsStorage<Q>>() {
363 Some(storage) => storage,
364 None => provide_root_context(MutationsStorage::<Q>::new_in_root()),
365 };
366
367 let current_mutation = use_hook(|| Rc::new(RefCell::new(None)));
368
369 let mutation = use_memo(use_reactive!(|mutation| {
371 let _data = storage.insert_or_get_mutation(mutation.clone());
372
373 if let Some(prev_mutation) = current_mutation.borrow_mut().take() {
375 storage.update_tasks(prev_mutation);
376 }
377
378 current_mutation.borrow_mut().replace(mutation.clone());
380
381 mutation
382 }));
383
384 use_drop({
386 move || {
387 storage.update_tasks(mutation.peek().clone());
388 }
389 });
390
391 UseMutation { mutation }
392}