use std::future::Future;
use std::ops::Deref;
use futures::future::{FutureExt, LocalBoxFuture};
use sycamore_futures::{SuspenseScope, SuspenseTaskGuard};
use crate::*;
#[derive(Clone, Copy)]
pub struct Resource<T: 'static> {
value: Signal<Option<T>>,
is_loading: Signal<bool>,
#[allow(clippy::complexity)]
refetch: Signal<Box<dyn FnMut() -> LocalBoxFuture<'static, T>>>,
scopes: Signal<Vec<SuspenseScope>>,
guards: Signal<Vec<SuspenseTaskGuard>>,
}
impl<T: 'static> Resource<T> {
fn new<F, Fut>(mut refetch: F) -> Self
where
F: FnMut() -> Fut + 'static,
Fut: Future<Output = T> + 'static,
{
Self {
value: create_signal(None),
is_loading: create_signal(true),
refetch: create_signal(Box::new(move || refetch().boxed_local())),
scopes: create_signal(Vec::new()),
guards: create_signal(Vec::new()),
}
}
fn always_refetch(self) -> Self {
create_effect(move || {
self.is_loading.set(true);
for scope in self.scopes.take() {
let guard = SuspenseTaskGuard::from_scope(scope);
self.guards.update(|guards| guards.push(guard));
}
let fut = self.refetch.update_silent(|f| f());
sycamore_futures::create_suspense_task(async move {
let value = fut.await;
batch(move || {
self.value.set(Some(value));
self.is_loading.set(false);
self.guards.update(|guards| guards.clear());
});
});
});
self
}
pub fn is_loading(&self) -> bool {
self.is_loading.get()
}
}
impl<T: 'static> Deref for Resource<T> {
type Target = ReadSignal<Option<T>>;
fn deref(&self) -> &Self::Target {
if self.is_loading.get() {
let guard = SuspenseTaskGuard::new();
self.guards.update(|guards| guards.push(guard));
} else if let Some(scope) = try_use_context::<SuspenseScope>() {
self.scopes.update(|scopes| scopes.push(scope));
}
&self.value
}
}
pub fn create_isomorphic_resource<F, Fut, T>(f: F) -> Resource<T>
where
F: FnMut() -> Fut + 'static,
Fut: Future<Output = T> + 'static,
T: 'static,
{
Resource::new(f).always_refetch()
}
pub fn create_client_resource<F, Fut, T>(f: F) -> Resource<T>
where
F: FnMut() -> Fut + 'static,
Fut: Future<Output = T> + 'static,
T: 'static,
{
let resource = Resource::new(f);
if is_not_ssr!() {
resource.always_refetch()
} else {
resource
}
}
#[cfg(test)]
mod tests {
use futures::channel::oneshot;
use sycamore_futures::provide_executor_scope;
use super::*;
#[tokio::test]
async fn create_isomorphic_resource_works() {
provide_executor_scope(async {
let (tx, rx) = oneshot::channel();
let mut tx = Some(tx);
let mut resource = None;
let mut value = None;
let root = create_root(|| {
value = Some(create_signal(123));
resource = Some(create_isomorphic_resource(on(
value.unwrap(),
move || async move { value.unwrap().get() },
)));
assert_eq!(resource.unwrap().get(), None);
assert!(resource.unwrap().is_loading());
create_effect(move || {
if !resource.unwrap().is_loading() {
if let Some(tx) = tx.take() {
tx.send(()).unwrap();
}
}
})
});
rx.await.unwrap();
root.run_in(move || {
assert_eq!(resource.unwrap().get(), Some(123));
assert!(!resource.unwrap().is_loading());
value.unwrap().set(456);
assert_eq!(
resource.unwrap().get(),
Some(123),
"resource should keep old value until new value is loaded"
);
assert!(resource.unwrap().is_loading());
});
})
.await;
}
}