Skip to content

Commit

Permalink
use_prepared_state & use_transitive_state (#2650)
Browse files Browse the repository at this point in the history
* Some initial implementation.

* Read prepared state during hydration.

* Decode each state with bincode.

* Feature gate prepared state.

* Update documentation.

* Switch from base64 to String.

* cargo +nightly fmt.

* Fix test.

* Add some tests.

* Minor adjustments.

* Remove unused marker.

* Update example.

* Add use_transitive_state.

* Remove unused dead code notation.

* Opt for better code size.

* Add tests for use_transitive_state.

* Fix cargo fmt.

* Fix rustdoc.

* Asynchronously decode data during hydration.

* Fix feature flags.

* Fix docs.

* Feature flags on ssr_router.

* Adjust workflow to reflect feature flags.

* Fix features.

* Restore wasm-bindgen-futures to be wasm32 only.

* Revert wasm-bindgen-futures.

* Second attempt to remove wasm-bindgen-futures.

* Remove spaces as well.

* Address reviews.

* Better diagnostic message.

* Update diagnostic messages.
  • Loading branch information
futursolo committed May 24, 2022
1 parent 027ab6a commit b29b453
Show file tree
Hide file tree
Showing 36 changed files with 1,555 additions and 78 deletions.
9 changes: 4 additions & 5 deletions .github/workflows/main-checks.yml
Expand Up @@ -25,7 +25,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets -- -D warnings
args: --all-targets --all-features -- -D warnings

- name: Lint feature soundness
run: |
Expand All @@ -51,12 +51,11 @@ jobs:

- uses: Swatinem/rust-cache@v1


- name: Run clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --release -- -D warnings
args: --all-targets --all-features --release -- -D warnings

- name: Lint feature soundness
run: |
Expand Down Expand Up @@ -108,7 +107,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --doc --workspace --exclude yew --exclude changelog --exclude website-test --target wasm32-unknown-unknown
args: --doc --workspace --exclude yew --exclude changelog --exclude website-test --exclude ssr_router --exclude simple_ssr --target wasm32-unknown-unknown

- name: Run website code snippet tests
uses: actions-rs/cargo@v1
Expand Down Expand Up @@ -196,7 +195,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --all-targets --workspace --exclude yew --exclude website-test
args: --all-targets --workspace --exclude yew --exclude website-test --exclude ssr_router --exclude simple_ssr

- name: Run native tests for yew
uses: actions-rs/cargo@v1
Expand Down
6 changes: 5 additions & 1 deletion examples/simple_ssr/Cargo.toml
Expand Up @@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
yew = { path = "../../packages/yew", features = ["ssr", "hydration"] }
yew = { path = "../../packages/yew" }
reqwest = { version = "0.11.8", features = ["json"] }
serde = { version = "1.0.132", features = ["derive"] }
uuid = { version = "1.0.0", features = ["serde"] }
Expand All @@ -23,3 +23,7 @@ num_cpus = "1.13"
tokio-util = { version = "0.7", features = ["rt"] }
once_cell = "1.5"
clap = { version = "3.1.7", features = ["derive"] }

[features]
hydration = ["yew/hydration"]
ssr = ["yew/ssr", "yew/tokio"]
2 changes: 1 addition & 1 deletion examples/simple_ssr/README.md
Expand Up @@ -10,7 +10,7 @@ This example demonstrates server-side rendering.

2. Run the server

`cargo run --bin simple_ssr_server -- --dir examples/simple_ssr/dist`
`cargo run --features=ssr --bin simple_ssr_server -- --dir examples/simple_ssr/dist`

3. Open Browser

Expand Down
2 changes: 1 addition & 1 deletion examples/simple_ssr/index.html
Expand Up @@ -4,6 +4,6 @@
<meta charset="utf-8" />
<title>Yew SSR Example</title>

<link data-trunk rel="rust" data-bin="simple_ssr_hydrate" />
<link data-trunk rel="rust" data-bin="simple_ssr_hydrate" data-cargo-features="hydration" />
</head>
</html>
58 changes: 2 additions & 56 deletions examples/simple_ssr/src/lib.rs
@@ -1,20 +1,13 @@
use std::cell::RefCell;
use std::rc::Rc;

use serde::{Deserialize, Serialize};
#[cfg(not(target_arch = "wasm32"))]
use tokio::task::spawn_local;
use uuid::Uuid;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_futures::spawn_local;
use yew::prelude::*;
use yew::suspense::{Suspension, SuspensionResult};

#[derive(Serialize, Deserialize)]
struct UuidResponse {
uuid: Uuid,
}

#[cfg(feature = "ssr")]
async fn fetch_uuid() -> Uuid {
// reqwest works for both non-wasm and wasm targets.
let resp = reqwest::get("https://httpbin.org/uuid").await.unwrap();
Expand All @@ -23,56 +16,9 @@ async fn fetch_uuid() -> Uuid {
uuid_resp.uuid
}

pub struct UuidState {
s: Suspension,
value: Rc<RefCell<Option<Uuid>>>,
}

impl UuidState {
fn new() -> Self {
let (s, handle) = Suspension::new();
let value: Rc<RefCell<Option<Uuid>>> = Rc::default();

{
let value = value.clone();
// we use tokio spawn local here.
spawn_local(async move {
let uuid = fetch_uuid().await;

{
let mut value = value.borrow_mut();
*value = Some(uuid);
}

handle.resume();
});
}

Self { s, value }
}
}

impl PartialEq for UuidState {
fn eq(&self, rhs: &Self) -> bool {
self.s == rhs.s
}
}

#[hook]
fn use_random_uuid() -> SuspensionResult<Uuid> {
let s = use_state(UuidState::new);

let result = match *s.value.borrow() {
Some(ref m) => Ok(*m),
None => Err(s.s.clone()),
};

result
}

#[function_component]
fn Content() -> HtmlResult {
let uuid = use_random_uuid()?;
let uuid = use_prepared_state!(async |_| -> Uuid { fetch_uuid().await }, ())?.unwrap();

Ok(html! {
<div>{"Random UUID: "}{uuid}</div>
Expand Down
6 changes: 5 additions & 1 deletion examples/ssr_router/Cargo.toml
Expand Up @@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
yew = { path = "../../packages/yew", features = ["ssr", "hydration"] }
yew = { path = "../../packages/yew" }
function_router = { path = "../function_router" }
log = "0.4"

Expand All @@ -24,3 +24,7 @@ num_cpus = "1.13"
tokio-util = { version = "0.7", features = ["rt"] }
once_cell = "1.5"
clap = { version = "3.1.7", features = ["derive"] }

[features]
ssr = ["yew/ssr"]
hydration = ["yew/hydration"]
2 changes: 1 addition & 1 deletion examples/ssr_router/README.md
Expand Up @@ -12,7 +12,7 @@ of the function router example.

2. Run the server

`cargo run --bin ssr_router_server -- --dir examples/ssr_router/dist`
`cargo run --features=ssr --bin ssr_router_server -- --dir examples/ssr_router/dist`

3. Open Browser

Expand Down
2 changes: 1 addition & 1 deletion examples/ssr_router/index.html
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link data-trunk rel="rust" data-bin="ssr_router_hydrate" />
<link data-trunk rel="rust" data-bin="ssr_router_hydrate" data-cargo-features="hydration" />

<title>Yew • SSR Router</title>
<link
Expand Down
5 changes: 5 additions & 0 deletions packages/yew-macro/src/function_component.rs
Expand Up @@ -319,6 +319,11 @@ impl FunctionComponent {
fn destroy(&mut self, _ctx: &::yew::html::Context<Self>) {
::yew::functional::FunctionComponent::<Self>::destroy(&self.function_component)
}

#[inline]
fn prepare_state(&self) -> ::std::option::Option<::std::string::String> {
::yew::functional::FunctionComponent::<Self>::prepare_state(&self.function_component)
}
}
}
}
Expand Down
28 changes: 28 additions & 0 deletions packages/yew-macro/src/lib.rs
Expand Up @@ -53,6 +53,8 @@ mod hook;
mod html_tree;
mod props;
mod stringify;
mod use_prepared_state;
mod use_transitive_state;

use derive_props::DerivePropsInput;
use function_component::{function_component_impl, FunctionComponent, FunctionComponentName};
Expand All @@ -62,6 +64,8 @@ use proc_macro::TokenStream;
use quote::ToTokens;
use syn::buffer::Cursor;
use syn::parse_macro_input;
use use_prepared_state::PreparedState;
use use_transitive_state::TransitiveState;

trait Peek<'a, T> {
fn peek(cursor: Cursor<'a>) -> Option<(T, Cursor<'a>)>;
Expand Down Expand Up @@ -150,3 +154,27 @@ pub fn hook(attr: TokenStream, item: TokenStream) -> proc_macro::TokenStream {
.unwrap_or_else(|err| err.to_compile_error())
.into()
}

#[proc_macro]
pub fn use_prepared_state_with_closure(input: TokenStream) -> TokenStream {
let prepared_state = parse_macro_input!(input as PreparedState);
prepared_state.to_token_stream_with_closure().into()
}

#[proc_macro]
pub fn use_prepared_state_without_closure(input: TokenStream) -> TokenStream {
let prepared_state = parse_macro_input!(input as PreparedState);
prepared_state.to_token_stream_without_closure().into()
}

#[proc_macro]
pub fn use_transitive_state_with_closure(input: TokenStream) -> TokenStream {
let transitive_state = parse_macro_input!(input as TransitiveState);
transitive_state.to_token_stream_with_closure().into()
}

#[proc_macro]
pub fn use_transitive_state_without_closure(input: TokenStream) -> TokenStream {
let transitive_state = parse_macro_input!(input as TransitiveState);
transitive_state.to_token_stream_without_closure().into()
}
116 changes: 116 additions & 0 deletions packages/yew-macro/src/use_prepared_state.rs
@@ -0,0 +1,116 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{parse_quote, Expr, ExprClosure, ReturnType, Token, Type};

#[derive(Debug)]
pub struct PreparedState {
closure: ExprClosure,
return_type: Type,
deps: Expr,
}

impl Parse for PreparedState {
fn parse(input: ParseStream) -> syn::Result<Self> {
// Reads a closure.
let expr: Expr = input.parse()?;

let closure = match expr {
Expr::Closure(m) => m,
other => return Err(syn::Error::new_spanned(other, "expected closure")),
};

input.parse::<Token![,]>().map_err(|e| {
syn::Error::new(
e.span(),
"this hook takes 2 arguments but 1 argument was supplied",
)
})?;

let return_type = match &closure.output {
ReturnType::Default => {
return Err(syn::Error::new_spanned(
&closure,
"You must specify a return type for this closure. This is used when the \
closure is omitted from the client side rendering bundle.",
))
}
ReturnType::Type(_rarrow, ty) => *ty.to_owned(),
};

// Reads the deps.
let deps = input.parse()?;

if !input.is_empty() {
let maybe_trailing_comma = input.lookahead1();

if !maybe_trailing_comma.peek(Token![,]) {
return Err(maybe_trailing_comma.error());
}
}

Ok(Self {
closure,
return_type,
deps,
})
}
}

impl PreparedState {
// Async closure is not stable, so we rewrite it to clsoure + async block
pub fn rewrite_to_closure_with_async_block(&self) -> ExprClosure {
let async_token = match &self.closure.asyncness {
Some(m) => m,
None => return self.closure.clone(),
};

let move_token = &self.closure.capture;
let body = &self.closure.body;

let inner = parse_quote! {
#async_token #move_token {
#body
}
};

let mut closure = self.closure.clone();

closure.asyncness = None;
// We omit the output type as it's an opaque future type.
closure.output = ReturnType::Default;

closure.body = inner;

closure.attrs.push(parse_quote! { #[allow(unused_braces)] });

closure
}

pub fn to_token_stream_with_closure(&self) -> TokenStream {
let deps = &self.deps;
let rt = &self.return_type;
let closure = self.rewrite_to_closure_with_async_block();

match &self.closure.asyncness {
Some(_) => quote! {
::yew::functional::use_prepared_state_with_suspension::<#rt, _, _, _>(#closure, #deps)
},
None => quote! {
::yew::functional::use_prepared_state::<#rt, _, _>(#closure, #deps)
},
}
}

// Expose a hook for the client side.
//
// The closure is stripped from the client side.
pub fn to_token_stream_without_closure(&self) -> TokenStream {
let deps = &self.deps;
let rt = &self.return_type;

quote! {
::yew::functional::use_prepared_state::<#rt, _>(#deps)
}
}
}

1 comment on commit b29b453

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yew master branch benchmarks (Lower is better)

Benchmark suite Current: b29b453 Previous: 027ab6a Ratio
yew-struct-keyed 01_run1k 174.806 204.1505 0.86
yew-struct-keyed 02_replace1k 191.269 210.018 0.91
yew-struct-keyed 03_update10th1k_x16 426.5925 421.48 1.01
yew-struct-keyed 04_select1k 60.132 85.28450000000001 0.71
yew-struct-keyed 05_swap1k 86.7955 108.018 0.80
yew-struct-keyed 06_remove-one-1k 29.3165 35.575 0.82
yew-struct-keyed 07_create10k 3261.5505000000003 3420.9745000000003 0.95
yew-struct-keyed 08_create1k-after1k_x2 453.2165 482.12 0.94
yew-struct-keyed 09_clear1k_x8 227.5145 205.782 1.11
yew-struct-keyed 21_ready-memory 1.4553260803222656 1.4598236083984375 1.00
yew-struct-keyed 22_run-memory 1.695892333984375 1.6948318481445312 1.00
yew-struct-keyed 23_update5-memory 1.7000389099121094 1.7120361328125 0.99
yew-struct-keyed 24_run5-memory 1.7169876098632812 1.7175521850585938 1.00
yew-struct-keyed 25_run-clear-memory 1.3335952758789062 1.3321037292480469 1.00
yew-struct-keyed 31_startup-ci 1837.6659999999997 1732.672 1.06
yew-struct-keyed 32_startup-bt 38.636 31.40799999999999 1.23
yew-struct-keyed 33_startup-mainthreadcost 268.2079999999999 261.19199999999995 1.03
yew-struct-keyed 34_startup-totalbytes 331.59765625 331.59765625 1

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.