Skip to content

SSR + Hydration #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
use flake
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ book_pages:
demo_todomvc:
@echo "====> building demo todomvc"
rm -rf gh-pages/demos/todomvc
wasm-pack build --release --target web --out-dir ../../gh-pages/demos/todomvc/pkg examples/web-todomvc
wasm-pack build --release --target web --out-dir ../../gh-pages/demos/todomvc/pkg examples/web-todomvc --features=csr
rm gh-pages/demos/todomvc/pkg/.gitignore
cp examples/web-todomvc/index.html gh-pages/demos/todomvc/

.PHONY: demo_todomvc_ssr
demo_todomvc_ssr:
@echo "====> running demo todomvc ssr"
# FIXME: how to unset target set via cargo config and just use current system's?
cargo run -p web-todomvc --features=ssr --bin ssr --target=x86_64-unknown-linux-gnu

.PHONY: demo_simple
demo_simple:
@echo "====> building demo simple"
Expand All @@ -40,4 +46,4 @@ demo_login_flow:
cp examples/login-flow/index.html gh-pages/demos/login-flow/

.PHONY: demos
demos: demo_todomvc demo_simple demo_x_bow_playground demo_login_flow
demos: demo_todomvc demo_simple demo_x_bow_playground demo_login_flow
12 changes: 10 additions & 2 deletions async_ui_web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ scopeguard = "1.1.0"
js-sys = "0.3.64"
wasm-bindgen = "0.2.87"

pin-project = "1.0"

[dependencies.web-sys]
version = "0.3.64"
features = [
Expand All @@ -35,5 +37,11 @@ features = [
'IntersectionObserver',
'IntersectionObserverInit',
'IntersectionObserverEntry',
'console'
]
'console',
]

[features]
default = []
csr = ['async_ui_web_core/csr', 'async_ui_web_html/csr']
ssr = ['async_ui_web_core/ssr', 'async_ui_web_html/ssr']

10 changes: 10 additions & 0 deletions async_ui_web/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::{cell::OnceCell, future::pending};

use async_executor::LocalExecutor;
use async_ui_web_core::executor::set_executor_future;
use std::future::Future;

thread_local! {
static EXECUTOR: OnceCell<&'static LocalExecutor<'static>> = OnceCell::new();
Expand All @@ -32,3 +33,12 @@ pub fn get_executor() -> &'static LocalExecutor<'static> {
})
})
}

/// If something needs to be run to completion before sending user html - it needs to be wrapped in `run_loading` call.
///
/// let data = run_loading(load_data()).await;
///
/// DataDisplay::new(data).render().await
pub async fn run_loading<V>(f: impl Future<Output = V>) -> V {
f.await
}
91 changes: 91 additions & 0 deletions async_ui_web/src/executor_ssr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use std::cell::RefCell;
use std::future::Future;
use std::pin::{pin, Pin};
use std::task::{Context, Poll};

pub fn get_executor() -> &'static () {
todo!("tokio/other executor abstraction is wanted here.")
}

/// If something needs to be run to completion before sending user html - it needs to be wrapped in `run_loading` call.
///
/// let data = run_loading(load_data()).await;
///
/// DataDisplay::new(data).render().await
///
/// Future will be pooled until all run_loading futures are resolved
// TODO: We also want user to be able to re-use loaded data during hydration, make an additional wrapper function, which will also feed frontend with serialized data version?
pub async fn run_loading<V>(f: impl Future<Output = V>) -> V {
CTX.with_borrow_mut(|ctx| {
let ctx = ctx
.as_mut()
.expect("ctx should be set by UntilLoadedFuture (before)");
ctx.loading += 1;
});
let res = f.await;
CTX.with_borrow_mut(|ctx| {
let ctx = ctx
.as_mut()
.expect("ctx should be set by UntilLoadedFuture (after)");
ctx.loading -= 1;
});
res
}

struct SsrContext {
loading: usize,
}

thread_local! {
static CTX: RefCell<Option<SsrContext>> = const { RefCell::new(None) };
}

#[pin_project::pin_project]
struct UntilLoadedFuture<F> {
#[pin]
inner: F,
ctx: Option<SsrContext>,
ran_once: bool,
}
impl<F: Future<Output = ()>> Future for UntilLoadedFuture<F> {
type Output = ();

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let project = self.project();
let ctx = project
.ctx
.take()
.expect("ctx is returned in between polls");
CTX.with_borrow_mut(move |v| {
assert!(v.is_none(), "CTX is empty between polls");
*v = Some(ctx);
});

let poll = project.inner.poll(cx);

let ctx = CTX.with_borrow_mut(move |v| v.take().expect("nothing should retake our ctx"));

if ctx.loading == 0 {
// We don't care about everything not needed for first contentful load.
return Poll::Ready(());
}

// TODO: This is not panic-safe, but I'm not sure how panics can be handled here yet.
*project.ctx = Some(ctx);
*project.ran_once = true;

match poll {
Poll::Ready(_) => Poll::Ready(()),
Poll::Pending => Poll::Pending,
}
}
}

pub(crate) async fn poll_until_loaded(inner: impl Future<Output = ()>) {
let fut = UntilLoadedFuture {
inner,
ctx: Some(SsrContext { loading: 0 }),
ran_once: false,
};
fut.await
}
10 changes: 10 additions & 0 deletions async_ui_web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,28 @@ See the [guide book](https://wishawa.github.io/async_ui/book/) to get started!
*/

pub mod components;
#[cfg(feature = "csr")]
pub mod executor;
#[cfg(feature = "ssr")]
#[path = "./executor_ssr.rs"]
pub mod executor;
pub mod lists;
#[cfg(feature = "csr")]
mod mount;
mod no_child;
mod shortcuts;
#[cfg(feature = "ssr")]
mod ssr;

pub use async_ui_internal_utils::reactive_cell::ReactiveCell;
pub use async_ui_web_core::combinators::{join, race, race_ok, try_join};
pub use async_ui_web_html::nodes as html;
pub use async_ui_web_macros::css;
pub use async_ui_web_macros::select;
#[cfg(feature = "csr")]
pub use mount::{mount, mount_at};
#[cfg(feature = "ssr")]
pub use ssr::render_to_string;
pub use no_child::NoChild;

#[doc(hidden)]
Expand Down
34 changes: 18 additions & 16 deletions async_ui_web/src/lists/dynamic_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ use std::{
};

use async_executor::{LocalExecutor, Task};
use async_ui_web_core::{ContainerNodeFuture, DetachmentBlocker, SiblingNodeFuture};
use async_ui_web_core::{
dom::{DocumentFragment, Node, marker_node},
ContainerNodeFuture, DetachmentBlocker, SiblingNodeFuture,
};
use wasm_bindgen::UnwrapThrowExt;
use web_sys::{Comment, DocumentFragment};

#[derive(Clone, Debug)]
enum ContainingNode {
Real(web_sys::Node),
Real(Node),
Fake(DocumentFragment),
}

Expand Down Expand Up @@ -53,8 +55,8 @@ join((
pub struct DynamicList<'c, K: Eq + Hash, F: Future + 'c> {
inner: RefCell<DynamicListInner<K, F>>,
executor: LocalExecutor<'c>,
list_end_marker: web_sys::Node,
list_start_marker: web_sys::Node,
list_end_marker: Node,
list_start_marker: Node,
detachment_blocker: DetachmentBlocker,
}

Expand All @@ -65,12 +67,12 @@ struct DynamicListInner<K: Eq + Hash, F: Future> {

struct Stored<F: Future> {
task: Task<F::Output>,
start_marker: web_sys::Node,
end_marker: web_sys::Node,
start_marker: Node,
end_marker: Node,
}

impl ContainingNode {
fn get(&self) -> &web_sys::Node {
fn get(&self) -> &Node {
match self {
ContainingNode::Real(real) => real,
ContainingNode::Fake(fake) => fake,
Expand All @@ -87,8 +89,8 @@ impl<'c, K: Eq + Hash, F: Future + 'c> DynamicList<'c, K, F> {
/// Create a new list, without anything in it.
pub fn new() -> Self {
let frag = DocumentFragment::new().unwrap_throw();
let list_end_marker = Comment::new().unwrap_throw().into();
let list_start_marker = Comment::new().unwrap_throw().into();
let list_end_marker = marker_node("list end").into();
let list_start_marker = marker_node("list start").into();
frag.append_child(&list_start_marker).unwrap_throw();
frag.append_child(&list_end_marker).unwrap_throw();
Self {
Expand Down Expand Up @@ -122,8 +124,8 @@ impl<'c, K: Eq + Hash, F: Future + 'c> DynamicList<'c, K, F> {
pub fn insert(&self, key: K, future: F, before: Option<&K>) -> bool {
let mut inner = self.inner.borrow_mut();
let container = inner.containing_node.get();
let start_marker: web_sys::Node = Comment::new().unwrap_throw().into();
let end_marker: web_sys::Node = Comment::new().unwrap_throw().into();
let start_marker: Node = marker_node("item start").into();
let end_marker: Node = marker_node("item end").into();
let after = before
.map(|k| &inner.items.get(k).unwrap().start_marker)
.unwrap_or(&self.list_end_marker);
Expand Down Expand Up @@ -349,10 +351,10 @@ impl<'c, K: Eq + Hash, F: Future> Drop for DynamicList<'c, K, F> {
/// Move `start_marker`, `end_marker`, and eveything between them
/// into `container` at location before `after`.
fn move_nodes_before(
container: &web_sys::Node,
start_marker: &web_sys::Node,
end_marker: &web_sys::Node,
after: Option<&web_sys::Node>,
container: &Node,
start_marker: &Node,
end_marker: &Node,
after: Option<&Node>,
) {
let mut node = start_marker.clone();
loop {
Expand Down
3 changes: 3 additions & 0 deletions async_ui_web/src/lists/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,12 @@
mod diffed_list;
mod dynamic_list;
mod modeled_list;
// TODO: Somehow make its api SSR-friendly?
#[cfg(feature = "csr")]
mod virtualized_list;

pub use diffed_list::DiffedList;
pub use dynamic_list::DynamicList;
pub use modeled_list::{ListModel, ModeledList};
#[cfg(feature = "csr")]
pub use virtualized_list::VirtualizedList;
8 changes: 6 additions & 2 deletions async_ui_web/src/lists/virtualized_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ use std::{
};

use async_ui_internal_utils::reactive_cell::ReactiveCell;
use async_ui_web_core::{combinators::join, ContainerNodeFuture};
use async_ui_web_core::{
combinators::join,
dom::{Element, HtmlElement},
ContainerNodeFuture,
};
use futures_lite::{Future, StreamExt};
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
use web_sys::{Element, HtmlElement, IntersectionObserver, IntersectionObserverInit};
use web_sys::{IntersectionObserver, IntersectionObserverInit};

use super::DynamicList;

Expand Down
28 changes: 21 additions & 7 deletions async_ui_web/src/shortcuts.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::future::Pending;

use async_ui_web_core::dom::Element;
use async_ui_web_core::ContainerNodeFuture;
use async_ui_web_html::nodes::Text;
use js_sys::Array;
use wasm_bindgen::{JsValue, UnwrapThrowExt};
use wasm_bindgen::UnwrapThrowExt;

pub trait ShortcutRenderStr {
/// Render the [str] as an HTML text node with that content.
Expand Down Expand Up @@ -45,28 +45,42 @@ pub trait ShortcutClassList {
}

/// Convert an iterator of str to a JS array of strings.
fn strs_to_js_array<'a>(values: impl Iterator<Item = &'a str>) -> Array {
values.into_iter().map(JsValue::from_str).collect()
#[cfg(feature = "csr")]
fn strs_to_js_array<'a>(values: impl Iterator<Item = &'a str>) -> js_sys::Array {
values
.into_iter()
.map(wasm_bindgen::JsValue::from_str)
.collect()
}

impl ShortcutClassList for web_sys::Element {
impl ShortcutClassList for Element {
fn add_class(&self, c: &str) {
self.class_list().add_1(c).unwrap_throw();
}
#[cfg(feature = "csr")]
fn add_classes<'a>(&self, c: impl IntoIterator<Item = &'a str>) {
self.class_list()
.add(&strs_to_js_array(c.into_iter()))
.unwrap_throw();
}
#[cfg(not(feature = "csr"))]
fn add_classes<'a>(&self, c: impl IntoIterator<Item = &'a str>) {
self.class_list().add(c).unwrap_throw();
}

fn del_class(&self, c: &str) {
self.class_list().remove_1(c).unwrap();
}
#[cfg(feature = "csr")]
fn del_classes<'a>(&self, c: impl IntoIterator<Item = &'a str>) {
self.class_list()
.remove(&strs_to_js_array(c.into_iter()))
.unwrap_throw();
}
#[cfg(not(feature = "csr"))]
fn del_classes<'a>(&self, c: impl IntoIterator<Item = &'a str>) {
self.class_list().remove(c).unwrap_throw();
}

fn set_class(&self, c: &str, included: bool) {
self.class_list()
Expand All @@ -75,7 +89,7 @@ impl ShortcutClassList for web_sys::Element {
}
}

pub trait ShortcutClassListBuilder: AsRef<web_sys::Element> {
pub trait ShortcutClassListBuilder: AsRef<Element> {
/// Add a classname to the element and return reference to the input.
///
/// This is for writing the UI "declaratively".
Expand Down Expand Up @@ -105,4 +119,4 @@ pub trait ShortcutClassListBuilder: AsRef<web_sys::Element> {
self
}
}
impl<T: AsRef<web_sys::Element>> ShortcutClassListBuilder for T {}
impl<T: AsRef<Element>> ShortcutClassListBuilder for T {}
Loading