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}