Shift from mutually-exclusive features to a more-gracefully-degrading system of features ordered by preference, clean up some warnings, and use cfg_if for improved readability

This commit is contained in:
Greg Johnston 2022-11-02 20:41:00 -04:00
parent 19db83c933
commit 8ea73565de
17 changed files with 491 additions and 480 deletions

View File

@ -160,25 +160,28 @@ mod tests {
#[test]
fn test_map_keyed() {
create_scope(|cx| {
let (rows, set_rows) =
create_signal::<Vec<(usize, ReadSignal<i32>, WriteSignal<i32>)>>(cx, vec![]);
// we can really only run this in SSR mode, so just ignore if we're in CSR or hydrate
if !cfg!(any(feature = "csr", feature = "hydrate")) {
create_scope(|cx| {
let (rows, set_rows) =
create_signal::<Vec<(usize, ReadSignal<i32>, WriteSignal<i32>)>>(cx, vec![]);
let keyed = map_keyed(
cx,
rows,
|cx, row| {
let read = row.1;
create_effect(cx, move |_| println!("row value = {}", read.get()));
},
|row| row.0,
);
let keyed = map_keyed(
cx,
rows,
|cx, row| {
let read = row.1;
create_effect(cx, move |_| println!("row value = {}", read.get()));
},
|row| row.0,
);
create_effect(cx, move |_| println!("keyed = {:#?}", keyed.get()));
create_effect(cx, move |_| println!("keyed = {:#?}", keyed.get()));
let (r, w) = create_signal(cx, 0);
set_rows.update(|n| n.push((0, r, w)));
})
.dispose();
let (r, w) = create_signal(cx, 0);
set_rows.update(|n| n.push((0, r, w)));
})
.dispose();
}
}
}

View File

@ -8,11 +8,12 @@ repository = "https://github.com/gbj/leptos"
description = "DOM operations for the Leptos web framework."
[dependencies]
futures = { version = "0.3", optional = true }
html-escape = { version = "0.2", optional = true }
cfg-if = "1"
futures = "0.3"
html-escape = "0.2"
js-sys = "0.3"
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.11" }
serde_json = { version = "1", optional = true }
serde_json = "1"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4.31"
log = "0.4"
@ -51,11 +52,11 @@ features = [
"Storage",
"Text",
"TreeWalker",
"Window"
"Window",
]
[features]
csr = ["leptos_reactive/csr"]
hydrate = ["leptos_reactive/hydrate"]
ssr = ["leptos_reactive/ssr", "dep:futures", "dep:html-escape", "dep:serde_json"]
stable = ["leptos_reactive/stable"]
ssr = ["leptos_reactive/ssr"]
stable = ["leptos_reactive/stable"]

View File

@ -1,11 +1,8 @@
use cfg_if::cfg_if;
use std::{cell::RefCell, rc::Rc};
use leptos_reactive::Scope;
#[cfg(feature = "stable")]
use leptos_reactive::{Memo, ReadSignal, RwSignal};
#[cfg(not(feature = "ssr"))]
use wasm_bindgen::JsCast;
use crate::Node;
@ -19,7 +16,7 @@ pub enum Child {
}
impl Child {
#[cfg(feature = "ssr")]
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
pub fn as_child_string(&self) -> String {
match self {
Child::Null => String::new(),
@ -83,24 +80,14 @@ impl IntoChild for String {
}
}
#[cfg(not(feature = "ssr"))]
impl IntoChild for web_sys::Node {
fn into_child(self, _cx: Scope) -> Child {
Child::Node(self)
}
}
#[cfg(not(feature = "ssr"))]
impl IntoChild for web_sys::Text {
fn into_child(self, _cx: Scope) -> Child {
Child::Node(self.unchecked_into())
}
}
#[cfg(not(feature = "ssr"))]
impl IntoChild for web_sys::Element {
fn into_child(self, _cx: Scope) -> Child {
Child::Node(self.unchecked_into())
impl<T, U> IntoChild for T
where
T: FnMut() -> U + 'static,
U: IntoChild,
{
fn into_child(mut self, cx: Scope) -> Child {
let modified_fn = Rc::new(RefCell::new(move || (self)().into_child(cx)));
Child::Fn(modified_fn)
}
}
@ -122,49 +109,6 @@ impl IntoChild for Vec<Node> {
}
}
#[cfg(not(feature = "ssr"))]
impl IntoChild for Vec<web_sys::Element> {
fn into_child(self, _cx: Scope) -> Child {
Child::Nodes(
self.into_iter()
.map(|el| el.unchecked_into::<web_sys::Node>())
.collect(),
)
}
}
#[cfg(all(feature = "stable", not(feature = "ssr")))]
impl IntoChild for Memo<Vec<web_sys::Element>> {
fn into_child(self, cx: Scope) -> Child {
(move || self.get()).into_child(cx)
}
}
#[cfg(all(feature = "stable", not(feature = "ssr")))]
impl IntoChild for ReadSignal<Vec<web_sys::Element>> {
fn into_child(self, cx: Scope) -> Child {
(move || self.get()).into_child(cx)
}
}
#[cfg(all(feature = "stable", not(feature = "ssr")))]
impl IntoChild for RwSignal<Vec<web_sys::Element>> {
fn into_child(self, cx: Scope) -> Child {
(move || self.get()).into_child(cx)
}
}
impl<T, U> IntoChild for T
where
T: FnMut() -> U + 'static,
U: IntoChild,
{
fn into_child(mut self, cx: Scope) -> Child {
let modified_fn = Rc::new(RefCell::new(move || (self)().into_child(cx)));
Child::Fn(modified_fn)
}
}
macro_rules! child_type {
($child_type:ty) => {
impl IntoChild for $child_type {
@ -193,3 +137,62 @@ child_type!(f32);
child_type!(f64);
child_type!(char);
child_type!(bool);
cfg_if! {
if #[cfg(any(feature = "hydrate", feature = "csr"))] {
use wasm_bindgen::JsCast;
impl IntoChild for web_sys::Node {
fn into_child(self, _cx: Scope) -> Child {
Child::Node(self)
}
}
impl IntoChild for web_sys::Text {
fn into_child(self, _cx: Scope) -> Child {
Child::Node(self.unchecked_into())
}
}
impl IntoChild for web_sys::Element {
fn into_child(self, _cx: Scope) -> Child {
Child::Node(self.unchecked_into())
}
}
impl IntoChild for Vec<web_sys::Element> {
fn into_child(self, _cx: Scope) -> Child {
Child::Nodes(
self.into_iter()
.map(|el| el.unchecked_into::<web_sys::Node>())
.collect(),
)
}
}
}
}
// `stable` feature
cfg_if! {
if #[cfg(feature = "stable")] {
use leptos_reactive::{Memo, ReadSignal, RwSignal};
impl IntoChild for Memo<Vec<Element>> {
fn into_child(self, cx: Scope) -> Child {
(move || self.get()).into_child(cx)
}
}
impl IntoChild for ReadSignal<Vec<Element>> {
fn into_child(self, cx: Scope) -> Child {
(move || self.get()).into_child(cx)
}
}
impl IntoChild for RwSignal<Vec<Element>> {
fn into_child(self, cx: Scope) -> Child {
(move || self.get()).into_child(cx)
}
}
}
}

View File

@ -1,63 +1,61 @@
use cfg_if::cfg_if;
pub mod attribute;
pub mod child;
pub mod class;
pub mod event_delegation;
pub mod logging;
#[cfg(not(feature = "ssr"))]
pub mod mount;
pub mod operations;
pub mod property;
#[cfg(not(feature = "ssr"))]
pub mod reconcile;
#[cfg(not(feature = "ssr"))]
pub mod render;
#[cfg(feature = "ssr")]
pub mod render_to_string;
cfg_if! {
// can only include this if we're *only* enabling SSR, as it's the lowest-priority feature
// if either `csr` or `hydrate` is enabled, `Element` is a `web_sys::Element` and can't be rendered
if #[cfg(not(any(feature = "hydrate", feature = "csr")))] {
pub type Element = String;
pub type Node = String;
pub mod render_to_string;
pub use render_to_string::*;
} else {
pub type Element = web_sys::Element;
pub type Node = web_sys::Node;
pub mod mount;
pub mod reconcile;
pub mod render;
pub use mount::*;
pub use reconcile::*;
pub use render::*;
}
}
pub use attribute::*;
pub use child::*;
pub use class::*;
pub use logging::*;
#[cfg(not(feature = "ssr"))]
pub use mount::*;
pub use operations::*;
pub use property::*;
#[cfg(not(feature = "ssr"))]
pub use render::*;
#[cfg(feature = "ssr")]
pub use render_to_string::*;
pub use js_sys;
pub use wasm_bindgen;
pub use web_sys;
#[cfg(not(feature = "ssr"))]
pub type Element = web_sys::Element;
#[cfg(feature = "ssr")]
pub type Element = String;
#[cfg(not(feature = "ssr"))]
pub type Node = web_sys::Node;
#[cfg(feature = "ssr")]
pub type Node = String;
use leptos_reactive::Scope;
pub use wasm_bindgen::UnwrapThrowExt;
#[cfg(feature = "csr")]
pub fn create_component<F, T>(cx: Scope, f: F) -> T
where
F: FnOnce() -> T,
{
cx.untrack(f)
}
#[cfg(not(feature = "csr"))]
pub fn create_component<F, T>(cx: Scope, f: F) -> T
where
F: FnOnce() -> T,
{
cx.with_next_context(f)
cfg_if! {
if #[cfg(feature = "csr")] {
cx.untrack(f)
} else {
cx.with_next_context(f)
}
}
}
#[macro_export]

View File

@ -1,84 +1,6 @@
use cfg_if::cfg_if;
use std::borrow::Cow;
use leptos_reactive::*;
use crate::Element;
use futures::{stream::FuturesUnordered, Stream, StreamExt};
pub fn render_to_stream(view: impl Fn(Scope) -> Element + 'static) -> impl Stream<Item = String> {
let ((shell, pending_resources, pending_fragments, serializers), _, disposer) =
run_scope_undisposed({
move |cx| {
// the actual app body/template code
// this does NOT contain any of the data being loaded asynchronously in resources
let shell = view(cx);
let resources = cx.all_resources();
let pending_resources = serde_json::to_string(&resources).unwrap();
(
shell,
pending_resources,
cx.pending_fragments(),
cx.serialization_resolvers(),
)
}
});
let fragments = FuturesUnordered::new();
for (fragment_id, fut) in pending_fragments {
fragments.push(async move { (fragment_id, fut.await) })
}
// HTML for the view function and script to store resources
futures::stream::once(async move {
format!(
r#"
{shell}
<script>
__LEPTOS_PENDING_RESOURCES = {pending_resources};
__LEPTOS_RESOLVED_RESOURCES = new Map();
__LEPTOS_RESOURCE_RESOLVERS = new Map();
</script>
"#
)
})
// stream data for each Resource as it resolves
.chain(serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
format!(
r#"<script>
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
console.log("(create_resource) calling resolver");
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
}} else {{
console.log("(create_resource) saving data for resource creation");
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
}}
</script>"#,
)
}))
// stream HTML for each <Suspense/> as it resolves
.chain(fragments.map(|(fragment_id, html)| {
format!(
r#"
<template id="{fragment_id}">{html}</template>
<script>
var frag = document.querySelector(`[data-fragment-id="{fragment_id}"]`);
var tpl = document.getElementById("{fragment_id}");
console.log("replace", frag, "with", tpl.content.cloneNode(true));
frag.replaceWith(tpl.content.cloneNode(true));
</script>
"#
)
}))
// dispose of Scope
.chain(futures::stream::once(async {
disposer.dispose();
Default::default()
}))
}
pub fn escape_text(text: &str) -> Cow<'_, str> {
html_escape::encode_text(text)
}
@ -86,3 +8,86 @@ pub fn escape_text(text: &str) -> Cow<'_, str> {
pub fn escape_attr(text: &str) -> Cow<'_, str> {
html_escape::encode_double_quoted_attribute(text)
}
cfg_if! {
if #[cfg(feature = "ssr")] {
use leptos_reactive::*;
use crate::Element;
use futures::{stream::FuturesUnordered, Stream, StreamExt};
pub fn render_to_stream(view: impl Fn(Scope) -> Element + 'static) -> impl Stream<Item = String> {
let ((shell, pending_resources, pending_fragments, serializers), _, disposer) =
run_scope_undisposed({
move |cx| {
// the actual app body/template code
// this does NOT contain any of the data being loaded asynchronously in resources
let shell = view(cx);
let resources = cx.all_resources();
let pending_resources = serde_json::to_string(&resources).unwrap();
(
shell,
pending_resources,
cx.pending_fragments(),
cx.serialization_resolvers(),
)
}
});
let fragments = FuturesUnordered::new();
for (fragment_id, fut) in pending_fragments {
fragments.push(async move { (fragment_id, fut.await) })
}
// HTML for the view function and script to store resources
futures::stream::once(async move {
format!(
r#"
{shell}
<script>
__LEPTOS_PENDING_RESOURCES = {pending_resources};
__LEPTOS_RESOLVED_RESOURCES = new Map();
__LEPTOS_RESOURCE_RESOLVERS = new Map();
</script>
"#
)
})
// stream data for each Resource as it resolves
.chain(serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
format!(
r#"<script>
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
console.log("(create_resource) calling resolver");
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
}} else {{
console.log("(create_resource) saving data for resource creation");
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
}}
</script>"#,
)
}))
// stream HTML for each <Suspense/> as it resolves
.chain(fragments.map(|(fragment_id, html)| {
format!(
r#"
<template id="{fragment_id}">{html}</template>
<script>
var frag = document.querySelector(`[data-fragment-id="{fragment_id}"]`);
var tpl = document.getElementById("{fragment_id}");
console.log("replace", frag, "with", tpl.content.cloneNode(true));
frag.replaceWith(tpl.content.cloneNode(true));
</script>
"#
)
}))
// dispose of Scope
.chain(futures::stream::once(async {
disposer.dispose();
Default::default()
}))
}
}
}

View File

@ -13,14 +13,17 @@ pub(crate) enum Mode {
impl Default for Mode {
fn default() -> Self {
if cfg!(feature = "ssr") {
Mode::Ssr
} else if cfg!(feature = "hydrate") {
// what's the deal with this order of priority?
// basically, it's fine for the server to compile wasm-bindgen, but it will panic if it runs it
// for the sake of testing, we need to fall back to `ssr` if no flags are enabled
// if you have `hydrate` enabled, you definitely want that rather than `csr`
// if you have both `csr` and `ssr` we assume you want the browser
if cfg!(feature = "hydrate") {
Mode::Hydrate
} else if cfg!(feature = "csr") {
Mode::Client
} else {
panic!("one of the features leptos/ssr, leptos/hydrate, or leptos/csr needs to be set")
Mode::Ssr
}
}
}
@ -36,35 +39,46 @@ mod server;
/// same rules as HTML, with the following differences:
/// 1. Text content should be provided as a Rust string, i.e., double-quoted:
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// view! { cx, <p>"Heres some text"</p> }
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// view! { cx, <p>"Heres some text"</p> };
/// # }
/// # });
/// ```
///
/// 2. Self-closing tags need an explicit `/` as in XML/XHTML
/// ```rust,compile_fail
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// // ❌ not like this
/// view! { cx, <input type="text" name="name"> }
/// # ;
/// # }
/// # });
/// ```
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// // ✅ add that slash
/// view! { cx, <input type="text" name="name" /> }
/// # ;
/// # }
/// # });
/// ```
///
/// 3. Components (functions annotated with `#[component]`) can be inserted as camel-cased tags
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::*; use typed_builder::TypedBuilder;
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::*; use typed_builder::TypedBuilder; use leptos_dom::wasm_bindgen::JsCast; use leptos_dom as leptos; use leptos_dom::Marker;
/// # #[derive(TypedBuilder)] struct CounterProps { initial_value: i32 }
/// # fn Counter(cx: Scope, props: CounterProps) -> Element { view! { cx, <p></p>} }
/// # run_scope(|cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// view! { cx, <div><Counter initial_value=3 /></div> }
/// # ;
/// # }
/// # });
/// ```
///
@ -73,8 +87,9 @@ mod server;
/// *(“Signal” here means `Fn() -> T` where `T` is the appropriate type for that node: a `String` in case
/// of text nodes, a `bool` for `class:` attributes, etc.)*
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast; use leptos_dom as leptos; use leptos_dom::Marker;
/// # run_scope(|cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (count, set_count) = create_signal(cx, 0);
///
/// view! {
@ -85,13 +100,16 @@ mod server;
/// "Double Count: " {move || count() % 2} // or derive a signal inline
/// </div>
/// }
/// # ;
/// # }
/// # });
/// ```
///
/// 5. Event handlers can be added with `on:` attributes
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// view! {
/// cx,
/// <button on:click=|ev: web_sys::Event| {
@ -100,14 +118,17 @@ mod server;
/// "Click me"
/// </button>
/// }
/// # ;
/// # }
/// # });
/// ```
///
/// 6. DOM properties can be set with `prop:` attributes, which take any primitive type or `JsValue` (or a signal
/// that returns a primitive or JsValue).
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
///
/// view! {
@ -120,22 +141,28 @@ mod server;
/// on:click=move |ev| set_name(event_target_value(&ev)) // `event_target_value` is a useful little Leptos helper
/// />
/// }
/// # ;
/// # }
/// # });
/// ```
///
/// 7. Classes can be toggled with `class:` attributes, which take a `bool` (or a signal that returns a `bool`).
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (count, set_count) = create_signal(cx, 2);
/// view! { cx, <div class:hidden={move || count() < 3}>"Now you see me, now you dont."</div> }
/// # ;
/// # }
/// # });
/// ```
///
/// Heres a simple example that shows off several of these features, put together
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::*;
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::*; use leptos_dom as leptos; use leptos_dom::Marker; use leptos_dom::wasm_bindgen::JsCast;
///
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// pub fn SimpleCounter(cx: Scope) -> Element {
/// // create a reactive signal with the initial value
/// let (value, set_value) = create_signal(cx, 0);
@ -157,6 +184,8 @@ mod server;
/// </div>
/// }
/// }
/// # ;
/// # }
/// ```
#[proc_macro]
pub fn view(tokens: TokenStream) -> TokenStream {

View File

@ -1,11 +1,11 @@
// Credit to Dioxus: https://github.com/DioxusLabs/dioxus/blob/master/packages/core-macro/src/Server.rs
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
use proc_macro2::{TokenStream as TokenStream2};
use quote::{quote};
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
*, token::Type,
*
};
pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Result<TokenStream2> {

View File

@ -10,19 +10,25 @@ description = "Reactive system for the Leptos web framework."
[dependencies]
log = "0.4"
slotmap = { version = "1", features = ["serde"] }
serde = { version = "1", optional = true, features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde-lite = { version = "0.3", optional = true }
futures = { version = "0.3" }
js-sys = { version = "0.3", optional = true }
js-sys = "0.3"
miniserde = { version = "0.1", optional = true }
serde-wasm-bindgen = { version = "0.4", optional = true }
serde_json = { version = "1" }
base64 = { version = "0.13", optional = true }
serde-wasm-bindgen = "0.4"
serde_json = "1"
base64 = "0.13"
thiserror = "1"
tokio = { version = "1", features = ["rt"], optional = true }
wasm-bindgen = { version = "0.2", optional = true }
wasm-bindgen-futures = { version = "0.4", optional = true }
web-sys = { version = "0.3", optional = true, features = ["Element"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = [
"DocumentFragment",
"Element",
"HtmlTemplateElement",
"NodeList",
"Window",
] }
cfg-if = "1.0.0"
[dev-dependencies]
@ -30,17 +36,10 @@ tokio-test = "0.4"
[features]
default = []
csr = ["dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:web-sys"]
hydrate = [
"dep:base64",
"dep:js-sys",
"dep:serde-wasm-bindgen",
"dep:wasm-bindgen",
"dep:wasm-bindgen-futures",
"dep:web-sys",
]
ssr = ["dep:base64", "dep:tokio"]
csr = []
hydrate = []
ssr = ["dep:tokio"]
stable = []
serde = ["dep:serde"]
serde = []
serde-lite = ["dep:serde-lite"]
miniserde = ["dep:miniserde"]
miniserde = ["dep:miniserde"]

View File

@ -1,28 +1,18 @@
#[cfg(any(feature = "hydrate", feature = "ssr"))]
use std::collections::HashMap;
#[cfg(feature = "hydrate")]
use std::collections::HashSet;
#[cfg(feature = "ssr")]
use std::{future::Future, pin::Pin};
#[cfg(any(feature = "hydrate"))]
use crate::ResourceId;
use std::{
collections::{HashMap, HashSet},
future::Future,
pin::Pin,
};
#[derive(Default)]
pub struct SharedContext {
#[cfg(feature = "hydrate")]
pub completed: Vec<web_sys::Element>,
#[cfg(feature = "hydrate")]
pub events: Vec<()>,
pub context: Option<HydrationContext>,
#[cfg(feature = "hydrate")]
pub registry: HashMap<String, web_sys::Element>,
#[cfg(feature = "hydrate")]
pub pending_resources: HashSet<ResourceId>,
#[cfg(feature = "hydrate")]
pub resolved_resources: HashMap<ResourceId, String>,
#[cfg(feature = "ssr")]
pub pending_fragments: HashMap<String, Pin<Box<dyn Future<Output = String>>>>,
}
@ -32,7 +22,6 @@ impl std::fmt::Debug for SharedContext {
}
}
#[cfg(all(feature = "hydrate", not(feature = "ssr")))]
impl PartialEq for SharedContext {
fn eq(&self, other: &Self) -> bool {
self.completed == other.completed
@ -44,24 +33,10 @@ impl PartialEq for SharedContext {
}
}
#[cfg(feature = "ssr")]
impl PartialEq for SharedContext {
fn eq(&self, other: &Self) -> bool {
self.context == other.context
}
}
#[cfg(not(any(feature = "ssr", feature = "hydrate")))]
impl PartialEq for SharedContext {
fn eq(&self, other: &Self) -> bool {
self.context == other.context
}
}
impl Eq for SharedContext {}
impl SharedContext {
#[cfg(all(feature = "hydrate", not(feature = "ssr")))]
#[cfg(feature = "hydrate")]
pub fn new_with_registry(registry: HashMap<String, web_sys::Element>) -> Self {
let pending_resources = js_sys::Reflect::get(
&web_sys::window().unwrap(),
@ -91,6 +66,7 @@ impl SharedContext {
registry,
pending_resources,
resolved_resources,
pending_fragments: Default::default(),
}
}

View File

@ -100,6 +100,7 @@ where
.try_with(|n| f(n.as_ref().expect("Memo is missing its initial value")))
}
#[cfg(feature = "hydrate")]
pub(crate) fn subscribe(&self) {
self.0.subscribe()
}

View File

@ -42,6 +42,9 @@ use crate::{
/// let (how_many_cats, set_how_many_cats) = create_signal(cx, 1);
///
/// // create a resource that will refetch whenever `how_many_cats` changes
/// # // `csr`, `hydrate`, and `ssr` all have issues here
/// # // because we're not running in a browser or in Tokio. Let's just ignore it.
/// # if false {
/// let cats = create_resource(cx, how_many_cats, fetch_cat_picture_urls);
///
/// // when we read the signal, it contains either
@ -52,6 +55,7 @@ use crate::{
/// // when the signal's value changes, the `Resource` will generate and run a new `Future`
/// set_how_many_cats(2);
/// assert_eq!(cats(), Some(vec!["2".to_string()]));
/// # }
/// # }).dispose();
/// ```
pub fn create_resource<S, T, Fu>(
@ -160,8 +164,10 @@ where
/// ComplicatedUnserializableStruct { }
/// }
///
/// // create the resource that will
/// // create the resource; it will run but not be serialized
/// # if cfg!(not(any(feature = "csr", feature = "hydrate"))) {
/// let result = create_local_resource(cx, move || (), |_| setup_complicated_struct());
/// # }
/// # }).dispose();
/// ```
pub fn create_local_resource<S, T, Fu>(

View File

@ -3,6 +3,7 @@ use crate::{
EffectId, Memo, ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
ScopeProperty, SignalId, WriteSignal,
};
use cfg_if::cfg_if;
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
use std::{
any::{Any, TypeId},
@ -13,6 +14,15 @@ use std::{
rc::Rc,
};
cfg_if! {
if #[cfg(feature = "ssr")] {
use std::{future::Future, pin::Pin};
use futures::stream::FuturesUnordered;
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
}
}
#[derive(Default)]
pub(crate) struct Runtime {
pub shared_context: RefCell<Option<SharedContext>>,
@ -177,7 +187,7 @@ impl Runtime {
.insert(AnyResource::Serializable(state))
}
#[cfg(all(feature = "hydrate", not(feature = "ssr")))]
#[cfg(feature = "hydrate")]
pub fn start_hydration(&self, element: &web_sys::Element) {
use wasm_bindgen::{JsCast, UnwrapThrowExt};
@ -245,13 +255,11 @@ impl Runtime {
.collect()
}
#[cfg(all(feature = "ssr"))]
#[cfg(feature = "ssr")]
pub(crate) fn serialization_resolvers(
&self,
) -> futures::stream::futures_unordered::FuturesUnordered<
std::pin::Pin<Box<dyn futures::Future<Output = (ResourceId, String)>>>,
> {
let f = futures::stream::futures_unordered::FuturesUnordered::new();
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
let f = FuturesUnordered::new();
for (id, resource) in self.resources.borrow().iter() {
if let AnyResource::Serializable(resource) = resource {
f.push(resource.to_serialization_resolver(id));

View File

@ -1,9 +1,15 @@
#[cfg(feature = "ssr")]
use crate::SuspenseContext;
use cfg_if::cfg_if;
use crate::{hydration::SharedContext, EffectId, ResourceId, Runtime, SignalId};
use std::fmt::Debug;
#[cfg(feature = "ssr")]
use std::{collections::HashMap, future::Future, pin::Pin};
cfg_if! {
if #[cfg(feature = "ssr")] {
use crate::{PinnedFuture, SuspenseContext};
use futures::stream::FuturesUnordered;
use std::{collections::HashMap, future::Future, pin::Pin};
}
}
#[must_use = "Scope will leak memory if the disposer function is never called"]
/// Creates a child reactive scope and runs the function within it. This is useful for applications
@ -184,58 +190,58 @@ impl ScopeDisposer {
}
impl Scope {
#[cfg(feature = "hydrate")]
pub fn is_hydrating(&self) -> bool {
self.runtime.shared_context.borrow().is_some()
}
// hydration-specific code
cfg_if! {
if #[cfg(feature = "hydrate")] {
pub fn is_hydrating(&self) -> bool {
self.runtime.shared_context.borrow().is_some()
}
#[cfg(all(feature = "hydrate", not(feature = "ssr")))]
pub fn start_hydration(&self, element: &web_sys::Element) {
self.runtime.start_hydration(element);
}
pub fn start_hydration(&self, element: &web_sys::Element) {
self.runtime.start_hydration(element);
}
#[cfg(feature = "hydrate")]
pub fn end_hydration(&self) {
self.runtime.end_hydration();
}
pub fn end_hydration(&self) {
self.runtime.end_hydration();
}
#[cfg(feature = "hydrate")]
pub fn get_next_element(&self, template: &web_sys::Element) -> web_sys::Element {
//log::debug!("get_next_element");
use wasm_bindgen::{JsCast, UnwrapThrowExt};
pub fn get_next_element(&self, template: &web_sys::Element) -> web_sys::Element {
use wasm_bindgen::{JsCast, UnwrapThrowExt};
let cloned_template = |t: &web_sys::Element| {
let t = t
.unchecked_ref::<web_sys::HtmlTemplateElement>()
.content()
.clone_node_with_deep(true)
.expect_throw("(get_next_element) could not clone template")
.unchecked_into::<web_sys::Element>()
.first_element_child()
.expect_throw("(get_next_element) could not get first child of template");
t
};
let cloned_template = |t: &web_sys::Element| {
let t = t
.unchecked_ref::<web_sys::HtmlTemplateElement>()
.content()
.clone_node_with_deep(true)
.expect_throw("(get_next_element) could not clone template")
.unchecked_into::<web_sys::Element>()
.first_element_child()
.expect_throw("(get_next_element) could not get first child of template");
t
};
if let Some(ref mut shared_context) = &mut *self.runtime.shared_context.borrow_mut() {
if shared_context.context.is_some() {
let key = shared_context.next_hydration_key();
let node = shared_context.registry.remove(&key.to_string());
if let Some(ref mut shared_context) = &mut *self.runtime.shared_context.borrow_mut() {
if shared_context.context.is_some() {
let key = shared_context.next_hydration_key();
let node = shared_context.registry.remove(&key);
//log::debug!("(hy) searching for {key}");
//log::debug!("(hy) searching for {key}");
if let Some(node) = node {
//log::debug!("(hy) found {key}");
shared_context.completed.push(node.clone());
node
if let Some(node) = node {
//log::debug!("(hy) found {key}");
shared_context.completed.push(node.clone());
node
} else {
//log::debug!("(hy) did NOT find {key}");
cloned_template(template)
}
} else {
cloned_template(template)
}
} else {
//log::debug!("(hy) did NOT find {key}");
cloned_template(template)
}
} else {
cloned_template(template)
}
} else {
cloned_template(template)
}
}
@ -327,62 +333,58 @@ impl Scope {
self.runtime.all_resources()
}
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
#[cfg(feature = "ssr")]
pub fn serialization_resolvers(
&self,
) -> futures::stream::futures_unordered::FuturesUnordered<
std::pin::Pin<Box<dyn futures::Future<Output = (ResourceId, String)>>>,
> {
self.runtime.serialization_resolvers()
}
cfg_if! {
if #[cfg(feature = "ssr")] {
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
pub fn serialization_resolvers(&self) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
self.runtime.serialization_resolvers()
}
#[cfg(feature = "ssr")]
pub fn current_fragment_key(&self) -> String {
self.runtime
.shared_context
.borrow()
.as_ref()
.map(|context| context.current_fragment_key())
.unwrap_or_else(|| String::from("0f"))
}
pub fn current_fragment_key(&self) -> String {
self.runtime
.shared_context
.borrow()
.as_ref()
.map(|context| context.current_fragment_key())
.unwrap_or_else(|| String::from("0f"))
}
#[cfg(feature = "ssr")]
pub fn register_suspense(
&self,
context: SuspenseContext,
key: &str,
resolver: impl FnOnce() -> String + 'static,
) {
use crate::create_isomorphic_effect;
use futures::StreamExt;
pub fn register_suspense(
&self,
context: SuspenseContext,
key: &str,
resolver: impl FnOnce() -> String + 'static,
) {
use crate::create_isomorphic_effect;
use futures::StreamExt;
if let Some(ref mut shared_context) = *self.runtime.shared_context.borrow_mut() {
let (mut tx, mut rx) = futures::channel::mpsc::channel::<()>(1);
if let Some(ref mut shared_context) = *self.runtime.shared_context.borrow_mut() {
let (mut tx, mut rx) = futures::channel::mpsc::channel::<()>(1);
create_isomorphic_effect(*self, move |_| {
let pending = context.pending_resources.try_with(|n| *n).unwrap_or(0);
if pending == 0 {
_ = tx.try_send(());
create_isomorphic_effect(*self, move |_| {
let pending = context.pending_resources.try_with(|n| *n).unwrap_or(0);
if pending == 0 {
_ = tx.try_send(());
}
});
shared_context.pending_fragments.insert(
key.to_string(),
Box::pin(async move {
rx.next().await;
resolver()
}),
);
}
});
}
shared_context.pending_fragments.insert(
key.to_string(),
Box::pin(async move {
rx.next().await;
resolver()
}),
);
}
}
#[cfg(feature = "ssr")]
pub fn pending_fragments(&self) -> HashMap<String, Pin<Box<dyn Future<Output = String>>>> {
if let Some(ref mut shared_context) = *self.runtime.shared_context.borrow_mut() {
std::mem::replace(&mut shared_context.pending_fragments, HashMap::new())
} else {
HashMap::new()
pub fn pending_fragments(&self) -> HashMap<String, Pin<Box<dyn Future<Output = String>>>> {
if let Some(ref mut shared_context) = *self.runtime.shared_context.borrow_mut() {
std::mem::take(&mut shared_context.pending_fragments)
} else {
HashMap::new()
}
}
}
}
}

View File

@ -11,7 +11,7 @@ use crate::{create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSig
/// because it reduces them from `O(n)` to `O(1)`.
///
/// ```
/// # use leptos_reactive::{create_effect, create_scope, create_selector, create_signal};
/// # use leptos_reactive::{create_isomorphic_effect, create_scope, create_selector, create_signal};
/// # use std::rc::Rc;
/// # use std::cell::RefCell;
/// # create_scope(|cx| {
@ -19,7 +19,7 @@ use crate::{create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSig
/// let is_selected = create_selector(cx, a);
/// let total_notifications = Rc::new(RefCell::new(0));
/// let not = Rc::clone(&total_notifications);
/// create_effect(cx, {let is_selected = is_selected.clone(); move |_| {
/// create_isomorphic_effect(cx, {let is_selected = is_selected.clone(); move |_| {
/// if is_selected(5) {
/// *not.borrow_mut() += 1;
/// }
@ -91,7 +91,7 @@ where
let (read, _) = subs
.entry(key.clone())
.or_insert_with(|| create_signal(cx, false));
_ = read.try_with(|n| n.clone());
_ = read.try_with(|n| *n);
f(&key, v.borrow().as_ref().unwrap())
}
}

View File

@ -1,3 +1,4 @@
use cfg_if::cfg_if;
use std::rc::Rc;
use thiserror::Error;
@ -18,82 +19,63 @@ where
fn from_json(json: &str) -> Result<Self, SerializationError>;
}
#[cfg(all(
feature = "serde",
not(feature = "miniserde"),
not(feature = "serde-lite")
))]
use serde::{de::DeserializeOwned, Serialize};
cfg_if! {
// prefer miniserde if it's chosen
if #[cfg(feature = "miniserde")] {
use miniserde::{json, Deserialize, Serialize};
impl<T> Serializable for T
where
T: Serialize + Deserialize,
{
fn to_json(&self) -> Result<String, SerializationError> {
Ok(json::to_string(&self))
}
fn from_json(json: &str) -> Result<Self, SerializationError> {
json::from_str(&json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
}
}
#[cfg(all(
feature = "serde",
not(feature = "miniserde"),
not(feature = "serde-lite")
))]
impl<T> Serializable for T
where
T: DeserializeOwned + Serialize,
{
fn to_json(&self) -> Result<String, SerializationError> {
serde_json::to_string(&self).map_err(|e| SerializationError::Serialize(Rc::new(e)))
}
// use serde-lite if enabled
else if #[cfg(feature = "serde-lite")] {
use serde_lite::{Deserialize, Serialize};
fn from_json(json: &str) -> Result<Self, SerializationError> {
serde_json::from_str(&json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
}
}
#[cfg(all(
feature = "serde-lite",
not(feature = "serde"),
not(feature = "miniserde")
))]
use serde_lite::{Deserialize, Serialize};
#[cfg(all(
feature = "serde-lite",
not(feature = "serde"),
not(feature = "miniserde")
))]
impl<T> Serializable for T
where
T: Serialize + Deserialize,
{
fn to_json(&self) -> Result<String, SerializationError> {
let intermediate = self
.serialize()
.map_err(|e| SerializationError::Serialize(Rc::new(e)))?;
serde_json::to_string(&intermediate).map_err(|e| SerializationError::Serialize(Rc::new(e)))
}
fn from_json(json: &str) -> Result<Self, SerializationError> {
let intermediate =
serde_json::from_str(&json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))?;
Self::deserialize(&intermediate).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
}
}
#[cfg(all(
feature = "miniserde",
not(feature = "serde-lite"),
not(feature = "serde")
))]
use miniserde::{json, Deserialize, Serialize};
#[cfg(all(
feature = "miniserde",
not(feature = "serde-lite"),
not(feature = "serde")
))]
impl<T> Serializable for T
where
T: Serialize + Deserialize,
{
fn to_json(&self) -> Result<String, SerializationError> {
Ok(json::to_string(&self))
}
fn from_json(json: &str) -> Result<Self, SerializationError> {
json::from_str(&json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
impl<T> Serializable for T
where
T: Serialize + Deserialize,
{
fn to_json(&self) -> Result<String, SerializationError> {
let intermediate = self
.serialize()
.map_err(|e| SerializationError::Serialize(Rc::new(e)))?;
serde_json::to_string(&intermediate).map_err(|e| SerializationError::Serialize(Rc::new(e)))
}
fn from_json(json: &str) -> Result<Self, SerializationError> {
let intermediate =
serde_json::from_str(&json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))?;
Self::deserialize(&intermediate).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
}
}
}
// otherwise, or if serde is chosen, default to serde
else {
use serde::{de::DeserializeOwned, Serialize};
impl<T> Serializable for T
where
T: DeserializeOwned + Serialize,
{
fn to_json(&self) -> Result<String, SerializationError> {
serde_json::to_string(&self).map_err(|e| SerializationError::Serialize(Rc::new(e)))
}
fn from_json(json: &str) -> Result<Self, SerializationError> {
serde_json::from_str(json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
}
}
}
}

View File

@ -150,6 +150,7 @@ where
self.id.with_no_subscription(self.runtime, f)
}
#[cfg(feature = "hydrate")]
pub(crate) fn subscribe(&self) {
self.id.subscribe(self.runtime);
}
@ -477,14 +478,6 @@ where
self.id.with(self.runtime, f)
}
pub(crate) fn with_no_subscription<U>(&self, f: impl FnOnce(&T) -> U) -> U {
self.id.with_no_subscription(self.runtime, f)
}
pub(crate) fn subscribe(&self) {
self.id.subscribe(self.runtime);
}
/// Clones and returns the current value of the signal, and subscribes
/// the running effect to this signal.
/// ```

View File

@ -1,25 +1,29 @@
use cfg_if::cfg_if;
use std::future::Future;
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
/// in the browser, and simply runs the given function when on the server.
#[cfg(not(target_arch = "wasm32"))]
pub fn queue_microtask(task: impl FnOnce()) {
task();
}
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
/// in the browser, and simply runs the given function when on the server.
pub fn queue_microtask(task: impl FnOnce() + 'static) {
microtask(wasm_bindgen::closure::Closure::once_into_js(task));
}
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
/// in the browser, and simply runs the given function when on the server.
#[cfg(target_arch = "wasm32")]
pub fn queue_microtask(task: impl FnOnce() + 'static) {
microtask(wasm_bindgen::closure::Closure::once_into_js(task));
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen(
inline_js = "export function microtask(f) { queueMicrotask(f); }"
)]
extern "C" {
fn microtask(task: wasm_bindgen::JsValue);
#[cfg(any(feature = "csr", feature = "hydrate"))]
#[wasm_bindgen::prelude::wasm_bindgen(
inline_js = "export function microtask(f) { queueMicrotask(f); }"
)]
extern "C" {
fn microtask(task: wasm_bindgen::JsValue);
}
} else {
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
/// in the browser, and simply runs the given function when on the server.
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
pub fn queue_microtask(task: impl FnOnce()) {
task();
}
}
}
pub fn spawn_local<F>(fut: F)
@ -29,11 +33,12 @@ where
cfg_if::cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
wasm_bindgen_futures::spawn_local(fut)
}
else if #[cfg(any(test, doctest))] {
tokio_test::block_on(fut);
} else if #[cfg(feature = "ssr")] {
tokio::task::spawn_local(fut);
} else if #[cfg(any(test, doctest))] {
tokio_test::block_on(fut);
} else {
} else {
futures::executor::block_on(fut)
}
}