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