This commit is contained in:
Greg Johnston 2022-08-03 10:28:36 -04:00
parent a8cc450939
commit 5612793f4e
14 changed files with 342 additions and 72 deletions

View file

@ -1,6 +1,6 @@
- [ ] Async
- [x] Resource
- [ ] Suspense
- [x] Suspense
- [ ] Transitions
- [ ] Router
- [ ] Docs (and clippy warning to insist on docs)

View file

@ -9,6 +9,8 @@ leptos = { path = "../../leptos" }
reqwasm = "0.5.0"
serde = { version = "1", features = ["derive"] }
wee_alloc = "0.4"
log = "0.4"
console_log = "0.2"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View file

@ -3,5 +3,11 @@
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
</head>
<style>
img {
max-width: 250px;
height: auto;
}
</style>
<body></body>
</html>

View file

@ -1,4 +1,3 @@
use anyhow::Result;
use leptos::*;
use serde::{Deserialize, Serialize};
@ -7,7 +6,7 @@ pub struct Cat {
url: String,
}
async fn fetch_cats(count: u32) -> Result<Vec<String>> {
async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
if count > 0 {
log!("fetching cats");
let res = reqwasm::http::Request::get(&format!(
@ -15,9 +14,11 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>> {
count
))
.send()
.await?
.await
.map_err(|_| ())?
.json::<Vec<Cat>>()
.await?
.await
.map_err(|_| ())?
.into_iter()
.map(|cat| cat.url)
.collect::<Vec<_>>();
@ -29,16 +30,15 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>> {
}
pub fn fetch_example(cx: Scope) -> web_sys::Element {
let (cat_count, set_cat_count) = cx.create_signal::<u32>(0);
let cats = cx.create_resource(cat_count.clone(), |count| fetch_cats(*count));
cx.create_effect(move || log!("cats data = {:?}", cats.data.get()));
let (cat_count, set_cat_count) = cx.create_signal::<u32>(3);
let cats = cx.create_ref(cx.create_resource(cat_count.clone(), |count| fetch_cats(*count)));
view! {
<div>
<label>
"How many cats would you like?"
<input type="number"
value={move || cat_count.get().to_string()}
on:input=move |ev| {
let val = event_target_value(&ev).parse::<u32>().unwrap_or(0);
log!("set_cat_count {val}");
@ -46,21 +46,26 @@ pub fn fetch_example(cx: Scope) -> web_sys::Element {
}
/>
</label>
{move || match &*cats.data.get() {
None => view! { <p>"Loading..."</p> },
Some(Err(e)) => view! { <pre>"Error: " {e.to_string()}</pre> },
Some(Ok(cats)) => view! {
<div>{
cats.iter()
.map(|src| {
view! {
<img src={src}/>
}
})
.collect::<Vec<_>>()
}</div>
},
}}
<div>
//<Suspense fallback={"Loading (Suspense Fallback)...".to_string()}>
{move || match &*cats.read() {
ResourceState::Idle => view! { <p>"(no data)"</p> },
ResourceState::Pending { .. } => view! { <p>"Loading..."</p> },
ResourceState::Ready { data: Err(_) } => view! { <pre>"Error"</pre> },
ResourceState::Ready { data: Ok(cats) } => view! {
<div>{
cats.iter()
.map(|src| {
view! {
<img src={src}/>
}
})
.collect::<Vec<_>>()
}</div>
}
}}
//</Suspense>
</div>
</div>
}
}

View file

@ -5,5 +5,7 @@ use leptos::*;
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
pub fn main() {
console_log::init_with_level(log::Level::Debug);
mount_to_body(fetch_example)
}

View file

@ -1,7 +1,9 @@
mod for_component;
mod map;
mod suspense;
pub use for_component::*;
pub use suspense::*;
pub trait Prop {
type Builder;

View file

@ -0,0 +1,40 @@
use crate as leptos;
use leptos_dom::{Child, IntoChild};
use leptos_macro::Props;
use leptos_reactive::{Scope, SuspenseContext};
#[derive(Props)]
pub struct SuspenseProps<F, C, G>
where
F: for<'a> IntoChild<'a> + Clone,
C: for<'a> IntoChild<'a> + Clone,
G: Fn() -> C,
{
fallback: F,
children: G,
}
#[allow(non_snake_case)]
pub fn Suspense<'a, F, C, G>(cx: Scope<'a>, props: SuspenseProps<F, C, G>) -> impl Fn() -> Child<'a>
where
F: for<'b> IntoChild<'b> + Clone,
C: for<'b> IntoChild<'b> + Clone,
G: Fn() -> C,
{
let context = SuspenseContext::new(cx);
// provide this SuspenseContext to any resources below it
cx.provide_context(context.clone());
leptos_dom::log!("point A");
move || {
if context.ready() {
leptos_dom::log!("point B");
(props.children)().into_child(cx)
} else {
leptos_dom::log!("point C");
props.fallback.clone().into_child(cx)
}
}
}

View file

@ -5,6 +5,8 @@ edition = "2021"
[dependencies]
bumpalo = "3"
futures = "0.3"
log = "0.4"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4"

View file

@ -1,5 +1,6 @@
use std::{
cell::RefCell,
fmt::Debug,
hash::Hash,
rc::{Rc, Weak},
};
@ -35,6 +36,8 @@ impl Effect {
impl EffectInner {
pub(crate) fn execute(&self, for_stack: Weak<EffectInner>) {
crate::debug_warn!("executing Effect: cleanup");
// clear previous dependencies
// at this point, Effect dependencies have been added to Signal
// and any Signal changes will call Effect dependency automatically
@ -42,12 +45,23 @@ impl EffectInner {
self.cleanup(upgraded);
}
crate::debug_warn!("executing Effect: pushing to stack");
// add it to the Scope stack, which means any signals called
// in the effect fn immediately below will add this Effect as a dependency
self.stack.push(for_stack);
// actually run the effect, which will re-add Signal dependencies as they're called
(self.f.borrow_mut())();
crate::debug_warn!(
"about to run effect — stack is {:?}",
self.stack.stack.borrow()
);
match self.f.try_borrow_mut() {
Ok(mut f) => (f)(),
Err(e) => crate::debug_warn!("failed to BorrowMut while executing Effect: {}", e),
}
crate::debug_warn!("executing Effect: popping from stack");
// pop it back off the stack
self.stack.pop();
@ -60,18 +74,40 @@ impl EffectInner {
// this kind of dynamic dependency graph reconstruction may seem silly,
// but is actually more efficient because it avoids resubscribing with Signals
// if they are excluded by some kind of conditional logic within the Effect fn
for dep in self.dependencies.borrow().iter() {
match self.dependencies.try_borrow() {
Ok(dependencies) => {
for dep in dependencies.iter() {
if let Some(dep) = dep.upgrade() {
dep.unsubscribe(for_subscriber.clone());
}
}
}
Err(e) => crate::debug_warn!("failed to BorrowMut while unsubscribing: {}", e),
}
/* for dep in self.dependencies.borrow().iter() {
if let Some(dep) = dep.upgrade() {
dep.unsubscribe(for_subscriber.clone());
}
}
} */
// and clear all our dependencies on Signals; these will be built back up
// by the Signals if/when they are called again
self.dependencies.borrow_mut().clear();
match self.dependencies.try_borrow_mut() {
Ok(mut deps) => deps.clear(),
Err(e) => crate::debug_warn!(
"failed to BorrowMut while clearing dependencies from Effect: {}",
e
),
}
}
pub(crate) fn add_dependency(&self, dep: Weak<dyn EffectDependency>) {
self.dependencies.borrow_mut().push(dep);
match self.dependencies.try_borrow_mut() {
Ok(mut deps) => deps.push(dep),
Err(e) => crate::debug_warn!(
"failed to BorrowMut while clearing dependencies from Effect: {}",
e
),
} // self.dependencies.borrow_mut().push(dep);
}
}
@ -94,3 +130,11 @@ impl Hash for EffectInner {
std::ptr::hash(&self.dependencies, state);
}
}
impl Debug for EffectInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EffectInner")
.field("dependencies", &self.dependencies.borrow().len())
.finish()
}
}

View file

@ -11,6 +11,7 @@ mod scope;
mod scope_arena;
mod signal;
mod spawn;
mod suspense;
pub use effect::*;
pub use resource::*;
@ -18,6 +19,7 @@ pub use root_context::*;
pub use scope::*;
pub use signal::*;
pub use spawn::*;
pub use suspense::*;
#[cfg(test)]
mod tests {
@ -76,3 +78,17 @@ mod tests {
});
}
}
#[macro_export]
macro_rules! debug_warn {
($($x:tt)*) => {
{
#[cfg(debug_assertions)]
{
log::warn!($($x)*)
}
#[cfg(not(debug_assertions))]
{ }
}
}
}

View file

@ -1,67 +1,144 @@
use std::{future::Future, rc::Rc};
use std::{cell::RefCell, future::Future, rc::Rc};
use crate::{spawn_local, ReadSignal, Scope, WriteSignal};
use futures::future::{AbortHandle, Abortable};
pub struct Resource<S, T, Fu>
use crate::{spawn_local, ReadSignal, ReadSignalRef, Scope, SuspenseContext, WriteSignal};
pub enum ResourceState<T> {
Idle,
Pending { abort_handle: AbortHandle },
Ready { data: T },
}
pub struct Resource<'a, S, T, Fu>
where
S: 'static,
S: 'static + Clone,
T: 'static,
Fu: Future<Output = T>,
{
pub data: ReadSignal<Option<T>>,
set_data: WriteSignal<Option<T>>,
state: ReadSignal<ResourceState<T>>,
set_state: WriteSignal<ResourceState<T>>,
source: ReadSignal<S>,
source_memoized: Rc<RefCell<Option<S>>>,
fetcher: Rc<dyn Fn(&S) -> Fu>,
cx: Scope<'a>,
}
impl<S, T, Fu> Resource<S, T, Fu>
impl<'a, S, T, Fu> Clone for Resource<'a, S, T, Fu>
where
S: 'static,
S: 'static + Clone + PartialEq,
T: 'static,
Fu: Future<Output = T>,
{
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
set_state: self.set_state.clone(),
source: self.source.clone(),
source_memoized: Rc::clone(&self.source_memoized),
fetcher: self.fetcher.clone(),
cx: self.cx,
}
}
}
impl<'a, S, T, Fu> Resource<'a, S, T, Fu>
where
S: 'static + Clone + PartialEq,
T: 'static,
Fu: Future<Output = T> + 'static,
{
pub fn new(cx: Scope, source: ReadSignal<S>, fetcher: impl Fn(&S) -> Fu + 'static) -> Self {
pub fn new(cx: Scope<'a>, source: ReadSignal<S>, fetcher: impl Fn(&S) -> Fu + 'static) -> Self {
// create signals to handle response
let (data, set_data) = cx.create_signal_owned(None);
let (state, set_state) = cx.create_signal_owned(ResourceState::Idle);
let fetcher = Rc::new(fetcher);
cx.create_effect({
let source = source.clone();
let set_data = set_data.clone();
let fetcher = Rc::clone(&fetcher);
move || {
let fut = (fetcher)(&source.get());
let set_data = set_data.clone();
spawn_local(async move {
let res = fut.await;
set_data.update(move |data| *data = Some(res));
});
}
});
// return the Resource synchronously
Self {
data,
set_data,
state,
set_state,
source,
source_memoized: Default::default(),
fetcher,
cx,
}
}
pub fn read(&self) -> ReadSignalRef<ResourceState<T>> {
self.cx.create_effect(|| {
// reactivity should only be driven by the source signal
let source = self.source.get();
let source_has_changed = {
let mut prev_source = self.source_memoized.borrow_mut();
let source_has_changed = prev_source.as_ref() != Some(&source);
if source_has_changed {
*prev_source = Some(source.clone());
}
source_has_changed
};
match (source_has_changed, &*self.state.get_untracked()) {
// if it's already loaded or is loading and source hasn't changed, return value when read
(false, ResourceState::Ready { .. } | ResourceState::Pending { .. }) => {
crate::debug_warn!("\nResource::read() called while ResourceState is Ready or Pending");
}
// if source has changed and we have a result, run fetch logic
(true, ResourceState::Ready { .. }) => {
crate::debug_warn!("\nResource::read() called while ResourceState::Ready but source has changed");
self.refetch();
}
// if source has changed and it's loading, abort loading and run fetch logic
(true, ResourceState::Pending { abort_handle}) => {
crate::debug_warn!("\nResource::read() called while ResourceState::Pending but source has changed");
abort_handle.abort();
self.refetch();
}
// if this is the first read, run the logic
(_, ResourceState::Idle) => {
crate::debug_warn!("\nResource::read() called while ResourceState is idle");
self.refetch();
}
}
});
self.state.get()
}
pub fn refetch(&self) {
let suspense_cx = self.cx.use_context::<SuspenseContext>().cloned();
if let Some(context) = &suspense_cx {
context.increment();
}
// actually await the future
let source = self.source.clone();
let set_data = self.set_data.clone();
let set_state = self.set_state.clone();
let fetcher = Rc::clone(&self.fetcher);
let fut = (fetcher)(&source.get());
// get Future from factory function and make it abortable
let fut = (fetcher)(&source.get_untracked());
let (abort_handle, abort_registration) = AbortHandle::new_pair();
let fut = Abortable::new(fut, abort_registration);
// set loading state
set_state.update(|state| *state = ResourceState::Pending { abort_handle });
spawn_local(async move {
let res = fut.await;
set_data.update(move |data| *data = Some(res));
let data = fut.await;
// if future has not been aborted, update state
if let Ok(data) = data {
set_state.update(move |state| *state = ResourceState::Ready { data });
}
// if any case, decrement the read counter
if let Some(suspense_cx) = &suspense_cx {
suspense_cx.decrement();
}
});
}
pub fn mutate(&self, update_fn: impl FnOnce(&mut Option<T>)) {
self.set_data.update(update_fn);
pub fn mutate(&self, update_fn: impl FnOnce(&mut ResourceState<T>)) {
self.set_state.update(update_fn);
}
}

View file

@ -89,13 +89,13 @@ impl<'a, 'b> BoundedScope<'a, 'b> {
self,
source: ReadSignal<S>,
fetcher: impl Fn(&S) -> Fu + 'static,
) -> &'a Resource<S, T, Fu>
) -> Resource<'a, S, T, Fu>
where
S: 'static,
S: 'static + Clone + PartialEq,
T: 'static,
Fu: Future<Output = T> + 'static,
{
self.create_ref(Resource::new(self, source, fetcher))
Resource::new(self, source, fetcher)
}
pub fn child_scope<F>(self, f: F) -> ScopeDisposer<'a>

View file

@ -1,5 +1,5 @@
//use debug_cell::{Ref, RefCell};
use std::{
borrow::Borrow,
cell::{Ref, RefCell},
collections::HashSet,
rc::{Rc, Weak},
@ -61,13 +61,28 @@ impl<T: 'static> ReadSignal<T> {
}
fn add_subscriber(&self, subscriber: Rc<EffectInner>) {
self.inner.subscriptions.borrow_mut().insert(subscriber);
match self.inner.subscriptions.try_borrow_mut() {
Ok(mut subs) => {
subs.insert(subscriber);
}
Err(e) => crate::debug_warn!(
"failed to BorrowMut while adding subscriber to Signal: {}",
e
),
}
//self.inner.subscriptions.borrow_mut().insert(subscriber);
}
}
impl<T> EffectDependency for SignalState<T> {
fn unsubscribe(&self, effect: Rc<EffectInner>) {
self.subscriptions.borrow_mut().remove(&effect);
match self.subscriptions.try_borrow_mut() {
Ok(mut subs) => {
subs.remove(&effect);
}
Err(e) => crate::debug_warn!("failed to unsubscribing Signal from Effect: {}", e),
}
//self.subscriptions.borrow_mut().remove(&effect);
}
}
@ -145,11 +160,41 @@ where
impl<T> WriteSignal<T> {
pub fn update(&self, update_fn: impl FnOnce(&mut T)) {
if let Some(inner) = self.inner.upgrade() {
(update_fn)(&mut inner.value.borrow_mut());
for subscription in inner.subscriptions.take().iter() {
subscription.execute(Rc::downgrade(&subscription))
match inner.value.try_borrow_mut() {
Ok(mut value) => (update_fn)(&mut value),
Err(e) => crate::debug_warn!("failed to BorrowMut while updating Signal: {}", e),
}
//(update_fn)(&mut inner.value.borrow_mut());
match inner.subscriptions.try_borrow() {
Ok(subs) => {
for subscription in subs.iter() {
subscription.execute(Rc::downgrade(&subscription));
}
}
Err(e) => crate::debug_warn!(
"failed to BorrowMut while running dependencies for Signal: {}",
e
),
}
/* for subscription in inner.subscriptions.borrow_mut().iter() {
subscription.execute(Rc::downgrade(&subscription));
} */
/* for subscription in inner.subscriptions.take().iter() {
subscription.execute(Rc::downgrade(&subscription))
} */
}
}
pub fn update_untracked(&self, update_fn: impl FnOnce(&mut T)) {
if let Some(inner) = self.inner.upgrade() {
match inner.value.try_borrow_mut() {
Ok(mut value) => (update_fn)(&mut value),
Err(e) => crate::debug_warn!(
"failed to BorrowMut while calling WriteSignal::update_untracked {}",
e
),
}
} else {
}
}
}

View file

@ -0,0 +1,29 @@
use crate::{ReadSignal, Scope, WriteSignal};
#[derive(Clone)]
pub struct SuspenseContext {
pending_resources: ReadSignal<usize>,
set_pending_resources: WriteSignal<usize>,
}
impl SuspenseContext {
pub fn new(cx: Scope) -> Self {
let (pending_resources, set_pending_resources) = cx.create_signal_owned(0);
Self {
pending_resources,
set_pending_resources,
}
}
pub fn increment(&self) {
self.set_pending_resources.update(|n| *n += 1);
}
pub fn decrement(&self) {
self.set_pending_resources.update(|n| *n -= 1);
}
pub fn ready(&self) -> bool {
*self.pending_resources.get() == 0
}
}