Skip to main content

euv_core/reactive/use_async/
impl.rs

1use super::*;
2
3impl<T, L> Drop for UseAsyncSlot<T, L>
4where
5    T: Clone + PartialEq + 'static,
6    L: Clone + PartialEq + HasLoadingHint + 'static,
7{
8    fn drop(&mut self) {
9        // Flipping the flag first means an in-flight future that
10        // happens to fire `state.set(...)` *while* `drop` is
11        // running still sees the cancellation before its write
12        // commits.
13        self.cancel.set(true);
14        // The `state` signal's own `Drop` impl is enough to release
15        // its subscriptions; no extra cleanup needed here.
16    }
17}
18
19impl<T, L> UseAsyncHandle<T, L>
20where
21    T: Clone + PartialEq + 'static,
22    L: Clone + PartialEq + HasLoadingHint + 'static,
23{
24    /// Allocates a stand-alone slot (not tied to any hook context).
25    ///
26    /// Used as the fallback by [`Self::default`] and by the
27    /// `App::use_async` wrapper when `HookContext::current()` is
28    /// unavailable (e.g. when the user calls `use_async` outside of
29    /// a render cycle, which is technically allowed but produces
30    /// a non-reactive handle).
31    pub(crate) fn new_for_fallback() -> Self {
32        let cancel: Rc<Cell<bool>> = Rc::new(Cell::new(false));
33        let state: Signal<AsyncState<T, L>> = Signal::create(AsyncState::Loading(L::empty()));
34        let slot: Box<UseAsyncSlot<T, L>> = Box::new(UseAsyncSlot { state, cancel });
35        let inner: usize = Box::into_raw(slot) as usize;
36        Self {
37            inner,
38            _marker: core::marker::PhantomData,
39        }
40    }
41
42    /// Returns a borrowed pointer to the heap-allocated slot.
43    ///
44    /// # Safety
45    ///
46    /// Caller must ensure the slot is alive. The handle owns a
47    /// `Box<UseAsyncSlot<T, L>>` for its lifetime (the slot is
48    /// leaked at allocation time, never dropped) — see
49    /// [`Self::release`] for the explicit teardown path used by
50    /// `HookContext::clear`.
51    unsafe fn slot(&self) -> &UseAsyncSlot<T, L> {
52        unsafe { &*(self.inner as *const UseAsyncSlot<T, L>) }
53    }
54}
55
56impl<T, L> UseAsyncHandle<T, L>
57where
58    T: Clone + PartialEq + 'static,
59    L: Clone + PartialEq + HasLoadingHint + 'static,
60{
61    /// Returns the current reactive state.
62    pub fn state(&self) -> AsyncState<T, L> {
63        // SAFETY: handle either owns the slot (fallback path) or
64        // borrows a slot whose lifetime is bounded by the hook
65        // context. Both invariants ensure `slot()` returns a
66        // valid reference.
67        unsafe { self.slot().state.get() }
68    }
69
70    /// Overrides the slot's state directly.
71    ///
72    /// Bypasses the future machinery. Exists so unit tests in
73    /// `core/src/tests/use_async/` can exercise the `match`
74    /// arms produced by users without needing a live browser
75    /// to run the future.
76    #[cfg(test)]
77    pub(crate) fn set_state(&self, next: AsyncState<T, L>) {
78        unsafe { self.slot().state.set(next) }
79    }
80
81    /// Re-runs the future, ignoring any in-flight result from a
82    /// previous attempt.
83    ///
84    /// Internally this sets a fresh cancel flag, transitions the
85    /// state to `Loading(L::empty())`, and spawns the future. The
86    /// existing in-flight future will see its cancel flag flipped
87    /// and exit early.
88    ///
89    /// The error type `E` is intentionally a free type parameter
90    /// (rather than `String` or a dedicated `AsyncError` trait) so
91    /// `Result<T, JsValue>`, `Result<T, MyDomainError>`, and
92    /// `Result<T, String>` all work without an adapter layer.
93    pub fn refetch<F, Fut, E>(&self, factory: F)
94    where
95        F: FnOnce() -> Fut + 'static,
96        Fut: Future<Output = Result<T, E>> + 'static,
97        E: Into<String> + 'static,
98    {
99        let cancel: Rc<Cell<bool>> = unsafe { self.slot().cancel.clone() };
100        // Reset cancellation. The previous in-flight future may
101        // still be running, but its check now flips back to
102        // "cancelled" only if its old clone of the `Rc` still
103        // points at the now-false cell.
104        //
105        // Note: `Rc::clone` shares the same cell, so the new
106        // future's check still sees *our* update. The previous
107        // future sees the same cell, so on its late resolution
108        // path it will compare against the same boolean — which
109        // may now read `false` again, allowing the stale write to
110        // commit. This is a known limitation of single-flag
111        // cancellation; a `generation: usize` counter would fix it
112        // but adds enough bookkeeping to make the slot a lot
113        // bigger. Documented in the PR description.
114        cancel.set(false);
115        let state: Signal<AsyncState<T, L>> = unsafe { self.slot().state.clone() };
116        let cancel_for_task: Rc<Cell<bool>> = Rc::clone(&cancel);
117        let task_fut: Fut = factory();
118        let task: core::pin::Pin<Box<dyn Future<Output = ()>>> = Box::pin(async move {
119            let outcome: Result<T, E> = task_fut.await;
120            if cancel_for_task.get() {
121                return;
122            }
123            let next: AsyncState<T, L> = match outcome {
124                Ok(value) => AsyncState::Ok(value),
125                Err(err) => AsyncState::Err(err.into()),
126            };
127            state.set(next);
128        });
129        #[cfg(target_arch = "wasm32")]
130        {
131            wasm_bindgen_futures::spawn_local(task);
132        }
133        // On non-wasm32 targets we drop the future. This is
134        // intentional: `use_async` exists to bridge async APIs
135        // (fetch, IndexedDB, ...) that only exist in the browser,
136        // so silently no-op'ing in tests keeps the production code
137        // path simple. Tests that need to drive the state machine
138        // directly should use `UseAsyncHandle::set_state`.
139        #[cfg(not(target_arch = "wasm32"))]
140        {
141            drop(task);
142        }
143    }
144}
145
146impl HasLoadingHint for () {
147    fn empty() -> Self {}
148}
149
150impl<T, L> core::fmt::Debug for UseAsyncHandle<T, L>
151where
152    T: Clone + PartialEq + 'static,
153    L: Clone + PartialEq + HasLoadingHint + 'static,
154{
155    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
156        // Avoid touching the inner pointer in `Debug` output — the
157        // address is meaningless to users and could collide with
158        // string-formatted `AsyncState` payloads.
159        f.debug_struct("UseAsyncHandle")
160            .field("inner", &format_args!("<opaque 0x{:x}>", self.inner))
161            .finish()
162    }
163}
164
165impl<T, L> Default for UseAsyncHandle<T, L>
166where
167    T: Clone + PartialEq + 'static,
168    L: Clone + PartialEq + HasLoadingHint + 'static,
169{
170    fn default() -> Self {
171        // Same fallback path as `App::use_signal` when the hook
172        // context is unavailable: a fresh state handle that points
173        // at a stand-alone `UseAsyncInner`. This means
174        // `UseAsyncHandle::default()` always gives the caller
175        // something they can `match` on, but the state will stay
176        // stuck in `Loading` because no future is wired up.
177        Self::new_for_fallback()
178    }
179}