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}