Merge pull request #35 from danheuck/unserializable-resources

Unserializable resources
This commit is contained in:
Greg Johnston 2022-10-23 21:23:36 -04:00 committed by GitHub
commit 88464fefe9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 182 additions and 18 deletions

View File

@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
anyhow = "1"
console_log = "0.2"
leptos = { path = "../../../leptos", default-features = false }
leptos = { path = "../../../leptos", default-features = false, features = ["serde"] }
leptos_meta = { path = "../../../meta", default-features = false }
leptos_router = { path = "../../../router", default-features = false }
log = "0.4"

View File

@ -10,18 +10,24 @@ use std::{
};
use crate::{
create_isomorphic_effect, create_memo, create_signal, queue_microtask, runtime::Runtime,
serialization::Serializable, spawn::spawn_local, use_context, Memo, ReadSignal, Scope,
ScopeProperty, SuspenseContext, WriteSignal,
create_effect, create_isomorphic_effect, create_memo, create_signal, queue_microtask,
runtime::Runtime, serialization::Serializable, spawn::spawn_local, use_context, Memo,
ReadSignal, Scope, ScopeProperty, SuspenseContext, WriteSignal,
};
/// Creates [Resource](crate::Resource), which is a signal that reflects the
/// current state of an asynchronous task, allowing you to integrate
/// `async` [Future]s into the synchronous reactive system.
/// current state of an asynchronous task, allowing you to integrate `async`
/// [Future]s into the synchronous reactive system.
///
/// Takes a `fetcher` function that generates a [Future] when called and a
/// `source` signal that provides the argument for the `fetcher`. Whenever
/// the value of the `source` changes, a new [Future] will be created and run.
/// `source` signal that provides the argument for the `fetcher`. Whenever the
/// value of the `source` changes, a new [Future] will be created and run.
///
/// When server-side rendering is used, the server will handle running the
/// [Future] and will stream the result to the client. This process requires the
/// output type of the Future to be [Serializable]. If your output cannot be
/// serialized, or you just want to make sure the [Future] runs locally, use
/// [create_local_resource()].
///
/// ```
/// # use leptos_reactive::*;
@ -73,6 +79,12 @@ where
/// Creates a [Resource](crate::Resource) with the given initial value, which
/// will only generate and run a [Future] using the `fetcher` when the `source` changes.
///
/// When server-side rendering is used, the server will handle running the
/// [Future] and will stream the result to the client. This process requires the
/// output type of the Future to be [Serializable]. If your output cannot be
/// serialized, or you just want to make sure the [Future] runs locally, use
/// [create_local_resource_with_initial_value()].
pub fn create_resource_with_initial_value<S, T, Fu>(
cx: Scope,
source: impl Fn() -> S + 'static,
@ -105,7 +117,7 @@ where
suspense_contexts: Default::default(),
});
let id = cx.runtime.create_resource(Rc::clone(&r));
let id = cx.runtime.create_serializable_resource(Rc::clone(&r));
create_isomorphic_effect(cx, {
let r = Rc::clone(&r);
@ -124,11 +136,111 @@ where
}
}
/// Creates a _local_ [Resource](crate::Resource), which is a signal that
/// reflects the current state of an asynchronous task, allowing you to
/// integrate `async` [Future]s into the synchronous reactive system.
///
/// Takes a `fetcher` function that generates a [Future] when called and a
/// `source` signal that provides the argument for the `fetcher`. Whenever the
/// value of the `source` changes, a new [Future] will be created and run.
///
/// Unlike [create_resource()], this [Future] is always run on the local system
/// and therefore it's result type does not need to be [Serializable].
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// #[derive(Debug, Clone)] // doesn't implement Serialize, Deserialize
/// struct ComplicatedUnserializableStruct {
/// // something here that can't be serialized
/// }
/// // any old async function; maybe this is calling a REST API or something
/// async fn setup_complicated_struct() -> ComplicatedUnserializableStruct {
/// // do some work
/// ComplicatedUnserializableStruct { }
/// }
///
/// // create the resource that will
/// let result = create_local_resource(cx, move || (), |_| setup_complicated_struct());
/// # }).dispose();
/// ```
pub fn create_local_resource<S, T, Fu>(
cx: Scope,
source: impl Fn() -> S + 'static,
fetcher: impl Fn(S) -> Fu + 'static,
) -> Resource<S, T>
where
S: PartialEq + Debug + Clone + 'static,
T: Debug + Clone + 'static,
Fu: Future<Output = T> + 'static,
{
let initial_value = None;
create_local_resource_with_initial_value(cx, source, fetcher, initial_value)
}
/// Creates a _local_ [Resource](crate::Resource) with the given initial value,
/// which will only generate and run a [Future] using the `fetcher` when the
/// `source` changes.
///
/// Unlike [create_resource_with_initial_value()], this [Future] will always run
/// on the local system and therefore its output type does not need to be
/// [Serializable].
pub fn create_local_resource_with_initial_value<S, T, Fu>(
cx: Scope,
source: impl Fn() -> S + 'static,
fetcher: impl Fn(S) -> Fu + 'static,
initial_value: Option<T>,
) -> Resource<S, T>
where
S: PartialEq + Debug + Clone + 'static,
T: Debug + Clone + 'static,
Fu: Future<Output = T> + 'static,
{
let resolved = initial_value.is_some();
let (value, set_value) = create_signal(cx, initial_value);
let (loading, set_loading) = create_signal(cx, false);
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let source = create_memo(cx, move |_| source());
let r = Rc::new(ResourceState {
scope: cx,
value,
set_value,
loading,
set_loading,
source,
fetcher,
resolved: Rc::new(Cell::new(resolved)),
scheduled: Rc::new(Cell::new(false)),
suspense_contexts: Default::default(),
});
let id = cx.runtime.create_unserializable_resource(Rc::clone(&r));
create_effect(cx, {
let r = Rc::clone(&r);
// This is a local resource, so we're always going to handle it on the
// client
move |_| r.load(false)
});
cx.with_scope_property(|prop| prop.push(ScopeProperty::Resource(id)));
Resource {
runtime: cx.runtime,
id,
source_ty: PhantomData,
out_ty: PhantomData,
}
}
#[cfg(not(feature = "hydrate"))]
fn load_resource<S, T>(_cx: Scope, _id: ResourceId, r: Rc<ResourceState<S, T>>)
where
S: PartialEq + Debug + Clone + 'static,
T: Debug + Clone + Serializable + 'static,
T: Debug + Clone + 'static,
{
r.load(false)
}
@ -143,6 +255,8 @@ where
if let Some(ref mut context) = *cx.runtime.shared_context.borrow_mut() {
if let Some(data) = context.resolved_resources.remove(&id) {
// The server already sent us the serialized resource value, so
// deserialize & set it now
context.pending_resources.remove(&id); // no longer pending
r.resolved.set(true);
@ -153,6 +267,9 @@ where
// for reactivity
_ = r.source.try_with(|n| n.clone());
} else if context.pending_resources.remove(&id) {
// We're still waiting for the resource, add a "resolver" closure so
// that it will be set as soon as the server sends the serialized
// value
r.set_loading.update(|n| *n = true);
let resolve = {
@ -174,7 +291,7 @@ where
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOURCE_RESOLVERS"),
)
.expect_throw("no __LEPTOS_RESOURCE_RESOLVERS found in the JS global scope");
let id = serde_json::to_string(&id).expect_throw("could not deserialize Resource ID");
let id = serde_json::to_string(&id).expect_throw("could not serialize Resource ID");
_ = js_sys::Reflect::set(
&resource_resolvers,
&wasm_bindgen::JsValue::from_str(&id),
@ -184,6 +301,8 @@ where
// for reactivity
_ = r.source.get();
} else {
// Server didn't mark the resource as pending, so load it on the
// client
r.load(false);
}
} else {
@ -421,7 +540,12 @@ where
}
}
pub(crate) trait AnyResource {
pub(crate) enum AnyResource {
Unserializable(Rc<dyn UnserializableResource>),
Serializable(Rc<dyn SerializableResource>),
}
pub(crate) trait SerializableResource {
fn as_any(&self) -> &dyn Any;
#[cfg(feature = "ssr")]
@ -431,7 +555,7 @@ pub(crate) trait AnyResource {
) -> Pin<Box<dyn Future<Output = (ResourceId, String)>>>;
}
impl<S, T> AnyResource for ResourceState<S, T>
impl<S, T> SerializableResource for ResourceState<S, T>
where
S: Debug + Clone,
T: Clone + Debug + Serializable,
@ -449,3 +573,17 @@ where
Box::pin(fut)
}
}
pub(crate) trait UnserializableResource {
fn as_any(&self) -> &dyn Any;
}
impl<S, T> UnserializableResource for ResourceState<S, T>
where
S: Debug + Clone,
T: Clone + Debug,
{
fn as_any(&self) -> &dyn Any {
self
}
}

View File

@ -28,7 +28,7 @@ pub(crate) struct Runtime {
pub signal_subscribers: RefCell<SecondaryMap<SignalId, RefCell<HashSet<EffectId>>>>,
pub effects: RefCell<SlotMap<EffectId, Rc<RefCell<dyn AnyEffect>>>>,
pub effect_sources: RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
pub resources: RefCell<SlotMap<ResourceId, Rc<dyn AnyResource>>>,
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
}
impl Debug for Runtime {
@ -147,12 +147,30 @@ impl Runtime {
Memo(read)
}
pub(crate) fn create_resource<S, T>(&self, state: Rc<ResourceState<S, T>>) -> ResourceId
pub(crate) fn create_unserializable_resource<S, T>(
&self,
state: Rc<ResourceState<S, T>>,
) -> ResourceId
where
S: Debug + Clone + 'static,
T: Debug + Clone + 'static,
{
self.resources
.borrow_mut()
.insert(AnyResource::Unserializable(state))
}
pub(crate) fn create_serializable_resource<S, T>(
&self,
state: Rc<ResourceState<S, T>>,
) -> ResourceId
where
S: Debug + Clone + 'static,
T: Debug + Clone + Serializable + 'static,
{
self.resources.borrow_mut().insert(state)
self.resources
.borrow_mut()
.insert(AnyResource::Serializable(state))
}
#[cfg(all(feature = "hydrate", not(feature = "ssr")))]
@ -194,7 +212,13 @@ impl Runtime {
let resources = self.resources.borrow();
let res = resources.get(id);
if let Some(res) = res {
if let Some(n) = res.as_any().downcast_ref::<ResourceState<S, T>>() {
let res_state = match res {
AnyResource::Unserializable(res) => res.as_any(),
AnyResource::Serializable(res) => res.as_any(),
}
.downcast_ref::<ResourceState<S, T>>();
if let Some(n) = res_state {
f(n)
} else {
panic!(
@ -225,7 +249,9 @@ impl Runtime {
> {
let f = futures::stream::futures_unordered::FuturesUnordered::new();
for (id, resource) in self.resources.borrow().iter() {
f.push(resource.to_serialization_resolver(id));
if let AnyResource::Serializable(resource) = resource {
f.push(resource.to_serialization_resolver(id));
}
}
f
}