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