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}