Skip to main content

euv_ui/hook/use_async/
impl.rs

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