Skip to main content

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