sycamore_futures/
suspense.rs1use futures::channel::oneshot;
7use futures::Future;
8use sycamore_reactive::*;
9
10use crate::*;
11
12#[derive(Copy, Clone, Debug, Default)]
17struct AllTasksRemaining {
18 all_tasks_remaining: Signal<Vec<Signal<u32>>>,
19}
20
21#[derive(Copy, Clone, Debug)]
23pub struct SuspenseScope {
24 tasks_remaining: Signal<u32>,
25 pub parent: Option<Signal<SuspenseScope>>,
27 pub sent: Signal<bool>,
30}
31
32impl SuspenseScope {
33 pub fn new(parent: Option<SuspenseScope>) -> Self {
38 let tasks_remaining = create_signal(0);
39 let global = use_global_scope().run_in(|| use_context_or_else(AllTasksRemaining::default));
40 global
41 .all_tasks_remaining
42 .update(|vec| vec.push(tasks_remaining));
43 Self {
44 tasks_remaining,
45 parent: parent.map(create_signal),
46 sent: create_signal(false),
47 }
48 }
49
50 fn _is_loading(self) -> bool {
53 self.tasks_remaining.get() > 0
54 || self
55 .parent
56 .as_ref()
57 .map_or(false, |parent| parent.get()._is_loading())
58 }
59
60 pub fn is_loading(self) -> ReadSignal<bool> {
64 create_selector(move || self._is_loading())
65 }
66
67 pub async fn until_finished(self) {
69 let (tx, rx) = oneshot::channel();
70 let mut tx = Some(tx);
71 create_effect(move || {
72 if !self._is_loading() {
73 if let Some(tx) = tx.take() {
74 tx.send(()).unwrap();
75 }
76 }
77 });
78
79 rx.await.unwrap()
80 }
81}
82
83#[derive(Debug)]
85pub struct SuspenseTaskGuard {
86 scope: Option<SuspenseScope>,
87}
88
89impl SuspenseTaskGuard {
90 pub fn new() -> Self {
93 let scope = try_use_context::<SuspenseScope>();
94 if let Some(mut scope) = scope {
95 scope.tasks_remaining += 1;
96 }
97 Self { scope }
98 }
99
100 pub fn from_scope(mut scope: SuspenseScope) -> Self {
102 scope.tasks_remaining += 1;
103 Self { scope: Some(scope) }
104 }
105}
106
107impl Default for SuspenseTaskGuard {
108 fn default() -> Self {
109 Self::new()
110 }
111}
112
113impl Drop for SuspenseTaskGuard {
114 fn drop(&mut self) {
115 if let Some(mut scope) = self.scope {
116 scope.tasks_remaining -= 1;
117 }
118 }
119}
120
121pub fn create_suspense_task(f: impl Future<Output = ()> + 'static) {
128 let guard = SuspenseTaskGuard::new();
129 spawn_local_scoped(async move {
130 f.await;
131 drop(guard);
132 });
133}
134
135pub fn create_detatched_suspense_scope<T>(f: impl FnOnce() -> T) -> (T, SuspenseScope) {
147 let scope = SuspenseScope::new(None);
148 provide_context_in_new_scope(scope, move || {
149 let ret = f();
150 (ret, scope)
151 })
152}
153
154pub fn create_suspense_scope<T>(f: impl FnOnce() -> T) -> (T, SuspenseScope) {
161 let parent = try_use_context::<SuspenseScope>();
162 let scope = SuspenseScope::new(parent);
163 provide_context_in_new_scope(scope, move || {
164 let ret = f();
165 (ret, scope)
166 })
167}
168
169pub async fn await_suspense_current() {
175 if let Some(scope) = try_use_context::<SuspenseScope>() {
176 scope.until_finished().await;
177 }
178}
179
180pub fn use_is_loading() -> ReadSignal<bool> {
188 try_use_context::<SuspenseScope>().map_or(*create_signal(false), |scope| scope.is_loading())
189}
190
191pub fn use_is_loading_global() -> bool {
196 if let Some(global) = try_use_context::<AllTasksRemaining>() {
197 global
198 .all_tasks_remaining
199 .with(|vec| vec.iter().any(|signal| signal.get() > 0))
200 } else {
201 false
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use std::cell::Cell;
208 use std::rc::Rc;
209
210 use super::*;
211
212 #[test]
213 fn suspense_scope() {
214 let _ = create_root(|| {
215 let _ = create_suspense_scope(|| {
216 let outer_scope = try_use_context::<SuspenseScope>();
217 assert!(outer_scope.is_some());
218 assert!(outer_scope.unwrap().parent.is_none());
219
220 let _ = create_suspense_scope(|| {
221 let inner_scope = try_use_context::<SuspenseScope>();
222 assert!(inner_scope.is_some());
223 assert!(inner_scope.unwrap().parent.is_some());
224 });
225 });
226 });
227 }
228
229 #[tokio::test]
230 async fn suspense_await_suspense() {
231 let (tx, rx) = oneshot::channel();
232 let is_completed = Rc::new(Cell::new(false));
233
234 let local = tokio::task::LocalSet::new();
235 local
236 .run_until(async {
237 let _ = create_root({
238 let is_completed = is_completed.clone();
239 || {
240 spawn_local_scoped(async move {
241 let (_, scope) = create_suspense_scope(|| {
242 create_suspense_task(async move {
243 rx.await.unwrap();
244 });
245 });
246
247 scope.until_finished().await;
248 is_completed.set(true);
249 });
250 }
251 });
252 })
253 .await;
254
255 assert!(!is_completed.get());
256
257 tx.send(()).unwrap();
258 local.await;
259 assert!(is_completed.get());
260 }
261
262 #[tokio::test]
263 async fn use_is_loading_global_works() {
264 let (tx, rx) = oneshot::channel();
265
266 let local = tokio::task::LocalSet::new();
267 let mut root = create_root(|| {});
268 local
269 .run_until(async {
270 root = create_root(|| {
271 let _ = create_suspense_scope(|| {
272 create_suspense_task(async move {
273 rx.await.unwrap();
274 });
275 });
276 });
277 })
278 .await;
279
280 root.run_in(|| {
281 assert!(use_is_loading_global());
282 });
283
284 tx.send(()).unwrap();
285 local.await;
286
287 root.run_in(|| {
288 assert!(!use_is_loading_global());
289 });
290 }
291}