refactor to allow rendering Resource directly in view
This commit is contained in:
parent
914b07491e
commit
3c39674622
|
@ -18,7 +18,7 @@ hydration_context = { workspace = true }
|
|||
either_of = { workspace = true }
|
||||
leptos_dom = { workspace = true }
|
||||
leptos_macro = { workspace = true }
|
||||
leptos_server = { workspace = true }
|
||||
leptos_server = { workspace = true, features = ["tachys"] }
|
||||
leptos_config = { workspace = true }
|
||||
leptos-spin-macro = { version = "0.1", optional = true }
|
||||
oco_ref = { workspace = true }
|
||||
|
|
|
@ -150,7 +150,6 @@ extern crate self as leptos;
|
|||
pub mod prelude {
|
||||
// Traits
|
||||
// These should always be exported from the prelude
|
||||
pub use crate::suspense_component::FutureViewExt;
|
||||
pub use reactive_graph::prelude::*;
|
||||
pub use tachys::prelude::*;
|
||||
|
||||
|
|
|
@ -212,7 +212,8 @@ where
|
|||
// out-of-order streams immediately push fallback,
|
||||
// wrapped by suspense markers
|
||||
if OUT_OF_ORDER {
|
||||
buf.push_fallback(self.fallback, position);
|
||||
let mut fallback_position = *position;
|
||||
buf.push_fallback(self.fallback, &mut fallback_position);
|
||||
buf.push_async_out_of_order(
|
||||
false, /* TODO should_block */ fut, position,
|
||||
);
|
||||
|
@ -266,215 +267,3 @@ where
|
|||
.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FutureViewExt: Sized {
|
||||
fn wait(self) -> Suspend<Self>
|
||||
where
|
||||
Self: Future,
|
||||
{
|
||||
Suspend(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> FutureViewExt for F where F: Future + Sized {}
|
||||
|
||||
/* // TODO remove in favor of Suspend()?
|
||||
#[macro_export]
|
||||
macro_rules! suspend {
|
||||
($fut:expr) => {
|
||||
move || $crate::prelude::FutureViewExt::wait(async move { $fut })
|
||||
};
|
||||
}*/
|
||||
|
||||
pub struct Suspend<Fut>(pub Fut);
|
||||
|
||||
impl<Fut> Debug for Suspend<Fut> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Suspend").finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SuspendState<T, Rndr>
|
||||
where
|
||||
T: Render<Rndr>,
|
||||
Rndr: Renderer,
|
||||
{
|
||||
inner: Rc<RefCell<OptionState<T::State, Rndr>>>,
|
||||
}
|
||||
|
||||
impl<T, Rndr> Mountable<Rndr> for SuspendState<T, Rndr>
|
||||
where
|
||||
T: Render<Rndr>,
|
||||
Rndr: Renderer,
|
||||
{
|
||||
fn unmount(&mut self) {
|
||||
self.inner.borrow_mut().unmount();
|
||||
}
|
||||
|
||||
fn mount(&mut self, parent: &Rndr::Element, marker: Option<&Rndr::Node>) {
|
||||
self.inner.borrow_mut().mount(parent, marker);
|
||||
}
|
||||
|
||||
fn insert_before_this(
|
||||
&self,
|
||||
parent: &Rndr::Element,
|
||||
child: &mut dyn Mountable<Rndr>,
|
||||
) -> bool {
|
||||
self.inner.borrow_mut().insert_before_this(parent, child)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut, Rndr> Render<Rndr> for Suspend<Fut>
|
||||
where
|
||||
Fut: Future + 'static,
|
||||
Fut::Output: Render<Rndr>,
|
||||
Rndr: Renderer + 'static,
|
||||
{
|
||||
type State = SuspendState<Fut::Output, Rndr>;
|
||||
|
||||
// TODO cancelation if it fires multiple times
|
||||
fn build(self) -> Self::State {
|
||||
// poll the future once immediately
|
||||
// if it's already available, start in the ready state
|
||||
// otherwise, start with the fallback
|
||||
let mut fut = Box::pin(ScopedFuture::new(self.0));
|
||||
let initial = fut.as_mut().now_or_never();
|
||||
let initially_pending = initial.is_none();
|
||||
let inner = Rc::new(RefCell::new(initial.build()));
|
||||
|
||||
// get a unique ID if there's a SuspenseContext
|
||||
let id = use_context::<SuspenseContext>().map(|sc| sc.task_id());
|
||||
|
||||
// if the initial state was pending, spawn a future to wait for it
|
||||
// spawning immediately means that our now_or_never poll result isn't lost
|
||||
// if it wasn't pending at first, we don't need to poll the Future again
|
||||
if initially_pending {
|
||||
Executor::spawn_local({
|
||||
let state = Rc::clone(&inner);
|
||||
async move {
|
||||
let value = fut.as_mut().await;
|
||||
drop(id);
|
||||
Some(value).rebuild(&mut *state.borrow_mut());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SuspendState { inner }
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
// get a unique ID if there's a SuspenseContext
|
||||
let fut = ScopedFuture::new(self.0);
|
||||
let id = use_context::<SuspenseContext>().map(|sc| sc.task_id());
|
||||
|
||||
// spawn the future, and rebuild the state when it resolves
|
||||
Executor::spawn_local({
|
||||
let state = Rc::clone(&state.inner);
|
||||
async move {
|
||||
let value = fut.await;
|
||||
drop(id);
|
||||
Some(value).rebuild(&mut *state.borrow_mut());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut, Rndr> AddAnyAttr<Rndr> for Suspend<Fut>
|
||||
where
|
||||
Fut: Future + Send + 'static,
|
||||
Fut::Output: AddAnyAttr<Rndr>,
|
||||
Rndr: Renderer + 'static,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<Rndr>> = Suspend<
|
||||
Pin<
|
||||
Box<
|
||||
dyn Future<
|
||||
Output = <Fut::Output as AddAnyAttr<Rndr>>::Output<
|
||||
SomeNewAttr::CloneableOwned,
|
||||
>,
|
||||
> + Send,
|
||||
>,
|
||||
>,
|
||||
>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Rndr>>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Rndr>,
|
||||
{
|
||||
let attr = attr.into_cloneable_owned();
|
||||
Suspend(Box::pin(async move {
|
||||
let this = self.0.await;
|
||||
this.add_any_attr(attr)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut, Rndr> RenderHtml<Rndr> for Suspend<Fut>
|
||||
where
|
||||
Fut: Future + Send + 'static,
|
||||
Fut::Output: RenderHtml<Rndr>,
|
||||
Rndr: Renderer + 'static,
|
||||
{
|
||||
type AsyncOutput = Option<Fut::Output>;
|
||||
|
||||
const MIN_LENGTH: usize = Fut::Output::MIN_LENGTH;
|
||||
|
||||
fn to_html_with_buf(self, _buf: &mut String, _position: &mut Position) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
self,
|
||||
_buf: &mut StreamBuilder,
|
||||
_position: &mut Position,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
todo!();
|
||||
}
|
||||
|
||||
// TODO cancellation
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &Cursor<Rndr>,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
// poll the future once immediately
|
||||
// if it's already available, start in the ready state
|
||||
// otherwise, start with the fallback
|
||||
let mut fut = Box::pin(ScopedFuture::new(self.0));
|
||||
let initial = fut.as_mut().now_or_never();
|
||||
let initially_pending = initial.is_none();
|
||||
let inner = Rc::new(RefCell::new(
|
||||
initial.hydrate::<FROM_SERVER>(cursor, position),
|
||||
));
|
||||
|
||||
// get a unique ID if there's a SuspenseContext
|
||||
let id = use_context::<SuspenseContext>().map(|sc| sc.task_id());
|
||||
|
||||
// if the initial state was pending, spawn a future to wait for it
|
||||
// spawning immediately means that our now_or_never poll result isn't lost
|
||||
// if it wasn't pending at first, we don't need to poll the Future again
|
||||
if initially_pending {
|
||||
Executor::spawn_local({
|
||||
let state = Rc::clone(&inner);
|
||||
async move {
|
||||
let value = fut.as_mut().await;
|
||||
drop(id);
|
||||
Some(value).rebuild(&mut *state.borrow_mut());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SuspendState { inner }
|
||||
}
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
Some(self.0.await)
|
||||
}
|
||||
|
||||
fn dry_resolve(&mut self) {}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ tracing = { version = "0.1", optional = true }
|
|||
#inventory = "0.3"
|
||||
futures = "0.3"
|
||||
|
||||
tachys = { workspace = true, optional = true, features = ["reactive_graph"] }
|
||||
|
||||
# serialization formats
|
||||
serde = { version = "1"}
|
||||
serde-wasm-bindgen = { version = "0.6", optional = true }
|
||||
|
@ -45,6 +47,7 @@ default-tls = ["server_fn/default-tls"]
|
|||
rustls = ["server_fn/rustls"]
|
||||
rkyv = ["dep:rkyv", "dep:base64"]
|
||||
serde-wasm-bindgen = ["dep:serde-wasm-bindgen", "dep:js-sys", "dep:wasm-bindgen"]
|
||||
tachys = ["dep:tachys"]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
|
|
@ -128,3 +128,112 @@ pub use resource::*;
|
|||
//pub use action::*;
|
||||
//pub use multi_action::*;
|
||||
//extern crate tracing;
|
||||
|
||||
#[cfg(feature = "tachys")]
|
||||
mod view_implementations {
|
||||
use crate::Resource;
|
||||
use reactive_graph::traits::Read;
|
||||
use std::{future::Future, pin::Pin};
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
reactive_graph::{RenderEffectState, Suspend, SuspendState},
|
||||
renderer::Renderer,
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml,
|
||||
},
|
||||
};
|
||||
|
||||
impl<T, R, Ser> Render<R> for Resource<T, Ser>
|
||||
where
|
||||
T: Render<R> + Send + Sync + Clone,
|
||||
Ser: Send + 'static,
|
||||
R: Renderer,
|
||||
{
|
||||
type State = RenderEffectState<SuspendState<T, R>>;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
(move || Suspend(async move { self.await })).build()
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
(move || Suspend(async move { self.await })).rebuild(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R, Ser> AddAnyAttr<R> for Resource<T, Ser>
|
||||
where
|
||||
T: RenderHtml<R> + Send + Sync + Clone,
|
||||
Ser: Send + 'static,
|
||||
R: Renderer,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<R>> = Box<
|
||||
dyn FnMut() -> Suspend<
|
||||
Pin<
|
||||
Box<
|
||||
dyn Future<
|
||||
Output = <T as AddAnyAttr<R>>::Output<
|
||||
<SomeNewAttr::CloneableOwned as Attribute<R>>::CloneableOwned,
|
||||
>,
|
||||
> + Send,
|
||||
>,
|
||||
>,
|
||||
> + Send,
|
||||
>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<R>>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<R>,
|
||||
{
|
||||
(move || Suspend(async move { self.await })).add_any_attr(attr)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R, Ser> RenderHtml<R> for Resource<T, Ser>
|
||||
where
|
||||
T: RenderHtml<R> + Send + Sync + Clone,
|
||||
Ser: Send + 'static,
|
||||
R: Renderer,
|
||||
{
|
||||
type AsyncOutput = Option<T>;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
fn dry_resolve(&mut self) {
|
||||
self.read();
|
||||
}
|
||||
|
||||
fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send {
|
||||
(move || Suspend(async move { self.await })).resolve()
|
||||
}
|
||||
|
||||
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
||||
(move || Suspend(async move { self.await }))
|
||||
.to_html_with_buf(buf, position);
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
self,
|
||||
buf: &mut StreamBuilder,
|
||||
position: &mut Position,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
(move || Suspend(async move { self.await }))
|
||||
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position);
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &Cursor<R>,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
(move || Suspend(async move { self.await }))
|
||||
.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -201,7 +201,7 @@ impl Owner {
|
|||
self.inner.cleanup();
|
||||
}
|
||||
|
||||
/// Registers a function to be run the next time this owner is cleaned up.
|
||||
/// Registers a function to be run the next time the current owner is cleaned up.
|
||||
///
|
||||
/// Because the ownership model is associated with reactive nodes, each "decision point" in an
|
||||
/// application tends to have a separate `Owner`: as a result, these cleanup functions often
|
||||
|
@ -263,6 +263,17 @@ impl Owner {
|
|||
}
|
||||
}
|
||||
|
||||
/// Registers a function to be run the next time the current owner is cleaned up.
|
||||
///
|
||||
/// Because the ownership model is associated with reactive nodes, each "decision point" in an
|
||||
/// application tends to have a separate `Owner`: as a result, these cleanup functions often
|
||||
/// fill the same need as an "on unmount" function in other UI approaches, etc.
|
||||
///
|
||||
/// This is an alias for [`Owner::on_cleanup`].
|
||||
pub fn on_cleanup(fun: impl FnOnce() + Send + Sync + 'static) {
|
||||
Owner::on_cleanup(fun)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct OwnerInner {
|
||||
pub parent: Option<Weak<RwLock<OwnerInner>>>,
|
||||
|
|
|
@ -1,287 +0,0 @@
|
|||
use crate::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
renderer::Renderer,
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, either::EitherState, Mountable, Position,
|
||||
PositionState, Render, RenderHtml,
|
||||
},
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
use either_of::Either;
|
||||
use futures::FutureExt;
|
||||
use parking_lot::RwLock;
|
||||
use std::{fmt::Debug, future::Future, sync::Arc};
|
||||
|
||||
pub trait FutureViewExt: Sized {
|
||||
fn suspend(self) -> Suspend<false, (), Self>
|
||||
where
|
||||
Self: Future,
|
||||
{
|
||||
Suspend {
|
||||
fallback: (),
|
||||
fut: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> FutureViewExt for F where F: Future + Sized {}
|
||||
|
||||
pub struct Suspend<const TRANSITION: bool, Fal, Fut> {
|
||||
pub fallback: Fal,
|
||||
pub fut: Fut,
|
||||
}
|
||||
|
||||
impl<const TRANSITION: bool, Fal, Fut> Suspend<TRANSITION, Fal, Fut> {
|
||||
pub fn with_fallback<Fal2>(
|
||||
self,
|
||||
fallback: Fal2,
|
||||
) -> Suspend<TRANSITION, Fal2, Fut> {
|
||||
let fut = self.fut;
|
||||
Suspend { fallback, fut }
|
||||
}
|
||||
|
||||
pub fn transition(self) -> Suspend<true, Fal, Fut> {
|
||||
let Suspend { fallback, fut } = self;
|
||||
Suspend { fallback, fut }
|
||||
}
|
||||
}
|
||||
|
||||
impl<const TRANSITION: bool, Fal, Fut> Debug for Suspend<TRANSITION, Fal, Fut> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SuspendedFuture")
|
||||
.field("transition", &TRANSITION)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO make this cancelable
|
||||
impl<const TRANSITION: bool, Fal, Fut, Rndr> Render<Rndr>
|
||||
for Suspend<TRANSITION, Fal, Fut>
|
||||
where
|
||||
Fal: Render<Rndr> + 'static,
|
||||
Fut: Future + 'static,
|
||||
Fut::Output: Render<Rndr>,
|
||||
Rndr: Renderer + 'static,
|
||||
{
|
||||
type State = Arc<
|
||||
RwLock<
|
||||
EitherState<Fal::State, <Fut::Output as Render<Rndr>>::State, Rndr>,
|
||||
>,
|
||||
>;
|
||||
// TODO fallible state/error
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
// poll the future once immediately
|
||||
// if it's already available, start in the ready state
|
||||
// otherwise, start with the fallback
|
||||
let mut fut = Box::pin(self.fut);
|
||||
let initial = match fut.as_mut().now_or_never() {
|
||||
Some(resolved) => Either::Right(resolved),
|
||||
None => Either::Left(self.fallback),
|
||||
};
|
||||
|
||||
// store whether this was pending at first
|
||||
// by the time we need to know, we will have consumed `initial`
|
||||
let initially_pending = matches!(initial, Either::Left(_));
|
||||
|
||||
// now we can build the initial state
|
||||
let state = Arc::new(RwLock::new(initial.build()));
|
||||
|
||||
// if the initial state was pending, spawn a future to wait for it
|
||||
// spawning immediately means that our now_or_never poll result isn't lost
|
||||
// if it wasn't pending at first, we don't need to poll the Future again
|
||||
if initially_pending {
|
||||
Executor::spawn_local({
|
||||
let state = Arc::clone(&state);
|
||||
async move {
|
||||
let value = fut.as_mut().await;
|
||||
Either::<Fal, Fut::Output>::Right(value)
|
||||
.rebuild(&mut *state.write());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
if !TRANSITION {
|
||||
// fall back to fallback state
|
||||
Either::<Fal, Fut::Output>::Left(self.fallback)
|
||||
.rebuild(&mut *state.write());
|
||||
}
|
||||
|
||||
// spawn the future, and rebuild the state when it resolves
|
||||
Executor::spawn_local({
|
||||
let state = Arc::clone(state);
|
||||
async move {
|
||||
let value = self.fut.await;
|
||||
Either::<Fal, Fut::Output>::Right(value)
|
||||
.rebuild(&mut *state.write());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<const TRANSITION: bool, Fal, Fut, Rndr> AddAnyAttr<Rndr>
|
||||
for Suspend<TRANSITION, Fal, Fut>
|
||||
where
|
||||
Fal: RenderHtml<Rndr> + 'static,
|
||||
Fut: Future + Send + 'static,
|
||||
Fut::Output: RenderHtml<Rndr> + Send,
|
||||
Rndr: Renderer + 'static,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<Rndr>> = Self;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Rndr>>(
|
||||
self,
|
||||
_attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Rndr>,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const TRANSITION: bool, Fal, Fut, Rndr> RenderHtml<Rndr>
|
||||
for Suspend<TRANSITION, Fal, Fut>
|
||||
where
|
||||
Fal: RenderHtml<Rndr> + 'static,
|
||||
Fut: Future + Send + 'static,
|
||||
Fut::Output: RenderHtml<Rndr> + Send,
|
||||
Rndr: Renderer + 'static,
|
||||
{
|
||||
type AsyncOutput = Fut::Output;
|
||||
|
||||
const MIN_LENGTH: usize = Fal::MIN_LENGTH;
|
||||
|
||||
fn dry_resolve(&mut self) {}
|
||||
|
||||
fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send {
|
||||
self.fut
|
||||
}
|
||||
|
||||
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
||||
Either::<Fal, Fut::Output>::Left(self.fallback)
|
||||
.to_html_with_buf(buf, position);
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
self,
|
||||
buf: &mut StreamBuilder,
|
||||
position: &mut Position,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
buf.next_id();
|
||||
|
||||
let mut fut = Box::pin(self.fut);
|
||||
match fut.as_mut().now_or_never() {
|
||||
Some(resolved) => {
|
||||
Either::<Fal, Fut::Output>::Right(resolved)
|
||||
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position);
|
||||
}
|
||||
None => {
|
||||
let id = buf.clone_id();
|
||||
|
||||
// out-of-order streams immediately push fallback,
|
||||
// wrapped by suspense markers
|
||||
if OUT_OF_ORDER {
|
||||
buf.push_fallback(self.fallback, position);
|
||||
buf.push_async_out_of_order(
|
||||
false, /* TODO should_block */ fut, position,
|
||||
);
|
||||
} else {
|
||||
buf.push_async(
|
||||
false, // TODO should_block
|
||||
{
|
||||
let mut position = *position;
|
||||
async move {
|
||||
let value = fut.await;
|
||||
let mut builder = StreamBuilder::new(id);
|
||||
Either::<Fal, Fut::Output>::Right(value)
|
||||
.to_html_async_with_buf::<OUT_OF_ORDER>(
|
||||
&mut builder,
|
||||
&mut position,
|
||||
);
|
||||
builder.finish().take_chunks()
|
||||
}
|
||||
},
|
||||
);
|
||||
*position = Position::NextChild;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &Cursor<Rndr>,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
// poll the future once immediately
|
||||
// if it's already available, start in the ready state
|
||||
// otherwise, start with the fallback
|
||||
let mut fut = Box::pin(self.fut);
|
||||
let initial = match fut.as_mut().now_or_never() {
|
||||
Some(resolved) => Either::Right(resolved),
|
||||
None => Either::Left(self.fallback),
|
||||
};
|
||||
|
||||
// store whether this was pending at first
|
||||
// by the time we need to know, we will have consumed `initial`
|
||||
let initially_pending = matches!(initial, Either::Left(_));
|
||||
|
||||
// now we can build the initial state
|
||||
let state = Arc::new(RwLock::new(
|
||||
initial.hydrate::<FROM_SERVER>(cursor, position),
|
||||
));
|
||||
|
||||
// if the initial state was pending, spawn a future to wait for it
|
||||
// spawning immediately means that our now_or_never poll result isn't lost
|
||||
// if it wasn't pending at first, we don't need to poll the Future again
|
||||
if initially_pending {
|
||||
Executor::spawn_local({
|
||||
let state = Arc::clone(&state);
|
||||
async move {
|
||||
let value = fut.as_mut().await;
|
||||
Either::<Fal, Fut::Output>::Right(value)
|
||||
.rebuild(&mut *state.write());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rndr, Fal, Output> Mountable<Rndr>
|
||||
for Arc<RwLock<EitherState<Fal, Output, Rndr>>>
|
||||
where
|
||||
Fal: Mountable<Rndr>,
|
||||
Output: Mountable<Rndr>,
|
||||
Rndr: Renderer,
|
||||
{
|
||||
fn unmount(&mut self) {
|
||||
self.write().unmount();
|
||||
}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &<Rndr as Renderer>::Element,
|
||||
marker: Option<&<Rndr as Renderer>::Node>,
|
||||
) {
|
||||
self.write().mount(parent, marker);
|
||||
}
|
||||
|
||||
fn insert_before_this(
|
||||
&self,
|
||||
parent: &<Rndr as Renderer>::Element,
|
||||
child: &mut dyn Mountable<Rndr>,
|
||||
) -> bool {
|
||||
self.write().insert_before_this(parent, child)
|
||||
}
|
||||
}
|
|
@ -2,8 +2,9 @@
|
|||
#![cfg_attr(feature = "nightly", feature(adt_const_params))]
|
||||
|
||||
pub mod prelude {
|
||||
#[cfg(feature = "reactive_graph")]
|
||||
pub use crate::reactive_graph::FutureViewExt;
|
||||
pub use crate::{
|
||||
async_views::FutureViewExt,
|
||||
html::{
|
||||
attribute::{
|
||||
aria::AriaAttributes,
|
||||
|
@ -27,7 +28,6 @@ pub mod prelude {
|
|||
use wasm_bindgen::JsValue;
|
||||
use web_sys::Node;
|
||||
|
||||
pub mod async_views;
|
||||
pub mod dom;
|
||||
pub mod html;
|
||||
pub mod hydration;
|
||||
|
|
|
@ -18,7 +18,9 @@ pub mod node_ref;
|
|||
mod owned;
|
||||
mod property;
|
||||
mod style;
|
||||
mod suspense;
|
||||
pub use owned::*;
|
||||
pub use suspense::*;
|
||||
|
||||
impl<F, V> ToTemplate for F
|
||||
where
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
use crate::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
renderer::Renderer,
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, iterators::OptionState, Mountable, Position,
|
||||
PositionState, Render, RenderHtml,
|
||||
},
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
use futures::FutureExt;
|
||||
use reactive_graph::{
|
||||
computed::{suspense::SuspenseContext, ScopedFuture},
|
||||
owner::use_context,
|
||||
};
|
||||
use std::{cell::RefCell, fmt::Debug, future::Future, pin::Pin, rc::Rc};
|
||||
|
||||
pub trait FutureViewExt: Sized {
|
||||
fn wait(self) -> Suspend<Self>
|
||||
where
|
||||
Self: Future,
|
||||
{
|
||||
Suspend(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> FutureViewExt for F where F: Future + Sized {}
|
||||
|
||||
/* // TODO remove in favor of Suspend()?
|
||||
#[macro_export]
|
||||
macro_rules! suspend {
|
||||
($fut:expr) => {
|
||||
move || $crate::prelude::FutureViewExt::wait(async move { $fut })
|
||||
};
|
||||
}*/
|
||||
|
||||
pub struct Suspend<Fut>(pub Fut);
|
||||
|
||||
impl<Fut> Debug for Suspend<Fut> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Suspend").finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SuspendState<T, Rndr>
|
||||
where
|
||||
T: Render<Rndr>,
|
||||
Rndr: Renderer,
|
||||
{
|
||||
inner: Rc<RefCell<OptionState<T::State, Rndr>>>,
|
||||
}
|
||||
|
||||
impl<T, Rndr> Mountable<Rndr> for SuspendState<T, Rndr>
|
||||
where
|
||||
T: Render<Rndr>,
|
||||
Rndr: Renderer,
|
||||
{
|
||||
fn unmount(&mut self) {
|
||||
self.inner.borrow_mut().unmount();
|
||||
}
|
||||
|
||||
fn mount(&mut self, parent: &Rndr::Element, marker: Option<&Rndr::Node>) {
|
||||
self.inner.borrow_mut().mount(parent, marker);
|
||||
}
|
||||
|
||||
fn insert_before_this(
|
||||
&self,
|
||||
parent: &Rndr::Element,
|
||||
child: &mut dyn Mountable<Rndr>,
|
||||
) -> bool {
|
||||
self.inner.borrow_mut().insert_before_this(parent, child)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut, Rndr> Render<Rndr> for Suspend<Fut>
|
||||
where
|
||||
Fut: Future + 'static,
|
||||
Fut::Output: Render<Rndr>,
|
||||
Rndr: Renderer + 'static,
|
||||
{
|
||||
type State = SuspendState<Fut::Output, Rndr>;
|
||||
|
||||
// TODO cancelation if it fires multiple times
|
||||
fn build(self) -> Self::State {
|
||||
// poll the future once immediately
|
||||
// if it's already available, start in the ready state
|
||||
// otherwise, start with the fallback
|
||||
let mut fut = Box::pin(ScopedFuture::new(self.0));
|
||||
let initial = fut.as_mut().now_or_never();
|
||||
let initially_pending = initial.is_none();
|
||||
let inner = Rc::new(RefCell::new(initial.build()));
|
||||
|
||||
// get a unique ID if there's a SuspenseContext
|
||||
let id = use_context::<SuspenseContext>().map(|sc| sc.task_id());
|
||||
|
||||
// if the initial state was pending, spawn a future to wait for it
|
||||
// spawning immediately means that our now_or_never poll result isn't lost
|
||||
// if it wasn't pending at first, we don't need to poll the Future again
|
||||
if initially_pending {
|
||||
Executor::spawn_local({
|
||||
let state = Rc::clone(&inner);
|
||||
async move {
|
||||
let value = fut.as_mut().await;
|
||||
drop(id);
|
||||
Some(value).rebuild(&mut *state.borrow_mut());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SuspendState { inner }
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
// get a unique ID if there's a SuspenseContext
|
||||
let fut = ScopedFuture::new(self.0);
|
||||
let id = use_context::<SuspenseContext>().map(|sc| sc.task_id());
|
||||
|
||||
// spawn the future, and rebuild the state when it resolves
|
||||
Executor::spawn_local({
|
||||
let state = Rc::clone(&state.inner);
|
||||
async move {
|
||||
let value = fut.await;
|
||||
drop(id);
|
||||
Some(value).rebuild(&mut *state.borrow_mut());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut, Rndr> AddAnyAttr<Rndr> for Suspend<Fut>
|
||||
where
|
||||
Fut: Future + Send + 'static,
|
||||
Fut::Output: AddAnyAttr<Rndr>,
|
||||
Rndr: Renderer + 'static,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<Rndr>> = Suspend<
|
||||
Pin<
|
||||
Box<
|
||||
dyn Future<
|
||||
Output = <Fut::Output as AddAnyAttr<Rndr>>::Output<
|
||||
SomeNewAttr::CloneableOwned,
|
||||
>,
|
||||
> + Send,
|
||||
>,
|
||||
>,
|
||||
>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Rndr>>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Rndr>,
|
||||
{
|
||||
let attr = attr.into_cloneable_owned();
|
||||
Suspend(Box::pin(async move {
|
||||
let this = self.0.await;
|
||||
this.add_any_attr(attr)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut, Rndr> RenderHtml<Rndr> for Suspend<Fut>
|
||||
where
|
||||
Fut: Future + Send + 'static,
|
||||
Fut::Output: RenderHtml<Rndr>,
|
||||
Rndr: Renderer + 'static,
|
||||
{
|
||||
type AsyncOutput = Option<Fut::Output>;
|
||||
|
||||
const MIN_LENGTH: usize = Fut::Output::MIN_LENGTH;
|
||||
|
||||
fn to_html_with_buf(self, _buf: &mut String, _position: &mut Position) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
self,
|
||||
_buf: &mut StreamBuilder,
|
||||
_position: &mut Position,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
todo!();
|
||||
}
|
||||
|
||||
// TODO cancellation
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &Cursor<Rndr>,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
// poll the future once immediately
|
||||
// if it's already available, start in the ready state
|
||||
// otherwise, start with the fallback
|
||||
let mut fut = Box::pin(ScopedFuture::new(self.0));
|
||||
let initial = fut.as_mut().now_or_never();
|
||||
let initially_pending = initial.is_none();
|
||||
let inner = Rc::new(RefCell::new(
|
||||
initial.hydrate::<FROM_SERVER>(cursor, position),
|
||||
));
|
||||
|
||||
// get a unique ID if there's a SuspenseContext
|
||||
let id = use_context::<SuspenseContext>().map(|sc| sc.task_id());
|
||||
|
||||
// if the initial state was pending, spawn a future to wait for it
|
||||
// spawning immediately means that our now_or_never poll result isn't lost
|
||||
// if it wasn't pending at first, we don't need to poll the Future again
|
||||
if initially_pending {
|
||||
Executor::spawn_local({
|
||||
let state = Rc::clone(&inner);
|
||||
async move {
|
||||
let value = fut.as_mut().await;
|
||||
drop(id);
|
||||
Some(value).rebuild(&mut *state.borrow_mut());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SuspendState { inner }
|
||||
}
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
Some(self.0.await)
|
||||
}
|
||||
|
||||
fn dry_resolve(&mut self) {}
|
||||
}
|
|
@ -105,6 +105,7 @@ macro_rules! render_primitive {
|
|||
}
|
||||
|
||||
let node = cursor.current();
|
||||
R::log_node(&node);
|
||||
let node = R::Text::cast_from(node)
|
||||
.expect("couldn't cast text node from node");
|
||||
|
||||
|
|
Loading…
Reference in New Issue