diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index f290d5541..6ed3df5d3 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -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 } diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index 569251c35..bf1cbbe9b 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -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::*; diff --git a/leptos/src/suspense_component.rs b/leptos/src/suspense_component.rs index 1172ce27b..152d89aca 100644 --- a/leptos/src/suspense_component.rs +++ b/leptos/src/suspense_component.rs @@ -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::(cursor, position) } } - -pub trait FutureViewExt: Sized { - fn wait(self) -> Suspend - where - Self: Future, - { - Suspend(self) - } -} - -impl 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(pub Fut); - -impl Debug for Suspend { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Suspend").finish() - } -} - -pub struct SuspendState -where - T: Render, - Rndr: Renderer, -{ - inner: Rc>>, -} - -impl Mountable for SuspendState -where - T: Render, - 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, - ) -> bool { - self.inner.borrow_mut().insert_before_this(parent, child) - } -} - -impl Render for Suspend -where - Fut: Future + 'static, - Fut::Output: Render, - Rndr: Renderer + 'static, -{ - type State = SuspendState; - - // 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::().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::().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 AddAnyAttr for Suspend -where - Fut: Future + Send + 'static, - Fut::Output: AddAnyAttr, - Rndr: Renderer + 'static, -{ - type Output> = Suspend< - Pin< - Box< - dyn Future< - Output = >::Output< - SomeNewAttr::CloneableOwned, - >, - > + Send, - >, - >, - >; - - fn add_any_attr>( - self, - attr: NewAttr, - ) -> Self::Output - where - Self::Output: RenderHtml, - { - let attr = attr.into_cloneable_owned(); - Suspend(Box::pin(async move { - let this = self.0.await; - this.add_any_attr(attr) - })) - } -} - -impl RenderHtml for Suspend -where - Fut: Future + Send + 'static, - Fut::Output: RenderHtml, - Rndr: Renderer + 'static, -{ - type AsyncOutput = Option; - - 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( - self, - _buf: &mut StreamBuilder, - _position: &mut Position, - ) where - Self: Sized, - { - todo!(); - } - - // TODO cancellation - fn hydrate( - self, - cursor: &Cursor, - 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::(cursor, position), - )); - - // get a unique ID if there's a SuspenseContext - let id = use_context::().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) {} -} diff --git a/leptos_server/Cargo.toml b/leptos_server/Cargo.toml index 7cc86dca1..c5265488f 100644 --- a/leptos_server/Cargo.toml +++ b/leptos_server/Cargo.toml @@ -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] diff --git a/leptos_server/src/lib.rs b/leptos_server/src/lib.rs index ed544d754..eb6c4a64a 100644 --- a/leptos_server/src/lib.rs +++ b/leptos_server/src/lib.rs @@ -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 Render for Resource + where + T: Render + Send + Sync + Clone, + Ser: Send + 'static, + R: Renderer, + { + type State = RenderEffectState>; + + 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 AddAnyAttr for Resource + where + T: RenderHtml + Send + Sync + Clone, + Ser: Send + 'static, + R: Renderer, + { + type Output> = Box< + dyn FnMut() -> Suspend< + Pin< + Box< + dyn Future< + Output = >::Output< + >::CloneableOwned, + >, + > + Send, + >, + >, + > + Send, + >; + + fn add_any_attr>( + self, + attr: NewAttr, + ) -> Self::Output + where + Self::Output: RenderHtml, + { + (move || Suspend(async move { self.await })).add_any_attr(attr) + } + } + + impl RenderHtml for Resource + where + T: RenderHtml + Send + Sync + Clone, + Ser: Send + 'static, + R: Renderer, + { + type AsyncOutput = Option; + + const MIN_LENGTH: usize = 0; + + fn dry_resolve(&mut self) { + self.read(); + } + + fn resolve(self) -> impl Future + 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( + self, + buf: &mut StreamBuilder, + position: &mut Position, + ) where + Self: Sized, + { + (move || Suspend(async move { self.await })) + .to_html_async_with_buf::(buf, position); + } + + fn hydrate( + self, + cursor: &Cursor, + position: &PositionState, + ) -> Self::State { + (move || Suspend(async move { self.await })) + .hydrate::(cursor, position) + } + } +} diff --git a/reactive_graph/src/owner.rs b/reactive_graph/src/owner.rs index 35c7a4157..f1b095a7f 100644 --- a/reactive_graph/src/owner.rs +++ b/reactive_graph/src/owner.rs @@ -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>>, diff --git a/tachys/src/async_views/mod.rs b/tachys/src/async_views/mod.rs deleted file mode 100644 index 556ea82f8..000000000 --- a/tachys/src/async_views/mod.rs +++ /dev/null @@ -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 - where - Self: Future, - { - Suspend { - fallback: (), - fut: self, - } - } -} - -impl FutureViewExt for F where F: Future + Sized {} - -pub struct Suspend { - pub fallback: Fal, - pub fut: Fut, -} - -impl Suspend { - pub fn with_fallback( - self, - fallback: Fal2, - ) -> Suspend { - let fut = self.fut; - Suspend { fallback, fut } - } - - pub fn transition(self) -> Suspend { - let Suspend { fallback, fut } = self; - Suspend { fallback, fut } - } -} - -impl Debug for Suspend { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SuspendedFuture") - .field("transition", &TRANSITION) - .finish() - } -} - -// TODO make this cancelable -impl Render - for Suspend -where - Fal: Render + 'static, - Fut: Future + 'static, - Fut::Output: Render, - Rndr: Renderer + 'static, -{ - type State = Arc< - RwLock< - EitherState>::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::::Right(value) - .rebuild(&mut *state.write()); - } - }); - } - - state - } - - fn rebuild(self, state: &mut Self::State) { - if !TRANSITION { - // fall back to fallback state - Either::::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::::Right(value) - .rebuild(&mut *state.write()); - } - }); - } -} - -impl AddAnyAttr - for Suspend -where - Fal: RenderHtml + 'static, - Fut: Future + Send + 'static, - Fut::Output: RenderHtml + Send, - Rndr: Renderer + 'static, -{ - type Output> = Self; - - fn add_any_attr>( - self, - _attr: NewAttr, - ) -> Self::Output - where - Self::Output: RenderHtml, - { - todo!() - } -} - -impl RenderHtml - for Suspend -where - Fal: RenderHtml + 'static, - Fut: Future + Send + 'static, - Fut::Output: RenderHtml + 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 + Send { - self.fut - } - - fn to_html_with_buf(self, buf: &mut String, position: &mut Position) { - Either::::Left(self.fallback) - .to_html_with_buf(buf, position); - } - - fn to_html_async_with_buf( - 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::::Right(resolved) - .to_html_async_with_buf::(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::::Right(value) - .to_html_async_with_buf::( - &mut builder, - &mut position, - ); - builder.finish().take_chunks() - } - }, - ); - *position = Position::NextChild; - } - } - }; - } - - fn hydrate( - self, - cursor: &Cursor, - 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::(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::::Right(value) - .rebuild(&mut *state.write()); - } - }); - } - - state - } -} - -impl Mountable - for Arc>> -where - Fal: Mountable, - Output: Mountable, - Rndr: Renderer, -{ - fn unmount(&mut self) { - self.write().unmount(); - } - - fn mount( - &mut self, - parent: &::Element, - marker: Option<&::Node>, - ) { - self.write().mount(parent, marker); - } - - fn insert_before_this( - &self, - parent: &::Element, - child: &mut dyn Mountable, - ) -> bool { - self.write().insert_before_this(parent, child) - } -} diff --git a/tachys/src/lib.rs b/tachys/src/lib.rs index 3b7a5954e..faa8d2632 100644 --- a/tachys/src/lib.rs +++ b/tachys/src/lib.rs @@ -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; diff --git a/tachys/src/reactive_graph/mod.rs b/tachys/src/reactive_graph/mod.rs index 543813977..2cfe12a1c 100644 --- a/tachys/src/reactive_graph/mod.rs +++ b/tachys/src/reactive_graph/mod.rs @@ -18,7 +18,9 @@ pub mod node_ref; mod owned; mod property; mod style; +mod suspense; pub use owned::*; +pub use suspense::*; impl ToTemplate for F where diff --git a/tachys/src/reactive_graph/suspense.rs b/tachys/src/reactive_graph/suspense.rs new file mode 100644 index 000000000..b1685ea19 --- /dev/null +++ b/tachys/src/reactive_graph/suspense.rs @@ -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 + where + Self: Future, + { + Suspend(self) + } +} + +impl 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(pub Fut); + +impl Debug for Suspend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Suspend").finish() + } +} + +pub struct SuspendState +where + T: Render, + Rndr: Renderer, +{ + inner: Rc>>, +} + +impl Mountable for SuspendState +where + T: Render, + 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, + ) -> bool { + self.inner.borrow_mut().insert_before_this(parent, child) + } +} + +impl Render for Suspend +where + Fut: Future + 'static, + Fut::Output: Render, + Rndr: Renderer + 'static, +{ + type State = SuspendState; + + // 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::().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::().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 AddAnyAttr for Suspend +where + Fut: Future + Send + 'static, + Fut::Output: AddAnyAttr, + Rndr: Renderer + 'static, +{ + type Output> = Suspend< + Pin< + Box< + dyn Future< + Output = >::Output< + SomeNewAttr::CloneableOwned, + >, + > + Send, + >, + >, + >; + + fn add_any_attr>( + self, + attr: NewAttr, + ) -> Self::Output + where + Self::Output: RenderHtml, + { + let attr = attr.into_cloneable_owned(); + Suspend(Box::pin(async move { + let this = self.0.await; + this.add_any_attr(attr) + })) + } +} + +impl RenderHtml for Suspend +where + Fut: Future + Send + 'static, + Fut::Output: RenderHtml, + Rndr: Renderer + 'static, +{ + type AsyncOutput = Option; + + 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( + self, + _buf: &mut StreamBuilder, + _position: &mut Position, + ) where + Self: Sized, + { + todo!(); + } + + // TODO cancellation + fn hydrate( + self, + cursor: &Cursor, + 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::(cursor, position), + )); + + // get a unique ID if there's a SuspenseContext + let id = use_context::().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) {} +} diff --git a/tachys/src/view/primitives.rs b/tachys/src/view/primitives.rs index b090433bf..8f595e19c 100644 --- a/tachys/src/view/primitives.rs +++ b/tachys/src/view/primitives.rs @@ -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");