bounce/states/
future_notion.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3use std::sync::atomic::{AtomicBool, Ordering};
4
5use futures::future::LocalBoxFuture;
6use wasm_bindgen::prelude::*;
7use yew::platform::spawn_local;
8use yew::prelude::*;
9
10use crate::root_state::{BounceRootState, BounceStates};
11
12/// A trait to implement a [`Future`](std::future::Future)-backed notion.
13///
14/// This trait is usually automatically implemented by the
15/// [`#[future_notion]`](macro@crate::future_notion) attribute macro.
16pub trait FutureNotion {
17    /// The input type.
18    type Input: 'static;
19    /// The output type.
20    type Output: 'static;
21
22    /// Runs a future notion.
23    fn run<'a>(
24        states: &'a BounceStates,
25        input: &'a Self::Input,
26    ) -> LocalBoxFuture<'a, Self::Output>;
27}
28
29/// A deferred result type for future notions.
30///
31/// For each future notion `T`, a `Deferred<T>` the following notions will be applied to states:
32///
33/// - A `Deferred::<T>::Pending` Notion will be applied before a future notion starts running.
34/// - A `Deferred::<T>::Complete` Notion will be applied after a future notion completes.
35/// - If any states are used during the run of a future notion,
36///   a `Deferred::<T>::Outdated` Notion will be applied **once** after the value of any used states changes.
37#[derive(Debug)]
38pub enum Deferred<T>
39where
40    T: FutureNotion,
41{
42    /// A future notion is running.
43    Pending {
44        /// The input value of a future notion.
45        input: Rc<T::Input>,
46    },
47    /// A future notion has completed.
48    Completed {
49        /// The input value of a future notion.
50        input: Rc<T::Input>,
51
52        /// The output value of a future notion.
53        output: Rc<T::Output>,
54    },
55    /// The states used in the future notion run has been changed.
56    Outdated {
57        /// The input value of a future notion.
58        input: Rc<T::Input>,
59    },
60}
61
62impl<T> Deferred<T>
63where
64    T: FutureNotion,
65{
66    /// Returns `true` if current future notion is still running.
67    pub fn is_pending(&self) -> bool {
68        match self {
69            Self::Pending { .. } => true,
70            Self::Completed { .. } => false,
71            Self::Outdated { .. } => false,
72        }
73    }
74
75    /// Returns `true` if current future notion has been completed.
76    pub fn is_completed(&self) -> bool {
77        match self {
78            Self::Pending { .. } => false,
79            Self::Completed { .. } => true,
80            Self::Outdated { .. } => false,
81        }
82    }
83
84    /// Returns `true` if current future notion is outdated.
85    pub fn is_outdated(&self) -> bool {
86        match self {
87            Self::Pending { .. } => false,
88            Self::Completed { .. } => false,
89            Self::Outdated { .. } => true,
90        }
91    }
92
93    /// Returns the input of current future notion.
94    pub fn input(&self) -> Rc<T::Input> {
95        match self {
96            Self::Pending { input } => input.clone(),
97            Self::Completed { input, .. } => input.clone(),
98            Self::Outdated { input } => input.clone(),
99        }
100    }
101
102    /// Returns the output of current future notion if it has completed.
103    pub fn output(&self) -> Option<Rc<T::Output>> {
104        match self {
105            Self::Pending { .. } => None,
106            Self::Completed { output, .. } => Some(output.clone()),
107            Self::Outdated { .. } => None,
108        }
109    }
110}
111
112impl<T> Clone for Deferred<T>
113where
114    T: FutureNotion,
115{
116    fn clone(&self) -> Self {
117        match self {
118            Self::Pending { ref input } => Self::Pending {
119                input: input.clone(),
120            },
121            Self::Completed {
122                ref input,
123                ref output,
124            } => Self::Completed {
125                input: input.clone(),
126                output: output.clone(),
127            },
128            Self::Outdated { ref input } => Self::Outdated {
129                input: input.clone(),
130            },
131        }
132    }
133}
134
135/// A hook to create a function that when called, runs a [`FutureNotion`] with provided input.
136///
137/// A `FutureNotion` is created by applying a `#[future_notion(NotionName)]` attribute to an async function.
138///
139/// When a future notion is run, it will be applied twice with a notion type [`Deferred<T>`]. The
140/// first time is before it starts with a variant `Pending` and the second time is when it
141/// completes with variant `Complete`.
142///
143/// If the notion read any other states using the `BounceStates` argument, it will subscribe to the
144/// states, when any state changes, an `Outdated` variant will be dispatched.
145///
146/// # Note
147///
148/// If you are trying to interact with a backend API, it is recommended to use the [Query](crate::query) API instead.
149///
150/// # Example
151///
152/// ```
153/// # use bounce::prelude::*;
154/// # use std::fmt;
155/// # use std::rc::Rc;
156/// # use yew::prelude::*;
157/// # use bounce::prelude::*;
158///
159/// #[derive(PartialEq)]
160/// struct User {
161///     id: u64,
162///     username: String,
163/// }
164///
165/// #[future_notion(FetchUser)]
166/// async fn fetch_user(id: &u64) -> User {
167///     // fetch user here...
168///
169///     User { id: *id, username: "username".into() }
170/// }
171///
172/// #[derive(PartialEq, Default, Atom)]
173/// #[bounce(with_notion(Deferred<FetchUser>))]  // A future notion with type `T` will be applied as `Deferred<T>`.
174/// struct UserState {
175///     inner: Option<Rc<User>>,
176/// }
177///
178/// // Each time a future notion is run, it will be applied twice.
179/// impl WithNotion<Deferred<FetchUser>> for UserState {
180///     fn apply(self: Rc<Self>, notion: Rc<Deferred<FetchUser>>) -> Rc<Self> {
181///         match notion.output() {
182///             Some(m) => Self { inner: Some(m) }.into(),
183///             None => self,
184///         }
185///     }
186/// }
187///
188/// # #[function_component(FetchUserComp)]
189/// # fn fetch_user_comp() -> Html {
190/// let load_user = use_future_notion_runner::<FetchUser>();
191/// load_user(1);
192/// # Html::default()
193/// # }
194/// ```
195#[hook]
196pub fn use_future_notion_runner<T>() -> Rc<dyn Fn(T::Input)>
197where
198    T: FutureNotion + 'static,
199{
200    let root = use_context::<BounceRootState>().expect_throw("No bounce root found.");
201
202    Rc::new(move |input: T::Input| {
203        let root = root.clone();
204        let input = Rc::new(input);
205
206        spawn_local(async move {
207            root.apply_notion(Rc::new(Deferred::<T>::Pending {
208                input: input.clone(),
209            }));
210
211            let states = root.states();
212
213            // send the listeners in to be destroyed.
214            let listeners = Rc::new(RefCell::new(None));
215            let listener_run = Rc::new(AtomicBool::new(false));
216
217            {
218                let listener_run = listener_run.clone();
219                let listeners = listeners.clone();
220                let root = root.clone();
221                let input = input.clone();
222                states.add_listener_callback(Rc::new(Callback::from(move |_| {
223                    // There's a chance that the listeners might be called during the time while the future
224                    // notion is running and there will be nothing to drop.
225                    let listeners = listeners.borrow_mut().take();
226                    let last_listener_run = listener_run.swap(true, Ordering::Relaxed);
227
228                    if !last_listener_run || listeners.is_some() {
229                        root.apply_notion(Rc::new(Deferred::<T>::Outdated {
230                            input: input.clone(),
231                        }));
232                    }
233                })))
234            }
235
236            let output = T::run(&states, &input).await;
237
238            if !listener_run.load(Ordering::Relaxed) {
239                let _result = listeners.borrow_mut().replace(states.take_listeners());
240            }
241
242            root.apply_notion(Rc::new(Deferred::<T>::Completed {
243                input,
244                output: output.into(),
245            }));
246        });
247    })
248}